launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #06041
[Merge] lp:~bac/launchpad/bug-904335-export-tags into lp:launchpad
Brad Crittenden has proposed merging lp:~bac/launchpad/bug-904335-export-tags into lp:launchpad with lp:~frankban/launchpad/view-904335 as a prerequisite.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #904335 in Launchpad itself: "Project groups need a way to aggregate project milestones"
https://bugs.launchpad.net/launchpad/+bug/904335
For more details, see:
https://code.launchpad.net/~bac/launchpad/bug-904335-export-tags/+merge/87635
Export the getTags and setTags methods to the API.
--
https://code.launchpad.net/~bac/launchpad/bug-904335-export-tags/+merge/87635
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~bac/launchpad/bug-904335-export-tags into lp:launchpad.
=== 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-05 15:15:17 +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-05 15:15:17 +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-05 15:15:17 +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-05 15:15:17 +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-05 15:15:17 +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-04 11:49:08 +0000
+++ database/schema/security.cfg 2012-01-05 15:15:17 +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
@@ -444,6 +447,7 @@
public.pofile = SELECT, UPDATE
public.potemplate = SELECT, UPDATE
public.job = SELECT, UPDATE, DELETE
+public.packaging = SELECT
public.pofilestatsjob = SELECT, UPDATE, DELETE
public.potmsgset = SELECT
public.product = SELECT
@@ -591,6 +595,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 +654,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 +868,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 +1308,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 +1412,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 +1511,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 +1716,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 +1957,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 +2056,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
@@ -2147,6 +2160,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-05 15:15:17 +0000
@@ -285,6 +285,7 @@
from lp.services.webapp.menu import structured
+
vocabulary_registry = getVocabularyRegistry()
DISPLAY_BUG_STATUS_FOR_PATCHES = {
@@ -3504,7 +3505,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-03 15:56:55 +0000
+++ lib/lp/bugs/interfaces/bugtask.py 2012-01-05 15:15:17 +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 2012-01-02 17:10:14 +0000
+++ lib/lp/bugs/model/bugtask.py 2012-01-05 15:15:17 +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.
@@ -2872,12 +2899,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 2012-01-04 23:49:46 +0000
+++ lib/lp/hardwaredb/doc/hwdb.txt 2012-01-05 15:15:17 +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-05 15:15:17 +0000
@@ -1279,17 +1279,22 @@
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"/>
+ <browser:url
+ for="lp.registry.interfaces.milestonetag.IProjectGroupMilestoneTag"
+ path_expression="string:+tags/${name}"
+ rootsite="mainsite"
+ attribute_to_parent="target"/>
<browser:page
for="*"
name="+milestone-macros"
@@ -1297,7 +1302,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
@@ -1322,6 +1327,20 @@
class="lp.registry.browser.milestone.MilestoneWithoutCountsView"
permission="zope.Public"
template="../templates/productseries-milestone-table-row.pt"/>
+ <browser:pages
+ for="lp.registry.interfaces.milestonetag.IProjectGroupMilestoneTag"
+ class="lp.registry.browser.milestone.MilestoneTagView"
+ permission="zope.Public">
+ <browser:page
+ name="+index"
+ template="../templates/milestone-index.pt"/>
+
+ <!-- Project Group Milestone Tag Portlets -->
+
+ <browser:page
+ name="+portlet-milestone-tag-search"
+ template="../templates/milestone-tag-search.pt"/>
+ </browser:pages>
<browser:page
name="+edit"
for="lp.registry.interfaces.milestone.IMilestone"
=== 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-05 15:15:17 +0000
@@ -6,6 +6,7 @@
__metaclass__ = type
__all__ = [
+ 'ISearchMilestoneTagsForm',
'MilestoneAddView',
'MilestoneBreadcrumb',
'MilestoneContextMenu',
@@ -15,9 +16,12 @@
'MilestoneNavigation',
'MilestoneOverviewNavigationMenu',
'MilestoneSetNavigation',
+ 'MilestoneTagView',
'MilestoneWithoutCountsView',
'MilestoneView',
+ 'MilestoneViewMixin',
'ObjectMilestonesView',
+ 'validate_tags',
]
@@ -27,7 +31,7 @@
implements,
Interface,
)
-from zope.schema import Choice
+from zope.schema import Choice, TextLine
from lp import _
from lp.app.browser.launchpadform import (
@@ -35,7 +39,9 @@
custom_widget,
LaunchpadEditFormView,
LaunchpadFormView,
+ safe_action,
)
+from lp.app.validators.name import valid_name
from lp.app.widgets.date import DateWidget
from lp.bugs.browser.bugtask import BugTaskListingItem
from lp.bugs.browser.structuralsubscription import (
@@ -52,12 +58,16 @@
from lp.registry.browser.product import ProductDownloadFileMixin
from lp.registry.interfaces.distroseries import IDistroSeries
from lp.registry.interfaces.milestone import (
+ IAbstractMilestone,
IMilestone,
+ IMilestoneData,
IMilestoneSet,
IProjectGroupMilestone,
)
+from lp.registry.interfaces.milestonetag import IProjectGroupMilestoneTag
from lp.registry.interfaces.person import IPersonSet
from lp.registry.interfaces.product import IProduct
+from lp.registry.model.milestonetag import ProjectGroupMilestoneTag
from lp.services.propertycache import cachedproperty
from lp.services.webapp import (
canonical_url,
@@ -84,16 +94,16 @@
class MilestoneNavigation(Navigation,
StructuralSubscriptionTargetTraversalMixin):
"""The navigation to traverse to a milestone."""
- usedfor = IMilestone
+ usedfor = IMilestoneData
class MilestoneBreadcrumb(Breadcrumb):
- """The Breadcrumb for an `IMilestone`."""
+ """The Breadcrumb for an `IMilestoneData`."""
@property
def text(self):
- milestone = IMilestone(self.context)
- if milestone.code_name:
+ milestone = IMilestoneData(self.context)
+ if hasattr(milestone, 'code_name') and milestone.code_name:
return '%s "%s"' % (milestone.name, milestone.code_name)
else:
return milestone.name
@@ -139,7 +149,7 @@
class MilestoneContextMenu(ContextMenu, MilestoneLinkMixin):
"""The menu for this milestone."""
- usedfor = IMilestone
+ usedfor = IMilestoneData
@cachedproperty
def links(self):
@@ -150,8 +160,8 @@
class MilestoneOverviewNavigationMenu(NavigationMenu, MilestoneLinkMixin):
- """Overview navigation menu for `IMilestone` objects."""
- usedfor = IMilestone
+ """Overview navigation menu for `IAbstractMilestone` objects."""
+ usedfor = IAbstractMilestone
facet = 'overview'
@cachedproperty
@@ -165,7 +175,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 = IMilestoneData
facet = 'overview'
links = ('edit', 'create_release')
@@ -181,38 +191,8 @@
links = ('edit', )
-class MilestoneView(LaunchpadView, ProductDownloadFileMixin):
- """A View for listing milestones and releases."""
- # XXX sinzui 2009-05-29 bug=381672: Extract the BugTaskListingItem rules
- # to a mixin so that MilestoneView and others can use it.
- implements(IMilestoneInline)
- show_series_context = False
-
- def __init__(self, context, request):
- """See `LaunchpadView`.
-
- This view may be used with a milestone or a release. The milestone
- and release (if it exists) are accessible as attributes. The context
- attribute will always be the milestone.
-
- :param context: `IMilestone` or `IProductRelease`.
- :param request: `ILaunchpadRequest`.
- """
- super(MilestoneView, self).__init__(context, request)
- if IMilestone.providedBy(context):
- self.milestone = context
- self.release = context.product_release
- else:
- self.milestone = context.milestone
- self.release = context
- self.context = self.milestone
-
- def initialize(self):
- """See `LaunchpadView`."""
- self.form = self.request.form
- self.processDeleteFiles()
- expose_structural_subscription_data_to_js(
- self.context, self.request, self.user)
+class MilestoneViewMixin(object):
+ """Common methods shared between MilestoneView and MilestoneTagView."""
@property
def expire_cache_minutes(self):
@@ -232,38 +212,19 @@
"""Return the HTML page title."""
return self.context.title
- def getReleases(self):
- """See `ProductDownloadFileMixin`."""
- return set([self.release])
-
- @cachedproperty
- def download_files(self):
- """The release's files as DownloadFiles."""
- if self.release is None or self.release.files.count() == 0:
- return None
- return list(self.release.files)
-
- # Listify and cache the specifications, ProductReleaseFiles and bugtasks
- # to avoid making the same query over and over again when evaluating in
- # the template.
+ # Listify and cache the specifications and bugtasks to avoid making
+ # the same query over and over again when evaluating in the template.
@cachedproperty
def specifications(self):
"""The list of specifications targeted to this milestone."""
return list(self.context.specifications)
@cachedproperty
- def product_release_files(self):
- """Files associated with this milestone."""
- return list(self.release.files)
-
- @cachedproperty
def _bugtasks(self):
"""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
@@ -359,6 +320,83 @@
return all_assignments
return all_assignments
+ @property
+ def is_project_milestone_tag(self):
+ """Check, if the current milestone is a project milestone tag.
+
+ Return true, if the current milestone is a project milestone tag,
+ else return False."""
+ return IProjectGroupMilestoneTag.providedBy(self.context)
+
+ @property
+ def is_project_milestone(self):
+ """Check, if the current milestone is a project milestone.
+
+ Return true, if the current milestone is a project milestone or
+ a project milestone tag, else return False."""
+ return (
+ IProjectGroupMilestone.providedBy(self.context) or
+ self.is_project_milestone_tag
+ )
+
+ @property
+ def has_bugs_or_specs(self):
+ """Does the milestone have any bugtasks and specifications?"""
+ return len(self.bugtasks) > 0 or len(self.specifications) > 0
+
+
+class MilestoneView(
+ LaunchpadView, MilestoneViewMixin, ProductDownloadFileMixin):
+ """A View for listing milestones and releases."""
+ # XXX sinzui 2009-05-29 bug=381672: Extract the BugTaskListingItem rules
+ # to a mixin so that MilestoneView and others can use it.
+ implements(IMilestoneInline)
+ show_series_context = False
+
+ def __init__(self, context, request):
+ """See `LaunchpadView`.
+
+ This view may be used with a milestone or a release. The milestone
+ and release (if it exists) are accessible as attributes. The context
+ attribute will always be the milestone.
+
+ :param context: `IMilestone` or `IProductRelease`.
+ :param request: `ILaunchpadRequest`.
+ """
+ super(MilestoneView, self).__init__(context, request)
+ if IMilestoneData.providedBy(context):
+ self.milestone = context
+ self.release = context.product_release
+ else:
+ self.milestone = context.milestone
+ self.release = context
+ self.context = self.milestone
+
+ def initialize(self):
+ """See `LaunchpadView`."""
+ self.form = self.request.form
+ self.processDeleteFiles()
+ expose_structural_subscription_data_to_js(
+ self.context, self.request, self.user)
+
+ def getReleases(self):
+ """See `ProductDownloadFileMixin`."""
+ return set([self.release])
+
+ @cachedproperty
+ def download_files(self):
+ """The release's files as DownloadFiles."""
+ if self.release is None or self.release.files.count() == 0:
+ return None
+ return list(self.release.files)
+
+ # Listify and cache ProductReleaseFiles to avoid making the same query
+ # over and over again when evaluating in the template.
+ @cachedproperty
+ def product_release_files(self):
+ """Files associated with this milestone."""
+ return list(self.release.files)
+
@cachedproperty
def total_downloads(self):
"""Total downloads of files associated with this milestone."""
@@ -373,19 +411,6 @@
"""
return IDistroSeries.providedBy(self.context.series_target)
- @property
- def is_project_milestone(self):
- """Check, if the current milestone is a project milestone.
-
- Return true, if the current milestone is a project milestone,
- else return False."""
- return IProjectGroupMilestone.providedBy(self.context)
-
- @property
- def has_bugs_or_specs(self):
- """Does the milestone have any bugtasks and specifications?"""
- return len(self.bugtasks) > 0 or len(self.specifications) > 0
-
class MilestoneWithoutCountsView(MilestoneView):
"""Show a milestone in a list of milestones."""
@@ -533,6 +558,57 @@
self.next_url = canonical_url(series)
+def validate_tags(tags):
+ """Check that `separator` separated `tags` are valid tag names."""
+ return (
+ all(valid_name(tag) for tag in tags) and
+ len(set(tags)) == len(tags)
+ )
+
+
+class ISearchMilestoneTagsForm(Interface):
+ """Schema for the search milestone tags form."""
+
+ tags = TextLine(
+ title=_('Search by tags'),
+ description=_('Insert space separated tag names'),
+ required=True, min_length=2, max_length=64,
+ constraint=lambda value: validate_tags(value.split()))
+
+
+class MilestoneTagView(
+ LaunchpadFormView, MilestoneViewMixin, ProductDownloadFileMixin):
+ """A View for listing bugtasks and specification for milestone tags."""
+ schema = ISearchMilestoneTagsForm
+
+ def __init__(self, context, request):
+ """See `LaunchpadView`.
+
+ :param context: `IProjectGroupMilestoneTag`
+ :param request: `ILaunchpadRequest`.
+ """
+ super(MilestoneTagView, self).__init__(context, request)
+ self.context = self.milestone = context
+ self.release = None
+
+ @property
+ def tags(self):
+ """Return a list of tag names associated with current milestonetag."""
+ return self.context.name.split(u',')
+
+ @property
+ def initial_values(self):
+ """Set the initial value of the search tags field."""
+ return {'tags': u' '.join(self.tags)}
+
+ @safe_action
+ @action(u'Search Milestone Tags', name='search')
+ def search_by_tags(self, action, data):
+ tags = data['tags'].split()
+ milestone_tag = ProjectGroupMilestoneTag(self.context.target, tags)
+ self.next_url = canonical_url(milestone_tag, request=self.request)
+
+
class ObjectMilestonesView(LaunchpadView):
"""A view for listing the milestones for any `IHasMilestones` object"""
=== modified file 'lib/lp/registry/browser/project.py'
--- lib/lp/registry/browser/project.py 2012-01-01 02:58:52 +0000
+++ lib/lp/registry/browser/project.py 2012-01-05 15:15:17 +0000
@@ -30,6 +30,7 @@
'ProjectView',
]
+
from z3c.ptcompat import ViewPageTemplateFile
from zope.app.form.browser import TextWidget
from zope.component import getUtility
@@ -75,6 +76,7 @@
IRegistryCollectionNavigationMenu,
RegistryCollectionActionMenuBase,
)
+from lp.registry.browser.milestone import validate_tags
from lp.registry.browser.objectreassignment import ObjectReassignmentView
from lp.registry.browser.product import (
ProductAddView,
@@ -87,6 +89,7 @@
IProjectGroupSeries,
IProjectGroupSet,
)
+from lp.registry.model.milestonetag import ProjectGroupMilestoneTag
from lp.services.feeds.browser import FeedsMixin
from lp.services.fields import (
PillarAliases,
@@ -130,6 +133,12 @@
def traverse_series(self, series_name):
return self.context.getSeries(series_name)
+ @stepthrough('+tags')
+ def traverse_tags(self, name):
+ tags = name.split(u',')
+ if validate_tags(tags):
+ return ProjectGroupMilestoneTag(self.context, tags)
+
class ProjectSetNavigation(Navigation):
@@ -388,6 +397,11 @@
"""
return self.context.products.count() > 10
+ @property
+ def project_group_milestone_tag(self):
+ """Return a ProjectGroupMilestoneTag based on this project."""
+ return ProjectGroupMilestoneTag(self.context, [])
+
class ProjectEditView(LaunchpadEditFormView):
"""View class that lets you edit a Project object."""
=== modified file 'lib/lp/registry/browser/tests/test_milestone.py'
--- lib/lp/registry/browser/tests/test_milestone.py 2012-01-01 02:58:52 +0000
+++ lib/lp/registry/browser/tests/test_milestone.py 2012-01-05 15:15:17 +0000
@@ -13,6 +13,7 @@
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.bugs.interfaces.bugtask import IBugTaskSet
from lp.registry.interfaces.person import TeamSubscriptionPolicy
+from lp.registry.model.milestonetag import ProjectGroupMilestoneTag
from lp.services.config import config
from lp.services.webapp import canonical_url
from lp.testing import (
@@ -430,3 +431,73 @@
self.assertThat(self.milestone, browses_under_limit)
self.add_bug(10)
self.assertThat(self.milestone, browses_under_limit)
+
+
+class TestMilestoneTagView(TestQueryCountBase):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestMilestoneTagView, self).setUp()
+ self.tags = [u'tag1']
+ 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)
+ with person_logged_in(self.owner):
+ self.milestone.setTags(self.tags, self.owner)
+ self.milestonetag = ProjectGroupMilestoneTag(
+ target=self.project_group, tags=self.tags)
+
+ def add_bug(self, count):
+ with person_logged_in(self.owner):
+ for n in range(count):
+ self.factory.makeBug(
+ product=self.product, owner=self.owner,
+ milestone=self.milestone)
+
+ def _make_form(self, tags):
+ return {
+ u'field.actions.search': u'Search',
+ u'field.tags': u' '.join(tags)
+ }
+
+ def _url_tail(self, url, separator='/'):
+ return url.rsplit(separator, 1)[1],
+
+ def test_view_properties(self):
+ # Ensure that the view is correctly initialized.
+ view = create_initialized_view(self.milestonetag, '+index')
+ self.assertEqual(self.milestonetag, view.context)
+ self.assertEqual(self.milestonetag.title, view.page_title)
+ self.assertContentEqual(self.tags, view.tags)
+
+ def test_view_form_redirect(self):
+ # Ensure a correct redirection is performed when tags are searched.
+ tags = [u'tag1', u'tag2']
+ form = self._make_form(tags)
+ view = create_initialized_view(self.milestonetag, '+index', form=form)
+ self.assertEqual(302, view.request.response.getStatus())
+ new_milestonetag = ProjectGroupMilestoneTag(
+ target=self.project_group, tags=tags)
+ self.assertEqual(
+ self._url_tail(canonical_url(new_milestonetag)),
+ self._url_tail(view.request.response.getHeader('Location')))
+
+ def test_view_form_error(self):
+ # Ensure the form correctly handles invalid submissions.
+ tags = [u'tag1', u't'] # One char tag is not valid.
+ form = self._make_form(tags)
+ view = create_initialized_view(self.milestonetag, '+index', form=form)
+ self.assertEqual(1, len(view.errors))
+ self.assertEqual('tags', view.errors[0].field_name)
+
+ def test_buktask_query_count(self):
+ # Ensure that a correct number of queries is executed for
+ # bugtasks retrieval.
+ bugtask_count = 10
+ self.assert_bugtasks_query_count(
+ self.milestonetag, bugtask_count, query_limit=11)
=== 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-05 15:15:17 +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"/>
@@ -1052,6 +1060,11 @@
<allow
interface="lp.registry.interfaces.milestone.IProjectGroupMilestone"/>
</class>
+ <class
+ class="lp.registry.model.milestonetag.ProjectGroupMilestoneTag">
+ <allow
+ interface="lp.registry.interfaces.milestonetag.IProjectGroupMilestoneTag"/>
+ </class>
<subscriber
for="lp.registry.interfaces.product.IProduct zope.lifecycleevent.interfaces.IObjectModifiedEvent"
handler="lp.registry.subscribers.product_modified"/>
@@ -1245,7 +1258,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"/>
=== added file 'lib/lp/registry/help/milestone-tags.html'
--- lib/lp/registry/help/milestone-tags.html 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/help/milestone-tags.html 2012-01-05 15:15:17 +0000
@@ -0,0 +1,33 @@
+<html>
+ <head>
+ <title>What is a milestone tag?</title>
+ <link rel="stylesheet" type="text/css"
+ href="/+icing/yui/cssreset/reset.css" />
+ <link rel="stylesheet" type="text/css"
+ href="/+icing/yui/cssfonts/fonts.css" />
+ <link rel="stylesheet" type="text/css"
+ href="/+icing/yui/cssbase/base.css" />
+ </head>
+ <body>
+ <h1>What is a milestone tag?</h1>
+
+ <p>
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+ tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+ quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+ consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
+ cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
+ proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ </p>
+
+ <p>
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+ tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+ quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+ consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
+ cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
+ proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ </p>
+
+ </body>
+</html>
=== 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-05 15:15:17 +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',
]
@@ -23,6 +25,7 @@
export_factory_operation,
export_operation_as,
export_read_operation,
+ export_write_operation,
exported,
operation_for_version,
operation_parameters,
@@ -42,6 +45,7 @@
Bool,
Choice,
Int,
+ List,
TextLine,
)
@@ -97,20 +101,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 +162,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 +224,35 @@
release.
"""
+
+class IMilestone(IAbstractMilestone):
+ """Actual interface for milestones."""
+
+ export_as_webservice_entry()
+
+ @operation_parameters(
+ tags=List(
+ title=_("Tags for this milestone"),
+ description=_("Space-separated keywords for classifying "
+ "this milestone."),
+ value_type=TextLine()))
+
+ @call_with(user=REQUEST_USER)
+ @export_write_operation()
+ @operation_for_version('devel')
+ 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.
+ """
+
+ @export_read_operation()
+ @operation_for_version('devel')
+ def getTags():
+ """Return the milestone tags in alphabetical order."""
+
+
# Avoid circular imports
IBugTask['milestone'].schema = IMilestone
patch_plain_parameter_type(
@@ -249,8 +291,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-05 15:15:17 +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-05 15:15:17 +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 list(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-05 15:15:17 +0000
@@ -0,0 +1,97 @@
+# 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
+ self.active = True
+ self.dateexpected = None
+
+ @property
+ def name(self):
+ return u','.join(self.tags)
+
+ @property
+ def displayname(self):
+ """See IMilestone."""
+ return "%s %s" % (self.target.displayname, 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-05 15:15:17 +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/stories/project/xx-project-index.txt'
--- lib/lp/registry/stories/project/xx-project-index.txt 2011-12-08 19:18:27 +0000
+++ lib/lp/registry/stories/project/xx-project-index.txt 2012-01-05 15:15:17 +0000
@@ -34,7 +34,6 @@
Gnome Applets
NetApplet
gnomebaker
- ...
The projects are linked.
=== modified file 'lib/lp/registry/templates/milestone-index.pt'
--- lib/lp/registry/templates/milestone-index.pt 2011-10-12 00:21:07 +0000
+++ lib/lp/registry/templates/milestone-index.pt 2012-01-05 15:15:17 +0000
@@ -39,11 +39,15 @@
milestone_menu view/milestone/menu:overview;
has_edit context/required:launchpad.Edit">
+ <tal:search_by_tags_form
+ content="structure context/@@+portlet-milestone-tag-search"
+ condition="view/is_project_milestone_tag"/>
+
<div class="top-portlet">
- <div id="description" tal:condition="view/milestone/summary">
+ <div id="description" tal:condition="view/milestone/summary|nothing">
<p class="documentDescription"
tal:content="structure view/milestone/summary/fmt:text-to-html">
- Milestone.summary.
+ Milestone summary.
</p>
</div>
</div>
@@ -67,11 +71,12 @@
</dl>
<dl id="version">
- <dt>Version:</dt>
+ <dt tal:condition="view/is_project_milestone_tag">Tags:</dt>
+ <dt tal:condition="not: view/is_project_milestone_tag">Version:</dt>
<dd><tal:version replace="context/name" /></dd>
</dl>
- <dl id="code-name">
+ <dl id="code-name" tal:condition="view/milestone/code_name|nothing">
<dt>Code name:</dt>
<dd>
<tal:code-name replace="view/milestone/code_name" />
@@ -80,40 +85,42 @@
</dd>
</dl>
- <dl tal:condition="not: view/release">
- <dt>Expected:</dt>
- <dd><span
- tal:attributes="title context/dateexpected/fmt:datetime"
- tal:content="context/dateexpected/fmt:approximatedate" />
-
- <a tal:replace="structure milestone_menu/create_release/fmt:icon-link" />
- </dd>
- </dl>
-
- <tal:has_release condition="view/release">
- <dl>
- <dt>Released:</dt>
+ <tal:is_not_project_milestone_tag condition="not: view/is_project_milestone_tag">
+ <dl tal:condition="not: view/release">
+ <dt>Expected:</dt>
<dd><span
- tal:attributes="title view/release/datereleased/fmt:datetime"
- tal:content="view/release/datereleased/fmt:approximatedate" />
+ tal:attributes="title context/dateexpected/fmt:datetime"
+ tal:content="context/dateexpected/fmt:approximatedate" />
- <a tal:replace="structure release_menu/delete/fmt:icon" />
- <a tal:replace="structure release_menu/edit/fmt:icon" />
- </dd>
- </dl>
-
- <dl>
- <dt>Registrant:</dt>
- <dd><tal:registrant replace="structure view/release/owner/fmt:link"/></dd>
- </dl>
-
- <dl>
- <dt>Release registered:</dt>
- <dd><span
- tal:attributes="title view/release/datecreated/fmt:datetime"
- tal:content="view/release/datecreated/fmt:approximatedate" /></dd>
- </dl>
- </tal:has_release>
+ <a tal:replace="structure milestone_menu/create_release/fmt:icon-link" />
+ </dd>
+ </dl>
+
+ <tal:has_release condition="view/release">
+ <dl>
+ <dt>Released:</dt>
+ <dd><span
+ tal:attributes="title view/release/datereleased/fmt:datetime"
+ tal:content="view/release/datereleased/fmt:approximatedate" />
+
+ <a tal:replace="structure release_menu/delete/fmt:icon" />
+ <a tal:replace="structure release_menu/edit/fmt:icon" />
+ </dd>
+ </dl>
+
+ <dl>
+ <dt>Registrant:</dt>
+ <dd><tal:registrant replace="structure view/release/owner/fmt:link"/></dd>
+ </dl>
+
+ <dl>
+ <dt>Release registered:</dt>
+ <dd><span
+ tal:attributes="title view/release/datecreated/fmt:datetime"
+ tal:content="view/release/datecreated/fmt:approximatedate" /></dd>
+ </dl>
+ </tal:has_release>
+ </tal:is_not_project_milestone_tag>
</div>
<dl tal:condition="not: view/is_project_milestone">
@@ -310,7 +317,7 @@
milestones for
<tal:project replace="view/milestone/target/project/displayname" /></a>
</li>
- <li tal:condition="view/milestone/series_target">
+ <li tal:condition="view/milestone/series_target|nothing">
<a class="sprite info"
tal:attributes="href view/milestone/series_target/fmt:url">View
releases for the
=== added file 'lib/lp/registry/templates/milestone-tag-search.pt'
--- lib/lp/registry/templates/milestone-tag-search.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/templates/milestone-tag-search.pt 2012-01-05 15:15:17 +0000
@@ -0,0 +1,40 @@
+<tal:root
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+ omit-tag="">
+
+ <form
+ action="."
+ tal:attributes="action view/action_url"
+ name="launchpadform"
+ method="get"
+ accept-charset="UTF-8">
+
+ <p class="error message"
+ tal:condition="view/errors"
+ tal:content="view/error_count" />
+
+ <p class="error message"
+ tal:repeat="form_wide_error view/form_wide_errors"
+ tal:content="structure form_wide_error">
+ Schema validation errors.
+ </p>
+
+ <p>
+ <tal:tags replace="structure view/widgets/tags" />
+ <input tal:replace="structure view/search_by_tags/render" />
+ <a target="help" href="/+help-registry/milestone-tags.html" class="sprite maybe">
+ <span class="invisible-link">Milestone Tags help</span>
+ </a>
+ </p>
+
+ <div class="error" tal:condition="view/errors">
+ <div class="message">Invalid tag name.</div>
+ </div>
+
+ <p class="formHelp" tal:content="view/widgets/tags/hint">Some Help Text</p>
+
+ </form>
+
+</tal:root>
=== modified file 'lib/lp/registry/templates/project-index.pt'
--- lib/lp/registry/templates/project-index.pt 2011-06-16 13:50:58 +0000
+++ lib/lp/registry/templates/project-index.pt 2012-01-05 15:15:17 +0000
@@ -109,6 +109,15 @@
<tal:has-many-project condition="view/has_many_projects">
<tal:sprints content="structure context/@@+portlet-coming-sprints" />
</tal:has-many-project>
+
+ <h2>Milestones</h2>
+ <ul>
+ <li>
+ <a tal:replace="structure context/menu:overview/milestones/fmt:link" />
+ </li>
+ </ul>
+ <tal:search_by_tags_form
+ content="structure view/project_group_milestone_tag/@@+portlet-milestone-tag-search" />
</div>
<div class="yui-u" id="products">
@@ -128,9 +137,6 @@
<li tal:condition="context/menu:overview/new_product/enabled">
<a tal:replace="structure context/menu:overview/new_product/fmt:link" />
</li>
- <li>
- <a tal:replace="structure context/menu:overview/milestones/fmt:link" />
- </li>
</ul>
</div>
=== 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-05 15:15:17 +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-05 15:15:17 +0000
@@ -0,0 +1,208 @@
+# Copyright 2011-2012 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
+
+import transaction
+
+from lp.testing.layers import (
+ AppServerLayer,
+ DatabaseFunctionalLayer,
+ )
+from lp.registry.model.milestonetag import ProjectGroupMilestoneTag
+from lp.testing import (
+ person_logged_in,
+ TestCaseWithFactory,
+ ws_object,
+ )
+
+
+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([], 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), 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), 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([], 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)
+
+
+class MilestoneTagWebserviceTest(TestCaseWithFactory):
+ """Test the getter and setter for milestonetags."""
+
+ layer = AppServerLayer
+
+ def setUp(self):
+ super(MilestoneTagWebserviceTest, self).setUp()
+ self.owner = self.factory.makePerson()
+ self.product = self.factory.makeProduct(owner=self.owner)
+ self.milestone = self.factory.makeMilestone(product=self.product)
+ transaction.commit()
+ self.service = self.factory.makeLaunchpadService(self.owner)
+ self.ws_milestone = ws_object(
+ self.service, self.milestone)
+
+ def test_get_tags_none(self):
+ self.assertEqual([], self.ws_milestone.getTags())
+
+ def test_get_tags(self):
+ tags = [u'zeta', u'alpha', u'beta']
+ self.milestone.setTags(tags, self.owner)
+ self.assertEqual(sorted(tags), self.ws_milestone.getTags())
+
+ def test_set_tags_initial(self):
+ tags = [u'zeta', u'alpha', u'beta']
+ self.ws_milestone.setTags(tags=tags)
+ self.ws_milestone.lp_save()
+ transaction.begin()
+ self.assertEqual(sorted(tags), self.milestone.getTags())
+
+ def test_set_tags_replace(self):
+ tags1 = [u'zeta', u'alpha', u'beta']
+ self.milestone.setTags(tags1, self.owner)
+ tags2 = [u'delta', u'alpha', u'gamma']
+ self.ws_milestone.setTags(tags=tags2)
+ self.ws_milestone.lp_save()
+ transaction.begin()
+ self.assertEqual(sorted(tags2), self.milestone.getTags())
=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py 2012-01-05 03:11:37 +0000
+++ lib/lp/registry/tests/test_person.py 2012-01-05 15:15:17 +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-05 15:15:17 +0000
@@ -108,6 +108,7 @@
from lp.registry.interfaces.location import IPersonLocation
from lp.registry.interfaces.milestone import (
IMilestone,
+ IAbstractMilestone,
IProjectGroupMilestone,
)
from lp.registry.interfaces.nameblacklist import (
@@ -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-05 15:15:17 +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.
-
Follow ups