yellow team mailing list archive
-
yellow team
-
Mailing list archive
-
Message #00270
[Merge] lp:~frankban/launchpad/bug-904335-get-tags into lp:launchpad
Francesco Banconi has proposed merging lp:~frankban/launchpad/bug-904335-get-tags into lp:launchpad with lp:~yellow/launchpad/bug-904335-devel-base as a prerequisite.
Requested reviews:
Canonical Launchpad Engineering (launchpad)
For more details, see:
https://code.launchpad.net/~frankban/launchpad/bug-904335-get-tags/+merge/87484
= Summary =
Project groups needs a way to aggregate milestones, and introducing milestone
tags seems a way to do that in a flexible manner.
== Proposed fix ==
A Project Group Milestone Tag object is needed to aggregate milestones
inside a project group. This patch introduces a convenient interface and
its implementation: the behaviour of the new model must be similar to what
is already present in Project Group Milestones. Above all we need the
ability to retrieve bugtasks and specifications associated with a milestone
tag.
== Pre-implementation notes ==
The concept of milestone tag was controversed but appears to be a good
tradeoff to meet Linaro needs. A db-devel change is needed to get milestone
tags work. That change is present in
lp:~frankban/launchpad/db-milestonetags-480123
== Implementation details ==
A milestone interfaces refactoring is present in this patch.
IMilestoneData is now a base interface for any other milestone-like
interface, and IAbstractMilestone is introduced as an intermediate interface
for milestone.
Both IMilestone and IProjectGroupMilestone are IAbstractMilestone subclasses.
Both IMilestone and IProjectGroupMilestoneTag (a marker interface) are
IMilestoneData subclasses.
The milestone model now has getter and setter methods for tags.
A MilestoneTag model is introduced as a storm interface to the new
milestonetag table.
A ProjectGroupMilestoneTag model is used to retrieve bugtasks
and specifications.
== Tests ==
bin/test -vv lp.registry.tests.test_milestone
bin/test -vv lp.registry.tests.test_milestonetag
bin/test -vv -t lp.registry.stories.webservice.xx-project-registry
== Demo and Q/A ==
The changes are not visible in the web ui.
However, due to milestone refactoring, checking the various milestone
pages may be a good idea.
--
https://code.launchpad.net/~frankban/launchpad/bug-904335-get-tags/+merge/87484
Your team Launchpad Yellow Squad is subscribed to branch lp:~yellow/launchpad/bug-904335-devel-base.
=== modified file 'database/sampledata/current-dev.sql'
--- database/sampledata/current-dev.sql 2011-12-13 15:21:25 +0000
+++ database/sampledata/current-dev.sql 2012-01-04 14:49:20 +0000
@@ -1,6 +1,6 @@
-- Copyright 2010-2011 Canonical Ltd. This software is licensed under the
-- GNU Affero General Public License version 3 (see the file LICENSE).
--- Created using pg_dump (PostgreSQL) 8.4.8
+-- Created using pg_dump (PostgreSQL) 8.4.9
SET check_function_bodies = false;
SET client_encoding = 'UTF8';
@@ -5752,6 +5752,13 @@
ALTER TABLE messagechunk ENABLE TRIGGER ALL;
+ALTER TABLE milestonetag DISABLE TRIGGER ALL;
+
+
+
+ALTER TABLE milestonetag ENABLE TRIGGER ALL;
+
+
ALTER TABLE mirror DISABLE TRIGGER ALL;
=== modified file 'database/sampledata/current.sql'
--- database/sampledata/current.sql 2011-12-13 15:21:25 +0000
+++ database/sampledata/current.sql 2012-01-04 14:49:20 +0000
@@ -1,6 +1,6 @@
-- Copyright 2010-2011 Canonical Ltd. This software is licensed under the
-- GNU Affero General Public License version 3 (see the file LICENSE).
--- Created using pg_dump (PostgreSQL) 8.4.8
+-- Created using pg_dump (PostgreSQL) 8.4.9
SET check_function_bodies = false;
SET client_encoding = 'UTF8';
@@ -5682,6 +5682,13 @@
ALTER TABLE messagechunk ENABLE TRIGGER ALL;
+ALTER TABLE milestonetag DISABLE TRIGGER ALL;
+
+
+
+ALTER TABLE milestonetag ENABLE TRIGGER ALL;
+
+
ALTER TABLE mirror DISABLE TRIGGER ALL;
=== modified file 'database/schema/comments.sql'
--- database/schema/comments.sql 2011-11-17 13:06:30 +0000
+++ database/schema/comments.sql 2012-01-04 14:49:20 +0000
@@ -2425,3 +2425,8 @@
'OpenId Identifiers that can be used to log into an Account.';
COMMENT ON COLUMN OpenIdIdentifier.identifier IS
'OpenId Identifier. This should be a URL, but is currently just a token that can be used to generate the Identity URL for the Canonical SSO OpenId Provider.';
+
+-- MilestoneTag
+COMMENT ON TABLE milestonetag IS 'Attaches simple text tags to a milestone.';
+COMMENT ON COLUMN milestonetag.milestone IS 'The milestone the tag is attached to.';
+COMMENT ON COLUMN milestonetag.tag IS 'The text representation of the tag.';
=== added file 'database/schema/patch-2209-00-3.sql'
--- database/schema/patch-2209-00-3.sql 1970-01-01 00:00:00 +0000
+++ database/schema/patch-2209-00-3.sql 2012-01-04 14:49:20 +0000
@@ -0,0 +1,22 @@
+-- Copyright 2011 Canonical Ltd. This software is licensed under the
+-- GNU Affero General Public License version 3 (see the file LICENSE).
+
+SET client_min_messages=ERROR;
+
+CREATE TABLE milestonetag (
+ id SERIAL PRIMARY KEY,
+ milestone integer NOT NULL REFERENCES milestone ON DELETE CASCADE,
+ tag text NOT NULL,
+ date_created timestamp without time zone DEFAULT
+ timezone('UTC'::text, now()) NOT NULL,
+ created_by integer NOT NULL REFERENCES person,
+ CONSTRAINT valid_tag CHECK (valid_name(tag))
+);
+
+ALTER TABLE ONLY milestonetag
+ ADD CONSTRAINT milestonetag__tag__milestone__key UNIQUE (tag, milestone);
+
+CREATE INDEX milestonetag__milestones_idx
+ ON milestonetag USING btree (milestone);
+
+INSERT INTO LaunchpadDatabaseRevision VALUES (2209, 0, 3);
=== added file 'database/schema/patch-2209-00-4.sql'
--- database/schema/patch-2209-00-4.sql 1970-01-01 00:00:00 +0000
+++ database/schema/patch-2209-00-4.sql 2012-01-04 14:49:20 +0000
@@ -0,0 +1,57 @@
+SET client_min_messages=ERROR;
+
+CREATE OR REPLACE FUNCTION check_email_address_person_account(
+ person integer, account integer)
+ RETURNS boolean
+ LANGUAGE plpythonu IMMUTABLE RETURNS NULL ON NULL INPUT AS
+$$
+ # It's possible for an EmailAddress to be created without an
+ # account. If that happens, and this function is called, we return
+ # True so as to avoid breakages.
+ if account is None:
+ return True
+ results = plpy.execute("""
+ SELECT account FROM Person WHERE id = %s""" % person)
+ # If there are no accounts with that Person in the DB, or the Person
+ # is new and hasn't yet been linked to an account, return success
+ # anyway. This helps avoid the PGRestore breaking (and referential
+ # integrity will prevent this from causing bugs later.
+ if results.nrows() == 0 or results[0]['account'] is None:
+ return True
+ return results[0]['account'] == account
+$$;
+
+COMMENT ON FUNCTION check_email_address_person_account(integer, integer) IS
+'Check that the person to which an email address is linked has the same account as that email address.';
+
+CREATE OR REPLACE FUNCTION check_person_email_address_account(
+ person integer, account integer)
+ RETURNS boolean
+ LANGUAGE plpythonu IMMUTABLE RETURNS NULL ON NULL INPUT AS
+$$
+ # It's possible for a Person to be created without an account. If
+ # that happens, return True so that things don't break.
+ if account is None:
+ return True
+ email_address_accounts = plpy.execute("""
+ SELECT account FROM EmailAddress WHERE
+ person = %s AND account IS NOT NULL""" % person)
+ # If there are no email address accounts to check, we're done.
+ if email_address_accounts.nrows() == 0:
+ return True
+ for email_account_row in email_address_accounts:
+ email_account = email_account_row['account']
+ if email_account is not None and email_account != account:
+ return False
+ return True
+$$;
+
+COMMENT ON FUNCTION check_person_email_address_account(integer, integer) IS
+'Check that the email addresses linked to a person have the same account ID as that person.';
+
+ALTER TABLE EmailAddress ADD CONSTRAINT valid_account_for_person
+ CHECK (check_email_address_person_account(person, account));
+ALTER TABLE Person ADD CONSTRAINT valid_account_for_emailaddresses
+ CHECK (check_person_email_address_account(id, account));
+
+INSERT INTO LaunchpadDatabaseRevision VALUES (2209, 00, 4);
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2012-01-03 01:40:09 +0000
+++ database/schema/security.cfg 2012-01-04 14:49:20 +0000
@@ -18,6 +18,8 @@
public.bug_update_latest_patch_uploaded(integer) =
public.bugnotificationarchive =
public.calculate_bug_heat(integer) = EXECUTE
+public.check_email_address_person_account(integer, integer) = EXECUTE
+public.check_person_email_address_account(integer, integer) = EXECUTE
public.cursor_fetch(refcursor, integer) = EXECUTE
public.databasediskutilization =
public.debversion(character) = EXECUTE
@@ -221,6 +223,7 @@
public.messageapproval = SELECT, INSERT, UPDATE, DELETE
public.messagechunk = SELECT, INSERT
public.milestone = SELECT, INSERT, UPDATE, DELETE
+public.milestonetag = SELECT, INSERT, UPDATE, DELETE
public.mirrorcdimagedistroseries = SELECT, INSERT, DELETE
public.mirrordistroarchseries = SELECT, INSERT, DELETE, UPDATE
public.mirrordistroseriessource = SELECT, INSERT, UPDATE, DELETE
@@ -591,6 +594,7 @@
public.message = SELECT, INSERT
public.messagechunk = SELECT, INSERT
public.milestone = SELECT
+public.milestonetag = SELECT
public.person = SELECT, INSERT, UPDATE
public.personlanguage = SELECT
public.personsettings = SELECT, INSERT
@@ -649,6 +653,7 @@
public.karmaaction = SELECT
public.message = SELECT, INSERT
public.messagechunk = SELECT, INSERT
+public.milestonetag = SELECT
public.person = SELECT
public.revision = SELECT, INSERT, UPDATE
public.revisionauthor = SELECT, INSERT, UPDATE
@@ -862,6 +867,7 @@
public.message = SELECT, INSERT
public.messagechunk = SELECT, INSERT
public.milestone = SELECT
+public.milestonetag = SELECT
public.packagecopyjob = SELECT, INSERT, DELETE
public.packagecopyrequest = SELECT, INSERT, UPDATE
public.packagediff = SELECT, INSERT, UPDATE
@@ -1301,6 +1307,7 @@
public.message = SELECT, INSERT
public.messagechunk = SELECT, INSERT
public.milestone = SELECT
+public.milestonetag = SELECT
public.packagebuild = SELECT, INSERT, UPDATE
public.packagecopyjob = SELECT, INSERT
public.packagediff = SELECT, INSERT, UPDATE, DELETE
@@ -1404,6 +1411,7 @@
public.message = SELECT, INSERT
public.messagechunk = SELECT, INSERT
public.milestone = SELECT
+public.milestonetag = SELECT
public.packagebuild = SELECT, INSERT, UPDATE
public.packagecopyjob = SELECT, INSERT, UPDATE
public.packagediff = SELECT, UPDATE
@@ -1502,6 +1510,7 @@
public.message = SELECT, INSERT
public.messagechunk = SELECT, INSERT
public.milestone = SELECT
+public.milestonetag = SELECT
public.person = SELECT
public.personlanguage = SELECT
public.personsettings = SELECT
@@ -1706,6 +1715,7 @@
public.message = SELECT, INSERT
public.messagechunk = SELECT, INSERT
public.milestone = SELECT
+public.milestonetag = SELECT, INSERT, DELETE
public.packageset = SELECT
public.packagesetgroup = SELECT
public.packagesetinclusion = SELECT
@@ -1946,6 +1956,7 @@
public.job = SELECT, INSERT, UPDATE
public.message = SELECT, INSERT
public.messagechunk = SELECT, INSERT
+public.milestonetag = SELECT
public.person = SELECT, INSERT
public.personsettings = SELECT, INSERT
public.product = SELECT, INSERT, UPDATE
@@ -2044,6 +2055,7 @@
public.message = SELECT, UPDATE
public.messageapproval = SELECT, UPDATE
public.milestone = SELECT, UPDATE
+public.milestonetag = SELECT, INSERT, UPDATE, DELETE
public.mirror = SELECT, UPDATE
public.nameblacklist = SELECT, UPDATE
public.oauthaccesstoken = SELECT, UPDATE
@@ -2146,6 +2158,7 @@
public.job = SELECT, INSERT, DELETE
public.logintoken = SELECT, DELETE
public.mailinglistsubscription = SELECT, DELETE
+public.milestonetag = SELECT
public.oauthnonce = SELECT, DELETE
public.openidconsumerassociation = SELECT, DELETE
public.openidconsumernonce = SELECT, DELETE
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2011-12-30 08:03:42 +0000
+++ lib/lp/bugs/browser/bugtask.py 2012-01-04 14:49:20 +0000
@@ -3504,7 +3504,7 @@
IDistributionSourcePackage.providedBy(self.context)):
search_params.setSourcePackage(self.context)
else:
- raise AssertionError('Uknown context type: %s' % self.context)
+ raise AssertionError('Unknown context type: %s' % self.context)
return u"".join("%d\n" % bug_id for bug_id in
getUtility(IBugTaskSet).searchBugIds(search_params))
=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
--- lib/lp/bugs/interfaces/bugtask.py 2012-01-01 02:58:52 +0000
+++ lib/lp/bugs/interfaces/bugtask.py 2012-01-04 14:49:20 +0000
@@ -1193,9 +1193,9 @@
def __init__(self, user, bug=None, searchtext=None, fast_searchtext=None,
status=None, importance=None, milestone=None,
- assignee=None, sourcepackagename=None, owner=None,
- attachmenttype=None, orderby=None, omit_dupes=False,
- subscriber=None, component=None,
+ milestone_tag=None, assignee=None, sourcepackagename=None,
+ owner=None, attachmenttype=None, orderby=None,
+ omit_dupes=False, subscriber=None, component=None,
pending_bugwatch_elsewhere=False, resolved_upstream=False,
open_upstream=False, has_no_upstream_bugtask=False, tag=None,
has_cve=False, bug_supervisor=None, bug_reporter=None,
@@ -1218,6 +1218,7 @@
self.status = status
self.importance = importance
self.milestone = milestone
+ self.milestone_tag = milestone_tag
self.assignee = assignee
self.sourcepackagename = sourcepackagename
self.owner = owner
=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py 2011-12-30 06:14:56 +0000
+++ lib/lp/bugs/model/bugtask.py 2012-01-04 14:49:20 +0000
@@ -117,6 +117,7 @@
IMilestoneSet,
IProjectGroupMilestone,
)
+from lp.registry.interfaces.milestonetag import IProjectGroupMilestoneTag
from lp.registry.interfaces.person import (
IPerson,
validate_person,
@@ -2052,6 +2053,13 @@
if params.status is not None:
extra_clauses.append(self._buildStatusClause(params.status))
+ if (params.exclude_conjoined_tasks and
+ not (params.milestone or params.milestone_tag)):
+ raise ValueError(
+ "BugTaskSearchParam.exclude_conjoined cannot be True if "
+ "BugTaskSearchParam.milestone or "
+ "BugTaskSearchParam.milestone_tag is not set")
+
if params.milestone:
if IProjectGroupMilestone.providedBy(params.milestone):
where_cond = """
@@ -2071,10 +2079,29 @@
params.milestone)
join_tables += tables
extra_clauses += clauses
- elif params.exclude_conjoined_tasks:
- raise ValueError(
- "BugTaskSearchParam.exclude_conjoined cannot be True if "
- "BugTaskSearchParam.milestone is not set")
+
+ if params.milestone_tag:
+ where_cond = """
+ IN (SELECT Milestone.id
+ FROM Milestone, Product, MilestoneTag
+ WHERE Milestone.product = Product.id
+ AND Product.project = %s
+ AND MilestoneTag.milestone = Milestone.id
+ AND MilestoneTag.tag IN %s
+ GROUP BY Milestone.id
+ HAVING COUNT(Milestone.id) = %s)
+ """ % sqlvalues(params.milestone_tag.target,
+ params.milestone_tag.tags,
+ len(params.milestone_tag.tags))
+ extra_clauses.append("BugTask.milestone %s" % where_cond)
+
+ # XXX frankban 2011-12-16 further investigation needed
+ # to make sure we can skip the _buildExcludeConjoinedClause call
+ # if params.exclude_conjoined_tasks:
+ # tables, clauses = self._buildExcludeConjoinedClause(
+ # params.milestone_tag)
+ # join_tables += tables
+ # extra_clauses += clauses
if params.project:
# Prevent circular import problems.
@@ -2870,12 +2897,18 @@
result[row[:-1]] = row[-1]
return result
- def getPrecachedNonConjoinedBugTasks(self, user, milestone):
+ def getPrecachedNonConjoinedBugTasks(self, user, milestone_data):
"""See `IBugTaskSet`."""
- params = BugTaskSearchParams(
- user, milestone=milestone,
- orderby=['status', '-importance', 'id'],
- omit_dupes=True, exclude_conjoined_tasks=True)
+ kwargs = {
+ 'orderby': ['status', '-importance', 'id'],
+ 'omit_dupes': True,
+ 'exclude_conjoined_tasks': True,
+ }
+ if IProjectGroupMilestoneTag.providedBy(milestone_data):
+ kwargs['milestone_tag'] = milestone_data
+ else:
+ kwargs['milestone'] = milestone_data
+ params = BugTaskSearchParams(user, **kwargs)
return self.search(params)
def createTask(self, bug, owner, target,
=== modified file 'lib/lp/hardwaredb/doc/hwdb.txt'
--- lib/lp/hardwaredb/doc/hwdb.txt 2011-12-18 13:45:20 +0000
+++ lib/lp/hardwaredb/doc/hwdb.txt 2012-01-04 14:49:20 +0000
@@ -109,7 +109,7 @@
Limitations:
* "No name" products like mainboards from companies like ASRock
or Asus that are directly sold to end users have fingerprints like
- "American Megatrends Inc. Uknown 1.0".
+ "American Megatrends Inc. Unknown 1.0".
* A manufacturer may erroneously assign identical DMI values for product
and vendor to different systems.
* submissions for "counterfeit systems".
@@ -743,4 +743,3 @@
>>> set(submission.owner.name for submission
... in owner.hardware_submissions)
set([u'name12'])
-
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml 2011-12-24 17:49:30 +0000
+++ lib/lp/registry/browser/configure.zcml 2012-01-04 14:49:20 +0000
@@ -1279,14 +1279,14 @@
MilestoneNavigation"/>
<adapter
provides="lp.services.webapp.interfaces.IBreadcrumb"
- for="lp.registry.interfaces.milestone.IMilestone"
+ for="lp.registry.interfaces.milestone.IMilestoneData"
factory="lp.registry.browser.milestone.MilestoneBreadcrumb"
permission="zope.Public"/>
<browser:defaultView
- for="lp.registry.interfaces.milestone.IMilestone"
+ for="lp.registry.interfaces.milestone.IMilestoneData"
name="+index"/>
<browser:url
- for="lp.registry.interfaces.milestone.IMilestone"
+ for="lp.registry.interfaces.milestone.IMilestoneData"
path_expression="string:+milestone/${name}"
rootsite="mainsite"
attribute_to_parent="target"/>
@@ -1297,7 +1297,7 @@
template="../templates/milestone-macros.pt"
class="lp.app.browser.launchpad.Macro"/>
<browser:pages
- for="lp.registry.interfaces.milestone.IMilestone"
+ for="lp.registry.interfaces.milestone.IMilestoneData"
class="lp.registry.browser.milestone.MilestoneView"
permission="zope.Public">
<browser:page
=== modified file 'lib/lp/registry/browser/milestone.py'
--- lib/lp/registry/browser/milestone.py 2012-01-01 02:58:52 +0000
+++ lib/lp/registry/browser/milestone.py 2012-01-04 14:49:20 +0000
@@ -52,6 +52,7 @@
from lp.registry.browser.product import ProductDownloadFileMixin
from lp.registry.interfaces.distroseries import IDistroSeries
from lp.registry.interfaces.milestone import (
+ IAbstractMilestone,
IMilestone,
IMilestoneSet,
IProjectGroupMilestone,
@@ -84,15 +85,15 @@
class MilestoneNavigation(Navigation,
StructuralSubscriptionTargetTraversalMixin):
"""The navigation to traverse to a milestone."""
- usedfor = IMilestone
+ usedfor = IAbstractMilestone
class MilestoneBreadcrumb(Breadcrumb):
- """The Breadcrumb for an `IMilestone`."""
+ """The Breadcrumb for an `IAbstractMilestone`."""
@property
def text(self):
- milestone = IMilestone(self.context)
+ milestone = IAbstractMilestone(self.context)
if milestone.code_name:
return '%s "%s"' % (milestone.name, milestone.code_name)
else:
@@ -139,7 +140,7 @@
class MilestoneContextMenu(ContextMenu, MilestoneLinkMixin):
"""The menu for this milestone."""
- usedfor = IMilestone
+ usedfor = IAbstractMilestone
@cachedproperty
def links(self):
@@ -151,7 +152,7 @@
class MilestoneOverviewNavigationMenu(NavigationMenu, MilestoneLinkMixin):
"""Overview navigation menu for `IMilestone` objects."""
- usedfor = IMilestone
+ usedfor = IAbstractMilestone
facet = 'overview'
@cachedproperty
@@ -165,7 +166,7 @@
"""Overview menus for `IMilestone` objects."""
# This menu must not contain 'subscribe' because the link state is too
# costly to calculate when this menu is used with a list of milestones.
- usedfor = IMilestone
+ usedfor = IAbstractMilestone
facet = 'overview'
links = ('edit', 'create_release')
@@ -199,7 +200,7 @@
:param request: `ILaunchpadRequest`.
"""
super(MilestoneView, self).__init__(context, request)
- if IMilestone.providedBy(context):
+ if IAbstractMilestone.providedBy(context):
self.milestone = context
self.release = context.product_release
else:
@@ -261,9 +262,7 @@
"""The list of non-conjoined bugtasks targeted to this milestone."""
# Put the results in a list so that iterating over it multiple
# times in this method does not make multiple queries.
- non_conjoined_slaves = list(
- getUtility(IBugTaskSet).getPrecachedNonConjoinedBugTasks(
- self.user, self.context))
+ non_conjoined_slaves = self.context.bugtasks(self.user)
# Checking bug permissions is expensive. We know from the query that
# the user has at least launchpad.View on the bugtasks and their bugs.
# NB: this is in principle unneeded due to injection of permission in
=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml 2011-12-30 08:03:42 +0000
+++ lib/lp/registry/configure.zcml 2012-01-04 14:49:20 +0000
@@ -404,8 +404,8 @@
setAliases"/>
<!-- IProjectGroupModerate -->
- <allow
- interface="lp.registry.interfaces.projectgroup.IProjectGroupModerate"/>
+ <allow
+ interface="lp.registry.interfaces.projectgroup.IProjectGroupModerate"/>
<require
permission="launchpad.Moderate"
set_schema="lp.registry.interfaces.projectgroup.IProjectGroupModerate"/>
@@ -1006,6 +1006,7 @@
series_target
displayname
title
+ bugtasks
specifications
product_release"/>
<require
@@ -1013,7 +1014,14 @@
attributes="
createProductRelease
closeBugsAndBlueprints
- destroySelf"/>
+ destroySelf
+ setTags
+ "/>
+ <require
+ permission="zope.Public"
+ attributes="
+ getTags
+ "/>
<allow interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
<allow
interface="lp.bugs.interfaces.bugtarget.IHasBugs"/>
@@ -1245,7 +1253,7 @@
<!-- https://lists.ubuntu.com/mailman/private/launchpad/2007-April/015189.html
for further discussion - stub 20070411 -->
- <!-- Per bug 588773, changing to launchpad.Moderate to allow Registry Experts (~registry) -->
+ <!-- Per bug 588773, changing to launchpad.Moderate to allow Registry Experts (~registry) -->
<require
permission="launchpad.Moderate"
set_attributes="name autoupdate registrant"/>
=== modified file 'lib/lp/registry/interfaces/milestone.py'
--- lib/lp/registry/interfaces/milestone.py 2012-01-01 02:58:52 +0000
+++ lib/lp/registry/interfaces/milestone.py 2012-01-04 14:49:20 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
# pylint: disable-msg=E0211,E0213
@@ -8,9 +8,11 @@
__metaclass__ = type
__all__ = [
+ 'IAbstractMilestone',
'ICanGetMilestonesDirectly',
'IHasMilestones',
'IMilestone',
+ 'IMilestoneData',
'IMilestoneSet',
'IProjectGroupMilestone',
]
@@ -97,20 +99,52 @@
return milestone
-class IMilestone(IHasBugs, IStructuralSubscriptionTarget,
- IHasOfficialBugTags):
- """A milestone, or a targeting point for bugs and other
- release-management items that need coordination.
+class IMilestoneData(IHasBugs, IStructuralSubscriptionTarget,
+ IHasOfficialBugTags):
+ """Interface containing the data for milestones.
+
+ To be registered for views but not instantiated.
"""
- export_as_webservice_entry()
-
id = Int(title=_("Id"))
+
name = exported(
MilestoneNameField(
title=_("Name"),
description=_(
"Only letters, numbers, and simple punctuation are allowed."),
constraint=name_validator))
+ target = exported(
+ Reference(
+ schema=Interface, # IHasMilestones
+ title=_(
+ "The product, distribution, or project group for this "
+ "milestone."),
+ required=False))
+ specifications = Attribute(
+ "A list of specifications targeted to this object.")
+ dateexpected = exported(
+ FormattableDate(title=_("Date Targeted"), required=False,
+ description=_("Example: 2005-11-24")),
+ exported_as='date_targeted')
+ active = exported(
+ Bool(
+ title=_("Active"),
+ description=_("Whether or not this object should be shown "
+ "in web forms for targeting.")),
+ exported_as='is_active')
+ displayname = Attribute("A displayname constructed from the name.")
+ title = exported(
+ TextLine(title=_("A context title for pages."),
+ readonly=True))
+
+ def bugtasks(user):
+ """Get a list of non-conjoined bugtasks visible to this user."""
+
+
+class IAbstractMilestone(IMilestoneData):
+ """An intermediate interface for milestone, or a targeting point for bugs
+ and other release-management items that need coordination.
+ """
code_name = exported(
NoneableTextLine(
title=u'Code name', required=False,
@@ -126,47 +160,24 @@
title=_("Product Series"),
description=_("The product series for which this is a milestone."),
vocabulary="FilteredProductSeries",
- required=False) # for now
+ required=False) # for now
distroseries = Choice(
title=_("Distro Series"),
description=_(
"The distribution series for which this is a milestone."),
vocabulary="FilteredDistroSeries",
- required=False) # for now
- dateexpected = exported(
- FormattableDate(title=_("Date Targeted"), required=False,
- description=_("Example: 2005-11-24")),
- exported_as='date_targeted')
- active = exported(
- Bool(
- title=_("Active"),
- description=_("Whether or not this milestone should be shown "
- "in web forms for bug targeting.")),
- exported_as='is_active')
+ required=False) # for now
summary = exported(
NoneableDescription(
title=_("Summary"),
required=False,
description=_(
"A summary of the features and status of this milestone.")))
- target = exported(
- Reference(
- schema=Interface, # IHasMilestones
- title=_("The product or distribution of this milestone."),
- required=False))
series_target = exported(
Reference(
- schema=Interface, # IHasMilestones
+ schema=Interface, # IHasMilestones
title=_("The productseries or distroseries of this milestone."),
required=False))
- displayname = Attribute("A displayname for this milestone, constructed "
- "from the milestone name.")
- title = exported(
- TextLine(title=_("A milestone context title for pages."),
- readonly=True))
- specifications = Attribute("A list of the specifications targeted to "
- "this milestone.")
-
product_release = exported(
Reference(
schema=IProductRelease,
@@ -211,6 +222,23 @@
release.
"""
+
+class IMilestone(IAbstractMilestone):
+ """Actual interface for milestones."""
+
+ export_as_webservice_entry()
+
+ def setTags(tags, user):
+ """Set the milestone tags.
+
+ :param: tags The list of tags to be associated with milestone.
+ :param: user The user who is updating tags for this milestone.
+ """
+
+ def getTags():
+ """Return the milestone tags in alphabetical order."""
+
+
# Avoid circular imports
IBugTask['milestone'].schema = IMilestone
patch_plain_parameter_type(
@@ -249,8 +277,9 @@
"""Return all visible milestones."""
-class IProjectGroupMilestone(IMilestone):
+class IProjectGroupMilestone(IAbstractMilestone):
"""A marker interface for milestones related to a project"""
+ export_as_webservice_entry()
class IHasMilestones(Interface):
=== added file 'lib/lp/registry/interfaces/milestonetag.py'
--- lib/lp/registry/interfaces/milestonetag.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/interfaces/milestonetag.py 2012-01-04 14:49:20 +0000
@@ -0,0 +1,20 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""MilestoneTag interfaces."""
+
+__metaclass__ = type
+__all__ = [
+ 'IProjectGroupMilestoneTag',
+ ]
+
+
+from lp.registry.interfaces.milestone import IMilestoneData
+
+
+class IProjectGroupMilestoneTag(IMilestoneData):
+ """An IProjectGroupMilestoneTag is a tag aggretating milestones for the
+ ProjectGroup with a given tag or tags.
+
+ This interface is just a marker.
+ """
=== modified file 'lib/lp/registry/model/milestone.py'
--- lib/lp/registry/model/milestone.py 2011-12-30 06:14:56 +0000
+++ lib/lp/registry/model/milestone.py 2012-01-04 14:49:20 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
# pylint: disable-msg=E0611,W0212
@@ -8,6 +8,7 @@
__all__ = [
'HasMilestonesMixin',
'Milestone',
+ 'MilestoneData',
'MilestoneSet',
'ProjectMilestone',
'milestone_sort_key',
@@ -49,6 +50,7 @@
from lp.registry.interfaces.milestone import (
IHasMilestones,
IMilestone,
+ IMilestoneData,
IMilestoneSet,
IProjectGroupMilestone,
)
@@ -129,11 +131,47 @@
super(MultipleProductReleases, self).__init__(msg)
-class Milestone(SQLBase, StructuralSubscriptionTargetMixin, HasBugsBase):
+class MilestoneData:
+ implements(IMilestoneData)
+
+ @property
+ def displayname(self):
+ """See IMilestone."""
+ return "%s %s" % (self.target.displayname, self.name)
+
+ @property
+ def title(self):
+ raise NotImplementedError
+
+ @property
+ def specifications(self):
+ raise NotImplementedError
+
+ def bugtasks(self, user):
+ """The list of non-conjoined bugtasks targeted to this milestone."""
+ # Put the results in a list so that iterating over it multiple
+ # times in this method does not make multiple queries.
+ non_conjoined_slaves = list(
+ getUtility(IBugTaskSet).getPrecachedNonConjoinedBugTasks(
+ user, self))
+ return non_conjoined_slaves
+
+
+class Milestone(SQLBase, MilestoneData, StructuralSubscriptionTargetMixin,
+ HasBugsBase):
implements(IHasBugs, IMilestone, IBugSummaryDimension)
+ active = BoolCol(notNull=True, default=True)
+
+ # XXX: EdwinGrubbs 2009-02-06 bug=326384:
+ # The Milestone.dateexpected should be changed into a date column,
+ # since the class defines the field as a DateCol, so that a list of
+ # milestones can't have some dateexpected attributes that are
+ # datetimes and others that are dates, which can't be compared.
+ dateexpected = DateCol(notNull=False, default=None)
+
# XXX: Guilherme Salgado 2007-03-27 bug=40978:
- # Milestones should be associated with productseries/distroseriess
+ # Milestones should be associated with productseries/distroseries
# so these columns are not needed.
product = ForeignKey(dbName='product',
foreignKey='Product', default=None)
@@ -145,23 +183,23 @@
distroseries = ForeignKey(dbName='distroseries',
foreignKey='DistroSeries', default=None)
name = StringCol(notNull=True)
- # XXX: EdwinGrubbs 2009-02-06 bug=326384:
- # The Milestone.dateexpected should be changed into a date column,
- # since the class defines the field as a DateCol, so that a list of
- # milestones can't have some dateexpected attributes that are
- # datetimes and others that are dates, which can't be compared.
- dateexpected = DateCol(notNull=False, default=None)
- active = BoolCol(notNull=True, default=True)
summary = StringCol(notNull=False, default=None)
code_name = StringCol(dbName='codename', notNull=False, default=None)
- # joins
specifications = SQLMultipleJoin('Specification', joinColumn='milestone',
orderBy=['-priority', 'definition_status',
'implementation_status', 'title'],
prejoins=['assignee'])
@property
+ def target(self):
+ """See IMilestone."""
+ if self.product:
+ return self.product
+ elif self.distribution:
+ return self.distribution
+
+ @property
def product_release(self):
store = Store.of(self)
result = store.find(ProductRelease,
@@ -173,14 +211,6 @@
return releases[0]
@property
- def target(self):
- """See IMilestone."""
- if self.product:
- return self.product
- elif self.distribution:
- return self.distribution
-
- @property
def series_target(self):
"""See IMilestone."""
if self.productseries:
@@ -189,11 +219,6 @@
return self.distroseries
@property
- def displayname(self):
- """See IMilestone."""
- return "%s %s" % (self.target.displayname, self.name)
-
- @property
def title(self):
"""See IMilestone."""
if not self.code_name:
@@ -255,6 +280,39 @@
from lp.bugs.model.bugsummary import BugSummary
return BugSummary.milestone_id == self.id
+ def setTags(self, tags, user):
+ """See IMilestone."""
+ # Circular reference prevention.
+ from lp.registry.model.milestonetag import MilestoneTag
+ store = Store.of(self)
+ if tags:
+ current_tags = set(self.getTags())
+ new_tags = set(tags)
+ if new_tags == current_tags:
+ return
+ # Removing deleted tags.
+ to_remove = current_tags.difference(new_tags)
+ if to_remove:
+ store.find(
+ MilestoneTag, MilestoneTag.tag.is_in(to_remove)).remove()
+ # Adding new tags.
+ for tag in new_tags.difference(current_tags):
+ store.add(MilestoneTag(self, tag, user))
+ else:
+ store.find(
+ MilestoneTag, MilestoneTag.milestone_id == self.id).remove()
+ store.commit()
+
+ def getTags(self):
+ """See IMilestone."""
+ # Circular reference prevention.
+ from lp.registry.model.milestonetag import MilestoneTag
+ store = Store.of(self)
+ return store.find(
+ MilestoneTag, MilestoneTag.milestone_id == self.id
+ ).order_by(MilestoneTag.tag
+ ).values(MilestoneTag.tag)
+
class MilestoneSet:
implements(IMilestoneSet)
@@ -300,7 +358,7 @@
return Milestone.selectBy(active=True, orderBy='id')
-class ProjectMilestone(HasBugsBase):
+class ProjectMilestone(MilestoneData, HasBugsBase):
"""A virtual milestone implementation for project.
The current database schema has no formal concept of milestones related to
@@ -315,12 +373,13 @@
implements(IProjectGroupMilestone)
def __init__(self, target, name, dateexpected, active):
- self.name = name
self.code_name = None
# The id is necessary for generating a unique memcache key
# in a page template loop. The ProjectMilestone.id is passed
# in as the third argument to the "cache" TALes.
self.id = 'ProjectGroup:%s/Milestone:%s' % (target.name, name)
+ self.name = name
+ self.target = target
self.code_name = None
self.product = None
self.distribution = None
@@ -329,7 +388,6 @@
self.product_release = None
self.dateexpected = dateexpected
self.active = active
- self.target = target
self.series_target = None
self.summary = None
=== added file 'lib/lp/registry/model/milestonetag.py'
--- lib/lp/registry/model/milestonetag.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/model/milestonetag.py 2012-01-04 14:49:20 +0000
@@ -0,0 +1,90 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Milestonetag model class."""
+
+__metaclass__ = type
+__all__ = [
+ 'MilestoneTag',
+ 'ProjectGroupMilestoneTag',
+ ]
+
+
+from zope.interface import implements
+from zope.component import getUtility
+
+from lp.services.webapp.interfaces import (
+ IStoreSelector,
+ MAIN_STORE,
+ DEFAULT_FLAVOR,
+ )
+
+from lp.blueprints.model.specification import Specification
+from lp.registry.interfaces.milestonetag import IProjectGroupMilestoneTag
+from lp.registry.model.milestone import MilestoneData, Milestone
+from lp.registry.model.product import Product
+from storm.locals import (
+ DateTime,
+ Int,
+ Unicode,
+ Reference,
+ )
+
+
+class MilestoneTag(object):
+ """A tag belonging to a milestone."""
+
+ __storm_table__ = 'milestonetag'
+
+ id = Int(primary=True)
+ milestone_id = Int(name='milestone', allow_none=False)
+ milestone = Reference(milestone_id, 'milestone.id')
+ tag = Unicode(allow_none=False)
+ created_by_id = Int(name='created_by', allow_none=False)
+ created_by = Reference(created_by_id, 'person.id')
+ date_created = DateTime(allow_none=False)
+
+ def __init__(self, milestone, tag, created_by, date_created=None):
+ self.milestone_id = milestone.id
+ self.tag = tag
+ self.created_by_id = created_by.id
+ if date_created is not None:
+ self.date_created = date_created
+
+
+class ProjectGroupMilestoneTag(MilestoneData):
+
+ implements(IProjectGroupMilestoneTag)
+
+ def __init__(self, target, tags):
+ self.target = target
+ # Tags is a sequence of Unicode strings.
+ self.tags = tags
+
+ @property
+ def name(self):
+ return u", ".join(self.tags)
+
+ @property
+ def title(self):
+ """See IMilestoneData."""
+ return self.displayname
+
+ @property
+ def specifications(self):
+ """See IMilestoneData."""
+ store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+ results = []
+ for tag in self.tags:
+ result = store.find(
+ Specification,
+ Specification.milestone == Milestone.id,
+ Milestone.product == Product.id,
+ Product.project == self.target,
+ MilestoneTag.milestone_id == Milestone.id,
+ MilestoneTag.tag == tag)
+ results.append(result)
+ result = results.pop()
+ for i in results:
+ result = result.intersection(i)
+ return result
=== modified file 'lib/lp/registry/model/projectgroup.py'
--- lib/lp/registry/model/projectgroup.py 2011-12-30 06:14:56 +0000
+++ lib/lp/registry/model/projectgroup.py 2012-01-04 14:49:20 +0000
@@ -464,19 +464,19 @@
@property
def milestones(self):
"""See `IProjectGroup`."""
- return self._getMilestones(True)
+ return self._getMilestones(only_active=True)
@property
def product_milestones(self):
"""Hack to avoid the ProjectMilestone in MilestoneVocabulary."""
# XXX: bug=644977 Robert Collins - this is a workaround for
- # insconsistency in project group milestone use.
+ # inconsistency in project group milestone use.
return self._get_milestones()
@property
def all_milestones(self):
"""See `IProjectGroup`."""
- return self._getMilestones(False)
+ return self._getMilestones(only_active=False)
def getMilestone(self, name):
"""See `IProjectGroup`."""
=== modified file 'lib/lp/registry/tests/test_milestone.py'
--- lib/lp/registry/tests/test_milestone.py 2012-01-01 02:58:52 +0000
+++ lib/lp/registry/tests/test_milestone.py 2012-01-04 14:49:20 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Milestone related test helper."""
@@ -18,6 +18,7 @@
)
from lp.registry.interfaces.product import IProductSet
from lp.testing import (
+ person_logged_in,
ANONYMOUS,
login,
logout,
@@ -128,3 +129,64 @@
def test_projectgroup(self):
projectgroup = self.factory.makeProject()
self.check_skipped(projectgroup)
+
+
+class MilestoneBugTaskTest(TestCaseWithFactory):
+ """Test cases for retrieving bugtasks for a milestone."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(MilestoneBugTaskTest, self).setUp()
+ self.owner = self.factory.makePerson()
+ self.product = self.factory.makeProduct(name="product1")
+ self.milestone = self.factory.makeMilestone(product=self.product)
+
+ def _create_bugtasks(self, num, milestone=None):
+ bugtasks = []
+ with person_logged_in(self.owner):
+ for n in xrange(num):
+ bugtask = self.factory.makeBugTask(
+ target=self.product,
+ owner=self.owner)
+ if milestone:
+ bugtask.milestone = milestone
+ bugtasks.append(bugtask)
+ return bugtasks
+
+ def test_bugtask_retrieval(self):
+ # Ensure that all bugtasks on a milestone can be retrieved.
+ bugtasks = self._create_bugtasks(5, self.milestone)
+ self.assertContentEqual(
+ bugtasks,
+ self.milestone.bugtasks(self.owner))
+
+
+class MilestoneSpecificationTest(TestCaseWithFactory):
+ """Test cases for retrieving specifications for a milestone."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(MilestoneSpecificationTest, self).setUp()
+ self.owner = self.factory.makePerson()
+ self.product = self.factory.makeProduct(name="product1")
+ self.milestone = self.factory.makeMilestone(product=self.product)
+
+ def _create_specifications(self, num, milestone=None):
+ specifications = []
+ with person_logged_in(self.owner):
+ for n in xrange(num):
+ specification = self.factory.makeSpecification(
+ product=self.product,
+ owner=self.owner,
+ milestone=milestone)
+ specifications.append(specification)
+ return specifications
+
+ def test_specification_retrieval(self):
+ # Ensure that all specifications on a milestone can be retrieved.
+ specifications = self._create_specifications(5, self.milestone)
+ self.assertContentEqual(
+ specifications,
+ self.milestone.specifications)
=== added file 'lib/lp/registry/tests/test_milestonetag.py'
--- lib/lp/registry/tests/test_milestonetag.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/tests/test_milestonetag.py 2012-01-04 14:49:20 +0000
@@ -0,0 +1,164 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Milestone related test helper."""
+
+__metaclass__ = type
+
+from lp.testing.layers import (
+ DatabaseFunctionalLayer,
+ )
+from lp.registry.model.milestonetag import ProjectGroupMilestoneTag
+from lp.testing import (
+ person_logged_in,
+ TestCaseWithFactory,
+ )
+
+
+class MilestoneTagTest(TestCaseWithFactory):
+ """Test cases for setting and retrieving milestone tags."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(MilestoneTagTest, self).setUp()
+ self.milestone = self.factory.makeMilestone()
+ self.person = self.milestone.target.owner
+ self.tags = [u'tag2', u'tag1', u'tag3']
+
+ def test_no_tags(self):
+ # Ensure a newly created milestone does not have associated tags.
+ self.assertEquals([], list(self.milestone.getTags()))
+
+ def test_tags_setting_and_retrieval(self):
+ # Ensure tags are correctly saved and retrieved from the db.
+ with person_logged_in(self.person):
+ self.milestone.setTags(self.tags, self.person)
+ self.assertEqual(sorted(self.tags), list(self.milestone.getTags()))
+
+ def test_tags_override(self):
+ # Ensure you can override tags already associated with the milestone.
+ with person_logged_in(self.person):
+ self.milestone.setTags(self.tags, self.person)
+ new_tags = [u'tag2', u'tag4', u'tag3']
+ self.milestone.setTags(new_tags, self.person)
+ self.assertEqual(sorted(new_tags), list(self.milestone.getTags()))
+
+ def test_tags_deletion(self):
+ # Ensure passing an empty sequence of tags deletes them all.
+ with person_logged_in(self.person):
+ self.milestone.setTags(self.tags, self.person)
+ self.milestone.setTags([], self.person)
+ self.assertEquals([], list(self.milestone.getTags()))
+
+
+class ProjectGroupMilestoneTagTest(TestCaseWithFactory):
+ """Test cases for retrieving bugtasks for a milestonetag."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(ProjectGroupMilestoneTagTest, self).setUp()
+ self.owner = self.factory.makePerson()
+ self.project_group = self.factory.makeProject(owner=self.owner)
+ self.product = self.factory.makeProduct(
+ name="product1",
+ owner=self.owner,
+ project=self.project_group)
+ self.milestone = self.factory.makeMilestone(product=self.product)
+
+ def _create_bugtasks(self, num, milestone=None):
+ bugtasks = []
+ with person_logged_in(self.owner):
+ for n in xrange(num):
+ bugtask = self.factory.makeBugTask(
+ target=self.product,
+ owner=self.owner)
+ if milestone:
+ bugtask.milestone = milestone
+ bugtasks.append(bugtask)
+ return bugtasks
+
+ def _create_specifications(self, num, milestone=None):
+ specifications = []
+ with person_logged_in(self.owner):
+ for n in xrange(num):
+ specification = self.factory.makeSpecification(
+ product=self.product,
+ owner=self.owner,
+ milestone=milestone)
+ specifications.append(specification)
+ return specifications
+
+ def _create_items_for_retrieval(self, factory, tag=u'tag1'):
+ with person_logged_in(self.owner):
+ self.milestone.setTags([tag], self.owner)
+ items = factory(5, self.milestone)
+ milestonetag = ProjectGroupMilestoneTag(
+ target=self.project_group, tags=[tag])
+ return items, milestonetag
+
+ def _create_items_for_untagged_milestone(self, factory, tag=u'tag1'):
+ new_milestone = self.factory.makeMilestone(product=self.product)
+ with person_logged_in(self.owner):
+ self.milestone.setTags([tag], self.owner)
+ items = factory(5, self.milestone)
+ factory(3, new_milestone)
+ milestonetag = ProjectGroupMilestoneTag(
+ target=self.project_group, tags=[tag])
+ return items, milestonetag
+
+ def _create_items_for_multiple_tags(
+ self, factory, tags=(u'tag1', u'tag2')):
+ new_milestone = self.factory.makeMilestone(product=self.product)
+ with person_logged_in(self.owner):
+ self.milestone.setTags(tags, self.owner)
+ new_milestone.setTags(tags[:1], self.owner)
+ items = factory(5, self.milestone)
+ factory(3, new_milestone)
+ milestonetag = ProjectGroupMilestoneTag(
+ target=self.project_group, tags=tags)
+ return items, milestonetag
+
+ # Add a test similar to TestProjectExcludeConjoinedMasterSearch in
+ # lp.bugs.tests.test_bugsearch_conjoined.
+
+ def test_bugtask_retrieve_single_milestone(self):
+ # Ensure that all bugtasks on a single milestone can be retrieved.
+ bugtasks, milestonetag = self._create_items_for_retrieval(
+ self._create_bugtasks)
+ self.assertContentEqual(bugtasks, milestonetag.bugtasks(self.owner))
+
+ def test_bugtasks_for_untagged_milestone(self):
+ # Ensure that bugtasks for a project group are retrieved
+ # only if associated with milestones having specified tags.
+ bugtasks, milestonetag = self._create_items_for_untagged_milestone(
+ self._create_bugtasks)
+ self.assertContentEqual(bugtasks, milestonetag.bugtasks(self.owner))
+
+ def test_bugtasks_multiple_tags(self):
+ # Ensure that, in presence of multiple tags, only bugtasks
+ # for milestones associated with all the tags are retrieved.
+ bugtasks, milestonetag = self._create_items_for_multiple_tags(
+ self._create_bugtasks)
+ self.assertContentEqual(bugtasks, milestonetag.bugtasks(self.owner))
+
+ def test_specification_retrieval(self):
+ # Ensure that all specifications on a milestone can be retrieved.
+ specs, milestonetag = self._create_items_for_retrieval(
+ self._create_specifications)
+ self.assertContentEqual(specs, milestonetag.specifications)
+
+ def test_specifications_for_untagged_milestone(self):
+ # Ensure that specifications for a project group are retrieved
+ # only if associated with milestones having specified tags.
+ specs, milestonetag = self._create_items_for_untagged_milestone(
+ self._create_specifications)
+ self.assertContentEqual(specs, milestonetag.specifications)
+
+ def test_specifications_multiple_tags(self):
+ # Ensure that, in presence of multiple tags, only specifications
+ # for milestones associated with all the tags are retrieved.
+ specs, milestonetag = self._create_items_for_multiple_tags(
+ self._create_specifications)
+ self.assertContentEqual(specs, milestonetag.specifications)
=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py 2011-12-30 06:14:56 +0000
+++ lib/lp/registry/tests/test_person.py 2012-01-04 14:49:20 +0000
@@ -55,7 +55,7 @@
IMasterStore,
IStore,
)
-from lp.services.database.sqlbase import cursor
+from lp.services.database.sqlbase import cursor, sqlvalues
from lp.services.identity.interfaces.account import (
AccountCreationRationale,
AccountStatus,
@@ -847,8 +847,17 @@
with celebrity_logged_in('admin'):
email = from_person.preferredemail
email.status = EmailAddressStatus.NEW
- email.person = to_person
- email.account = to_person.account
+ store = IMasterStore(EmailAddress)
+ # EmailAddress.acount and .person need to be updated at the
+ # same time to prevent the constraints on the account field
+ # from kicking the change out.
+ store.execute("""
+ UPDATE EmailAddress SET
+ person = %s,
+ account = %s
+ WHERE id = %s
+ """ % sqlvalues(
+ to_person.id, to_person.accountID, email.id))
transaction.commit()
def _do_merge(self, from_person, to_person, reviewer=None):
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2011-12-30 06:14:56 +0000
+++ lib/lp/security.py 2012-01-04 14:49:20 +0000
@@ -107,6 +107,7 @@
from lp.registry.interfaces.irc import IIrcID
from lp.registry.interfaces.location import IPersonLocation
from lp.registry.interfaces.milestone import (
+ IAbstractMilestone,
IMilestone,
IProjectGroupMilestone,
)
@@ -430,8 +431,8 @@
class ViewMilestone(AnonymousAuthorization):
- """Anyone can view an IMilestone."""
- usedfor = IMilestone
+ """Anyone can view an IMilestone or an IProjectGroupMilestone."""
+ usedfor = IAbstractMilestone
class EditSpecificationBranch(AuthorizationBase):
=== modified file 'lib/lp/services/mail/tests/emails/x-unknown-encoding.txt'
--- lib/lp/services/mail/tests/emails/x-unknown-encoding.txt 2009-07-03 09:20:34 +0000
+++ lib/lp/services/mail/tests/emails/x-unknown-encoding.txt 2012-01-04 14:49:20 +0000
@@ -1,7 +1,7 @@
MIME-Version: 1.0
Date: Wed, 13 Apr 2005 19:20:48 -0400
Message-ID: <bbe079320907030136g23826072s7a653e75f8ecdd58@xxxxxxxxxxxxxxxx>
-Subject: Uknown Encoding
+Subject: Unknown Encoding
From: test@xxxxxxxxxxxxx
To: someone@xxxxxxxxxxx
X-Original-To: someone@xxxxxxxxxxx
@@ -9,4 +9,3 @@
Content-Transfer-Encoding: QUOTED-PRINTABLE
Mysterious, intriguing, the encoding is unknown.
-