~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/bugs/model/bug.py

  • Committer: Stuart Bishop
  • Date: 2011-09-28 12:49:24 UTC
  • mfrom: (9893.10.1 trivial)
  • mto: This revision was merged to the branch mainline in revision 14178.
  • Revision ID: stuart.bishop@canonical.com-20110928124924-m5a22fymqghw6c5i
Merged trivial into distinct-db-users.

Show diffs side-by-side

added added

removed removed

Lines of Context:
30
30
from functools import wraps
31
31
from itertools import chain
32
32
import operator
33
 
import pytz
34
33
import re
35
34
 
36
35
from lazr.lifecycle.event import (
39
38
    ObjectModifiedEvent,
40
39
    )
41
40
from lazr.lifecycle.snapshot import Snapshot
 
41
import pytz
42
42
from pytz import timezone
43
43
from sqlobject import (
44
44
    BoolCol,
122
122
    BranchLinkedToBug,
123
123
    BranchUnlinkedFromBug,
124
124
    BugConvertedToQuestion,
 
125
    BugDuplicateChange,
125
126
    BugWatchAdded,
126
127
    BugWatchRemoved,
127
 
    BugDuplicateChange,
128
128
    SeriesNominated,
129
129
    UnsubscribedFromBug,
130
130
    )
158
158
from lp.bugs.interfaces.bugwatch import IBugWatchSet
159
159
from lp.bugs.interfaces.cve import ICveSet
160
160
from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
 
161
from lp.bugs.model.bugactivity import BugActivity
161
162
from lp.bugs.model.bugattachment import BugAttachment
162
163
from lp.bugs.model.bugbranch import BugBranch
163
164
from lp.bugs.model.bugcve import BugCve
169
170
from lp.bugs.model.bugtask import (
170
171
    BugTask,
171
172
    bugtask_sort_key,
 
173
    get_bug_privacy_filter,
172
174
    )
173
175
from lp.bugs.model.bugwatch import BugWatch
174
176
from lp.bugs.model.structuralsubscription import (
197
199
from lp.registry.model.pillar import pillar_sort_key
198
200
from lp.registry.model.teammembership import TeamParticipation
199
201
from lp.services.database.stormbase import StormBase
 
202
from lp.services.features import getFeatureFlag
200
203
from lp.services.fields import DuplicateBug
201
204
from lp.services.messages.interfaces.message import (
202
205
    IMessage,
811
814
        self.updateHeat()
812
815
        return sub
813
816
 
814
 
    def unsubscribe(self, person, unsubscribed_by):
 
817
    def unsubscribe(self, person, unsubscribed_by, **kwargs):
815
818
        """See `IBug`."""
816
819
        # Drop cached subscription info.
817
820
        clear_property_cache(self)
821
824
        if person is None:
822
825
            person = unsubscribed_by
823
826
 
 
827
        ignore_permissions = kwargs.get('ignore_permissions', False)
 
828
        recipients = kwargs.get('recipients')
824
829
        for sub in self.subscriptions:
825
830
            if sub.person.id == person.id:
826
 
                if not sub.canBeUnsubscribedByUser(unsubscribed_by):
 
831
                if (not ignore_permissions
 
832
                        and not sub.canBeUnsubscribedByUser(unsubscribed_by)):
827
833
                    raise UserCannotUnsubscribePerson(
828
834
                        '%s does not have permission to unsubscribe %s.' % (
829
835
                            unsubscribed_by.displayname,
830
836
                            person.displayname))
831
837
 
832
838
                self.addChange(UnsubscribedFromBug(
833
 
                    when=UTC_NOW, person=unsubscribed_by,
834
 
                    unsubscribed_user=person))
 
839
                        when=UTC_NOW, person=unsubscribed_by,
 
840
                        unsubscribed_user=person, **kwargs),
 
841
                    recipients=recipients)
835
842
                store = Store.of(sub)
836
843
                store.remove(sub)
837
844
                # Make sure that the subscription removal has been
1644
1651
 
1645
1652
        return bugtask
1646
1653
 
1647
 
    def setPrivate(self, private, who):
1648
 
        """See `IBug`.
1649
 
 
1650
 
        We also record who made the change and when the change took
1651
 
        place.
1652
 
        """
 
1654
    def setPrivacyAndSecurityRelated(self, private, security_related, who):
 
1655
        """ See `IBug`."""
 
1656
        private_changed = False
 
1657
        security_related_changed = False
 
1658
        bug_before_modification = Snapshot(self, providing=providedBy(self))
 
1659
 
 
1660
        f_flag_str = 'disclosure.enhanced_private_bug_subscriptions.enabled'
 
1661
        f_flag = bool(getFeatureFlag(f_flag_str))
 
1662
        if f_flag:
 
1663
            # Before we update the privacy or security_related status, we need to
 
1664
            # reconcile the subscribers to avoid leaking private information.
 
1665
            if (self.private != private
 
1666
                    or self.security_related != security_related):
 
1667
                self.reconcileSubscribers(private, security_related, who)
 
1668
 
1653
1669
        if self.private != private:
1654
 
            if private:
1655
 
                # Change indirect subscribers into direct subscribers
1656
 
                # *before* setting private because
1657
 
                # getIndirectSubscribers() behaves differently when
1658
 
                # the bug is private.
1659
 
                for person in self.getIndirectSubscribers():
1660
 
                    self.subscribe(person, who)
1661
 
                subscribers_for_who = self.getSubscribersForPerson(who)
1662
 
                if subscribers_for_who.is_empty():
1663
 
                    # We also add `who` as a subscriber, if they're not
1664
 
                    # already directly subscribed or part of a team
1665
 
                    # that's directly subscribed, so that they can
1666
 
                    # see the bug they've just marked private.
1667
 
                    self.subscribe(who, who)
1668
 
 
 
1670
            private_changed = True
1669
1671
            self.private = private
1670
1672
 
1671
1673
            if private:
1680
1682
            for attachment in self.attachments_unpopulated:
1681
1683
                attachment.libraryfile.restricted = private
1682
1684
 
1683
 
            # Correct the heat for the bug immediately, so that we don't have
1684
 
            # to wait for the next calculation job for the adjusted heat.
1685
 
            self.updateHeat()
1686
 
            return True  # Changed.
1687
 
        else:
1688
 
            return False  # Not changed.
1689
 
 
1690
 
    def setSecurityRelated(self, security_related):
1691
 
        """Setter for the `security_related` property."""
1692
1685
        if self.security_related != security_related:
 
1686
            security_related_changed = True
1693
1687
            self.security_related = security_related
1694
1688
 
 
1689
        if private_changed or security_related_changed:
1695
1690
            # Correct the heat for the bug immediately, so that we don't have
1696
1691
            # to wait for the next calculation job for the adjusted heat.
1697
1692
            self.updateHeat()
1698
1693
 
1699
 
            return True  # Changed
1700
 
        else:
1701
 
            return False  # Unchanged
 
1694
        if private_changed or security_related_changed:
 
1695
            changed_fields = []
 
1696
            if private_changed:
 
1697
                changed_fields.append('private')
 
1698
            if security_related_changed:
 
1699
                changed_fields.append('security_related')
 
1700
                if not f_flag and security_related:
 
1701
                    # The bug turned out to be security-related, subscribe the
 
1702
                    # security contact. We do it here only if the feature flag
 
1703
                    # is not set, otherwise it's done in
 
1704
                    # reconcileSubscribers().
 
1705
                    for pillar in self.affected_pillars:
 
1706
                        if pillar.security_contact is not None:
 
1707
                            self.subscribe(pillar.security_contact, who)
 
1708
            notify(ObjectModifiedEvent(
 
1709
                    self, bug_before_modification, changed_fields, user=who))
 
1710
 
 
1711
        return private_changed, security_related_changed
 
1712
 
 
1713
    def setPrivate(self, private, who):
 
1714
        """See `IBug`.
 
1715
 
 
1716
        We also record who made the change and when the change took
 
1717
        place.
 
1718
        """
 
1719
        return self.setPrivacyAndSecurityRelated(
 
1720
            private, self.security_related, who)[0]
 
1721
 
 
1722
    def setSecurityRelated(self, security_related, who):
 
1723
        """Setter for the `security_related` property."""
 
1724
        return self.setPrivacyAndSecurityRelated(
 
1725
            self.private, security_related, who)[1]
 
1726
 
 
1727
    def getRequiredSubscribers(self, for_private, for_security_related, who):
 
1728
        """Return the mandatory subscribers for a bug with given attributes.
 
1729
 
 
1730
        When a bug is marked as private or security related, it is required
 
1731
        that certain people be subscribed so they can access details about the
 
1732
        bug. The rules are:
 
1733
            security=true, private=true/false ->
 
1734
                subscribers = the reporter + security contact for each task
 
1735
            security=false, private=true ->
 
1736
                subscribers = the reporter + bug supervisor for each task
 
1737
            security=false, private=false ->
 
1738
                subscribers = ()
 
1739
 
 
1740
        If bug supervisor or security contact is unset, fallback to bugtask
 
1741
        reporter/owner.
 
1742
        """
 
1743
        if not for_private and not for_security_related:
 
1744
            return set()
 
1745
        result = set()
 
1746
        result.add(self.owner)
 
1747
        for bugtask in self.bugtasks:
 
1748
            maintainer = bugtask.pillar.owner
 
1749
            if for_security_related:
 
1750
                result.add(bugtask.pillar.security_contact or maintainer)
 
1751
            if for_private:
 
1752
                result.add(bugtask.pillar.bug_supervisor or maintainer)
 
1753
        if for_private:
 
1754
            subscribers_for_who = self.getSubscribersForPerson(who)
 
1755
            if subscribers_for_who.is_empty():
 
1756
                result.add(who)
 
1757
        return result
 
1758
 
 
1759
    def getAutoRemovedSubscribers(self, for_private, for_security_related):
 
1760
        """Return the to be removed subscribers for bug with given attributes.
 
1761
 
 
1762
        When a bug's privacy or security related attributes change, some
 
1763
        existing subscribers may need to be automatically removed.
 
1764
        The rules are:
 
1765
            security=false ->
 
1766
                auto removed subscribers = (bugtask security contacts)
 
1767
            privacy=false ->
 
1768
                auto removed subscribers = (bugtask bug supervisors)
 
1769
 
 
1770
        """
 
1771
        bug_supervisors = []
 
1772
        security_contacts = []
 
1773
        for pillar in self.affected_pillars:
 
1774
            if (self.security_related and not for_security_related
 
1775
                and pillar.security_contact):
 
1776
                    security_contacts.append(pillar.security_contact)
 
1777
            if (self.private and not for_private
 
1778
                and pillar.bug_supervisor):
 
1779
                    bug_supervisors.append(pillar.bug_supervisor)
 
1780
        return bug_supervisors, security_contacts
 
1781
 
 
1782
    def reconcileSubscribers(self, for_private, for_security_related, who):
 
1783
        """ Ensure only appropriate people are subscribed to private bugs.
 
1784
 
 
1785
        When a bug is marked as either private = True or security_related =
 
1786
        True, we need to ensure that only people who are authorised to know
 
1787
        about the privileged contents of the bug remain directly subscribed
 
1788
        to it. So we:
 
1789
          1. Get the required subscribers depending on the bug status.
 
1790
          2. Get the auto removed subscribers depending on the bug status.
 
1791
             eg security contacts when a bug is updated to security related =
 
1792
             false.
 
1793
          3. Get the allowed subscribers = required subscribers
 
1794
                                            + bugtask owners
 
1795
          4. Remove any current direct subscribers who are not allowed or are
 
1796
             to be auto removed.
 
1797
          5. Add any subscribers who are required.
 
1798
        """
 
1799
        current_direct_subscribers = (
 
1800
            self.getSubscriptionInfo().direct_subscribers)
 
1801
        required_subscribers = self.getRequiredSubscribers(
 
1802
            for_private, for_security_related, who)
 
1803
        removed_bug_supervisors, removed_security_contacts = (
 
1804
            self.getAutoRemovedSubscribers(for_private, for_security_related))
 
1805
        for subscriber in removed_bug_supervisors:
 
1806
            recipients = BugNotificationRecipients()
 
1807
            recipients.addBugSupervisor(subscriber)
 
1808
            notification_text = ("This bug is no longer private so the bug "
 
1809
                "supervisor was unsubscribed. They will no longer be "
 
1810
                "notified of changes to this bug for privacy related "
 
1811
                "reasons, but may receive notifications about this bug from "
 
1812
                "other subscriptions.")
 
1813
            self.unsubscribe(
 
1814
                subscriber, who, ignore_permissions=True,
 
1815
                send_notification=True,
 
1816
                notification_text=notification_text,
 
1817
                recipients=recipients)
 
1818
        for subscriber in removed_security_contacts:
 
1819
            recipients = BugNotificationRecipients()
 
1820
            recipients.addSecurityContact(subscriber)
 
1821
            notification_text = ("This bug is no longer security related so "
 
1822
                "the security contact was unsubscribed. They will no longer "
 
1823
                "be notified of changes to this bug for security related "
 
1824
                "reasons, but may receive notifications about this bug "
 
1825
                "from other subscriptions.")
 
1826
            self.unsubscribe(
 
1827
                subscriber, who, ignore_permissions=True,
 
1828
                send_notification=True,
 
1829
                notification_text=notification_text,
 
1830
                recipients=recipients)
 
1831
 
 
1832
        # If this bug is for a project that is marked as having private bugs
 
1833
        # by default, and the bug is private or security related, we will
 
1834
        # unsubscribe any unauthorised direct subscribers.
 
1835
        pillar = self.default_bugtask.pillar
 
1836
        private_project = IProduct.providedBy(pillar) and pillar.private_bugs
 
1837
        if private_project and (for_private or for_security_related):
 
1838
            allowed_subscribers = set()
 
1839
            allowed_subscribers.add(self.owner)
 
1840
            for bugtask in self.bugtasks:
 
1841
                allowed_subscribers.add(bugtask.owner)
 
1842
                allowed_subscribers.add(bugtask.pillar.owner)
 
1843
                allowed_subscribers.update(set(bugtask.pillar.drivers))
 
1844
            allowed_subscribers = required_subscribers.union(
 
1845
                allowed_subscribers)
 
1846
            subscribers_to_remove = (
 
1847
                current_direct_subscribers.difference(allowed_subscribers))
 
1848
            for subscriber in subscribers_to_remove:
 
1849
                self.unsubscribe(subscriber, who, ignore_permissions=True)
 
1850
 
 
1851
        subscribers_to_add = (
 
1852
            required_subscribers.difference(current_direct_subscribers))
 
1853
        for subscriber in subscribers_to_add:
 
1854
            self.subscribe(subscriber, who)
1702
1855
 
1703
1856
    def getBugTask(self, target):
1704
1857
        """See `IBug`."""
2089
2242
            self._attachments_query(),
2090
2243
            operator.itemgetter(0))
2091
2244
 
 
2245
    def getActivityForDateRange(self, start_date, end_date):
 
2246
        """See `IBug`."""
 
2247
        store = Store.of(self)
 
2248
        activity_in_range = store.find(
 
2249
            BugActivity,
 
2250
            BugActivity.bug == self,
 
2251
            BugActivity.datechanged >= start_date,
 
2252
            BugActivity.datechanged <= end_date)
 
2253
        return activity_in_range
 
2254
 
2092
2255
 
2093
2256
@ProxyFactory
2094
2257
def get_also_notified_subscribers(
2453
2616
        if duplicateof:
2454
2617
            where_clauses.append("Bug.duplicateof = %d" % duplicateof.id)
2455
2618
 
2456
 
        admins = getUtility(ILaunchpadCelebrities).admin
2457
 
        if user:
2458
 
            if not user.inTeam(admins):
2459
 
                # Enforce privacy-awareness for logged-in, non-admin users,
2460
 
                # so that they can only see the private bugs that they're
2461
 
                # allowed to see.
2462
 
                where_clauses.append("""
2463
 
                    (Bug.private = FALSE OR
2464
 
                      Bug.id in (
2465
 
                         -- Users who have a subscription to this bug.
2466
 
                         SELECT BugSubscription.bug
2467
 
                           FROM BugSubscription, TeamParticipation
2468
 
                           WHERE
2469
 
                             TeamParticipation.person = %(personid)s AND
2470
 
                             BugSubscription.person = TeamParticipation.team
2471
 
                         UNION
2472
 
                         -- Users who are the assignee for one of the bug's
2473
 
                         -- bugtasks.
2474
 
                         SELECT BugTask.bug
2475
 
                           FROM BugTask, TeamParticipation
2476
 
                           WHERE
2477
 
                             TeamParticipation.person = %(personid)s AND
2478
 
                             TeamParticipation.team = BugTask.assignee
2479
 
                      )
2480
 
                    )""" % sqlvalues(personid=user.id))
2481
 
        else:
2482
 
            # Anonymous user; filter to include only public bugs in
2483
 
            # the search results.
2484
 
            where_clauses.append("Bug.private = FALSE")
 
2619
        privacy_filter = get_bug_privacy_filter(user)
 
2620
        if privacy_filter:
 
2621
            where_clauses.append(privacy_filter)
2485
2622
 
2486
2623
        other_params = {}
2487
2624
        if orderBy: