launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #06518
[Merge] lp:~linaro-infrastructure/launchpad/workitems-widget into lp:launchpad
Mattias Backman has proposed merging lp:~linaro-infrastructure/launchpad/workitems-widget into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #940450 in Launchpad itself: "Implement new UI for editing work items"
https://bugs.launchpad.net/launchpad/+bug/940450
For more details, see:
https://code.launchpad.net/~linaro-infrastructure/launchpad/workitems-widget/+merge/94790
Hi,
This branch adds a text area for editing Work Items, similar to the blueprint Whiteboard. The input will be parsed and verified to conform to the format assumed today by status.ubuntu.com and status.linaro.org. Additionally some validation is done to ensure that valid lp accounts are used as assignees as well as milestones that are valid targets for the specification.
The format of this text area is:
--
This is blocked on something: BLOCKED
This is already done: DONE
[bob] Something Bob should do: TODO
Work items for ubuntu-12.04:
This will be worked on later: TODO
--
Thanks,
Mattias
--
https://code.launchpad.net/~linaro-infrastructure/launchpad/workitems-widget/+merge/94790
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~linaro-infrastructure/launchpad/workitems-widget into lp:launchpad.
=== modified file 'lib/lp/blueprints/adapters.py'
--- lib/lp/blueprints/adapters.py 2010-07-30 12:56:27 +0000
+++ lib/lp/blueprints/adapters.py 2012-02-27 20:09:20 +0000
@@ -18,12 +18,14 @@
summary=None, whiteboard=None, specurl=None, productseries=None,
distroseries=None, milestone=None, name=None, priority=None,
definition_status=None, target=None, bugs_linked=None,
- bugs_unlinked=None, approver=None, assignee=None, drafter=None):
+ bugs_unlinked=None, approver=None, assignee=None, drafter=None,
+ workitems_text=None):
self.specification = specification
self.user = user
self.title = title
self.summary = summary
self.whiteboard = whiteboard
+ self.workitems_text = workitems_text
self.specurl = specurl
self.productseries = productseries
self.distroseries = distroseries
=== modified file 'lib/lp/blueprints/browser/configure.zcml'
--- lib/lp/blueprints/browser/configure.zcml 2012-02-17 04:09:06 +0000
+++ lib/lp/blueprints/browser/configure.zcml 2012-02-27 20:09:20 +0000
@@ -373,6 +373,12 @@
permission="launchpad.AnyPerson"
template="../templates/specification-edit.pt"/>
<browser:page
+ name="+workitems"
+ for="lp.blueprints.interfaces.specification.ISpecification"
+ class="lp.blueprints.browser.specification.SpecificationEditWorkItemsView"
+ permission="launchpad.AnyPerson"
+ template="../templates/specification-edit.pt"/>
+ <browser:page
name="+people"
for="lp.blueprints.interfaces.specification.ISpecification"
class="lp.blueprints.browser.specification.SpecificationEditPeopleView"
=== modified file 'lib/lp/blueprints/browser/specification.py'
--- lib/lp/blueprints/browser/specification.py 2012-01-01 02:58:52 +0000
+++ lib/lp/blueprints/browser/specification.py 2012-02-27 20:09:20 +0000
@@ -21,6 +21,7 @@
'SpecificationEditStatusView',
'SpecificationEditView',
'SpecificationEditWhiteboardView',
+ 'SpecificationEditWorkItemsView',
'SpecificationGoalDecideView',
'SpecificationGoalProposeView',
'SpecificationLinkBranchView',
@@ -410,7 +411,7 @@
usedfor = ISpecification
links = ['edit', 'people', 'status', 'priority',
- 'whiteboard', 'proposegoal',
+ 'whiteboard', 'proposegoal', 'workitems',
'milestone', 'requestfeedback', 'givefeedback', 'subscription',
'addsubscriber',
'linkbug', 'unlinkbug', 'linkbranch',
@@ -520,6 +521,11 @@
return Link('+whiteboard', text, icon='edit')
@enabled_with_permission('launchpad.AnyPerson')
+ def workitems(self):
+ text = 'Edit work items'
+ return Link('+workitems', text, icon='edit')
+
+ @enabled_with_permission('launchpad.AnyPerson')
def linkbranch(self):
if self.context.linked_branches.count() > 0:
text = 'Link to another branch'
@@ -647,6 +653,14 @@
hide_empty=False)
@property
+ def workitems_text_widget(self):
+ """The Work Items text as a widget."""
+ return TextAreaEditorWidget(
+ self.context, ISpecification['workitems_text'], title="Work Items",
+ edit_view='+workitems', edit_title='Edit work items',
+ hide_empty=False)
+
+ @property
def direction_widget(self):
return BooleanChoiceWidget(
self.context, ISpecification['direction_approved'],
@@ -709,6 +723,12 @@
custom_widget('whiteboard', TextAreaWidget, height=15)
+class SpecificationEditWorkItemsView(SpecificationEditView):
+ label = 'Edit specification work items'
+ field_names = ['workitems_text']
+ custom_widget('workitems_text', TextAreaWidget, height=15)
+
+
class SpecificationEditPeopleView(SpecificationEditView):
label = 'Change the people involved'
field_names = ['assignee', 'drafter', 'approver', 'whiteboard']
=== modified file 'lib/lp/blueprints/configure.zcml'
--- lib/lp/blueprints/configure.zcml 2012-02-10 17:32:41 +0000
+++ lib/lp/blueprints/configure.zcml 2012-02-27 20:09:20 +0000
@@ -181,7 +181,8 @@
<require
permission="launchpad.AnyPerson"
attributes="linkBug
- unlinkBug"/>
+ unlinkBug
+ setWorkItems"/>
</class>
<class class="lp.blueprints.model.specificationbug.SpecificationBug">
=== modified file 'lib/lp/blueprints/interfaces/specification.py'
--- lib/lp/blueprints/interfaces/specification.py 2012-02-20 13:34:23 +0000
+++ lib/lp/blueprints/interfaces/specification.py 2012-02-27 20:09:20 +0000
@@ -79,6 +79,7 @@
PublicPersonChoice,
Summary,
Title,
+ WorkItemsText,
)
from lp.services.webapp import canonical_url
from lp.services.webapp.menu import structured
@@ -303,6 +304,13 @@
"Any notes on the status of this spec you would like to "
"make. Your changes will override the current text.")),
as_of="devel")
+ workitems_text = exported(
+ WorkItemsText(
+ title=_('Work Items'), required=False, readonly=True,
+ description=_(
+ "Work items for this specification input in a text format. "
+ "Your changes will override the current work items.")),
+ as_of="devel")
direction_approved = exported(
Bool(title=_('Basic direction approved?'),
required=True, default=False,
@@ -616,6 +624,16 @@
export_as_webservice_entry(as_of="beta")
+ @mutator_for(ISpecificationPublic['workitems_text'])
+ @operation_parameters(new_work_items=WorkItemsText())
+ @export_write_operation()
+ @operation_for_version('devel')
+ def setWorkItems(new_work_items):
+ """Set work items on this specification.
+
+ :param new_work_items: Work items to set.
+ """
+
@operation_parameters(
bug=Reference(schema=Interface)) # Really IBug
@export_write_operation()
@@ -694,6 +712,7 @@
title = Attribute("The spec title or None.")
summary = Attribute("The spec summary or None.")
whiteboard = Attribute("The spec whiteboard or None.")
+ workitems_text = Attribute("The spec work items as text or None.")
specurl = Attribute("The URL to the spec home page (not in Launchpad).")
productseries = Attribute("The product series.")
distroseries = Attribute("The series to which this is targeted.")
=== modified file 'lib/lp/blueprints/model/specification.py'
--- lib/lp/blueprints/model/specification.py 2012-02-20 14:19:42 +0000
+++ lib/lp/blueprints/model/specification.py 2012-02-27 20:09:20 +0000
@@ -224,6 +224,30 @@
self._subscriptions, key=lambda sub: person_sort_key(sub.person))
@property
+ def workitems_text(self):
+ """See ISpecification."""
+ workitems_lines = []
+ milestone = None
+ for work_item in self.work_items:
+ if work_item.milestone != milestone:
+ milestone = work_item.milestone
+ # Separate milestone blocks, but no blank line if this is the
+ # first work item
+ if work_item.sequence > 0:
+ workitems_lines.append("")
+ workitems_lines.append("Work items for %s:" % milestone.name)
+ assignee = work_item.assignee
+ if assignee is not None:
+ assignee_part = "[%s] " % assignee.name
+ else:
+ assignee_part = ""
+ # work_items are ordered by sequence
+ workitems_lines.append("%s%s: %s" % (assignee_part,
+ work_item.title,
+ work_item.status.name))
+ return "\n".join(workitems_lines)
+
+ @property
def target(self):
"""See ISpecification."""
if self.product:
@@ -249,6 +273,10 @@
SpecificationWorkItem, specification=self,
deleted=False).order_by("sequence")
+ def setWorkItems(self, new_work_items):
+ field = ISpecification['workitems_text'].bind(self)
+ self.updateWorkItems(field.parseAndValidate(new_work_items))
+
def _deleteWorkItemsNotMatching(self, titles):
"""Delete all work items whose title does not match the given ones.
@@ -570,7 +598,7 @@
"distroseries", "milestone"))
delta.recordNewAndOld(("name", "priority", "definition_status",
"target", "approver", "assignee", "drafter",
- "whiteboard"))
+ "whiteboard", "workitems_text"))
delta.recordListAddedAndRemoved("bugs",
"bugs_linked",
"bugs_unlinked")
@@ -1035,7 +1063,7 @@
def new(self, name, title, specurl, summary, definition_status,
owner, approver=None, product=None, distribution=None, assignee=None,
- drafter=None, whiteboard=None,
+ drafter=None, whiteboard=None, workitems_text=None,
priority=SpecificationPriority.UNDEFINED):
"""See ISpecificationSet."""
# Adapt the NewSpecificationDefinitionStatus item to a
=== modified file 'lib/lp/blueprints/model/tests/test_specification.py'
--- lib/lp/blueprints/model/tests/test_specification.py 2012-02-20 14:19:42 +0000
+++ lib/lp/blueprints/model/tests/test_specification.py 2012-02-27 20:09:20 +0000
@@ -152,6 +152,11 @@
layer = DatabaseFunctionalLayer
+ def assertWorkItemsTextContains(self, spec, work_items):
+ expected = "\n".join(
+ ["%s: %s" % (wi.title, wi.status.name) for wi in work_items])
+ self.assertEqual(expected, spec.workitems_text)
+
def test_anonymous_newworkitem_not_allowed(self):
spec = self.factory.makeSpecification()
login(ANONYMOUS)
@@ -179,6 +184,98 @@
self.assertEqual(title, work_item.title)
self.assertEqual(milestone, work_item.milestone)
+ def test_workitems_text_no_workitems(self):
+ spec = self.factory.makeSpecification()
+ self.assertEqual('', spec.workitems_text)
+
+ def test_workitems_text_deleted_workitem(self):
+ work_item = self.factory.makeSpecificationWorkItem(deleted=True)
+ self.assertEqual('', work_item.specification.workitems_text)
+
+ def test_workitems_text_single_workitem(self):
+ work_item = self.factory.makeSpecificationWorkItem()
+ self.assertWorkItemsTextContains(work_item.specification, [work_item])
+
+ def test_workitems_text_multi_workitems_all_statuses(self):
+ work_item1 = self.factory.makeSpecificationWorkItem(
+ status=SpecificationWorkItemStatus.TODO)
+ work_item2 = self.factory.makeSpecificationWorkItem(
+ specification=work_item1.specification,
+ status=SpecificationWorkItemStatus.DONE)
+ work_item3 = self.factory.makeSpecificationWorkItem(
+ specification=work_item1.specification,
+ status=SpecificationWorkItemStatus.POSTPONED)
+ work_item4 = self.factory.makeSpecificationWorkItem(
+ specification=work_item1.specification,
+ status=SpecificationWorkItemStatus.INPROGRESS)
+ work_item5 = self.factory.makeSpecificationWorkItem(
+ specification=work_item1.specification,
+ status=SpecificationWorkItemStatus.BLOCKED)
+ work_items = [work_item1, work_item2, work_item3, work_item4, work_item5]
+ self.assertWorkItemsTextContains(work_item1.specification, work_items)
+
+ def test_workitems_text_with_milestone(self):
+ spec = self.factory.makeSpecification()
+ milestone = self.factory.makeMilestone(product=spec.product)
+ login_person(spec.owner)
+ work_item = spec.newWorkItem(
+ title=u'new-work-item', sequence=0,
+ status=SpecificationWorkItemStatus.TODO,
+ milestone=milestone)
+ expected_wi_text = ("Work items for %s:\n"
+ "%s: TODO" % \
+ (milestone.name, work_item.title))
+ self.assertEqual(expected_wi_text, spec.workitems_text)
+
+ def test_workitems_text_with_implicit_and_explicit_milestone(self):
+ spec = self.factory.makeSpecification()
+ milestone = self.factory.makeMilestone(product=spec.product)
+ login_person(spec.owner)
+ work_item1 = spec.newWorkItem(
+ title=u'Work item with default milestone', sequence=0,
+ status=SpecificationWorkItemStatus.TODO,
+ milestone=None)
+ work_item2 = spec.newWorkItem(
+ title=u'Work item with set milestone', sequence=1,
+ status=SpecificationWorkItemStatus.TODO,
+ milestone=milestone)
+ expected_wi_text = ("%s: TODO\n"
+ "\nWork items for %s:\n"
+ "%s: TODO" % \
+ (work_item1.title, milestone.name,
+ work_item2.title))
+ self.assertEqual(expected_wi_text, spec.workitems_text)
+
+ def test_workitems_text_with_different_milestones(self):
+ spec = self.factory.makeSpecification()
+ milestone1 = self.factory.makeMilestone(product=spec.product)
+ milestone2 = self.factory.makeMilestone(product=spec.product)
+ login_person(spec.owner)
+ work_item1 = spec.newWorkItem(
+ title=u'Work item with first milestone', sequence=0,
+ status=SpecificationWorkItemStatus.TODO,
+ milestone=milestone1)
+ work_item2 = spec.newWorkItem(
+ title=u'Work item with second milestone', sequence=1,
+ status=SpecificationWorkItemStatus.TODO,
+ milestone=milestone2)
+ expected_wi_text = ("Work items for %s:\n"
+ "%s: TODO\n"
+ "\nWork items for %s:\n"
+ "%s: TODO" % \
+ (milestone1.name, work_item1.title,
+ milestone2.name, work_item2.title))
+ self.assertEqual(expected_wi_text, spec.workitems_text)
+
+ def test_workitems_text_with_assignee(self):
+ assignee = self.factory.makePerson()
+ work_item = self.factory.makeSpecificationWorkItem(assignee=assignee)
+ expected_wi_text = ("[%s] %s: %s" % \
+ (work_item.assignee.name, work_item.title,
+ work_item.status.name))
+ self.assertEqual(expected_wi_text,
+ work_item.specification.workitems_text)
+
def test_work_items_property(self):
spec = self.factory.makeSpecification()
wi1 = self.factory.makeSpecificationWorkItem(
=== modified file 'lib/lp/blueprints/templates/specification-index.pt'
--- lib/lp/blueprints/templates/specification-index.pt 2012-02-01 15:31:32 +0000
+++ lib/lp/blueprints/templates/specification-index.pt 2012-02-27 20:09:20 +0000
@@ -280,6 +280,10 @@
</div>
<div class="portlet">
+ <div class="wide" tal:content="structure view/workitems_text_widget" />
+ </div>
+
+ <div class="portlet">
<div class="wide" tal:content="structure view/whiteboard_widget" />
</div>
=== modified file 'lib/lp/blueprints/tests/test_webservice.py'
--- lib/lp/blueprints/tests/test_webservice.py 2012-01-01 02:58:52 +0000
+++ lib/lp/blueprints/tests/test_webservice.py 2012-02-27 20:09:20 +0000
@@ -143,6 +143,12 @@
spec_webservice = self.getSpecOnWebservice(spec)
self.assertEqual(spec.whiteboard, spec_webservice.whiteboard)
+ def test_representation_contains_workitems(self):
+ work_item = self.factory.makeSpecificationWorkItem()
+ spec_webservice = self.getSpecOnWebservice(work_item.specification)
+ self.assertEqual(work_item.specification.work_items_text,
+ spec_webservice.work_items_text)
+
def test_representation_contains_milestone(self):
product = self.factory.makeProduct()
productseries = self.factory.makeProductSeries(product=product)
=== modified file 'lib/lp/services/fields/__init__.py'
--- lib/lp/services/fields/__init__.py 2012-02-16 00:38:53 +0000
+++ lib/lp/services/fields/__init__.py 2012-02-27 20:09:20 +0000
@@ -53,6 +53,7 @@
'URIField',
'UniqueField',
'Whiteboard',
+ 'WorkItemsText',
'is_public_person_or_closed_team',
'is_public_person',
]
@@ -102,6 +103,7 @@
name_validator,
valid_name,
)
+from lp.blueprints.enums import SpecificationWorkItemStatus
from lp.bugs.errors import InvalidDuplicateValue
from lp.registry.interfaces.pillar import IPillarNameSet
from lp.services.webapp.interfaces import ILaunchBag
@@ -858,3 +860,101 @@
else:
# The vocabulary prevents the revealing of private team names.
raise PrivateTeamNotAllowed(value)
+
+
+class WorkItemsText(Text):
+
+ def parseLine(self, line):
+ assert line.strip() != '', "Please don't give us an empty line"
+ try:
+ title, status = line.rsplit(':', 1)
+ except ValueError:
+ raise LaunchpadValidationError(
+ 'Missing work item status on "%s".' % line)
+
+ status = status.strip().lower()
+
+ assignee = None
+ if title.startswith('['):
+ if ']' in title:
+ off = title.index(']')
+ assignee = title[1:off]
+ title = title[off + 1:].strip()
+ else:
+ raise LaunchpadValidationError(
+ 'Missing closing "]" for assignee on "%s".' % line)
+
+ if title == '':
+ raise LaunchpadValidationError(
+ 'No work item title found on "%s"' % line)
+
+ valid_statuses = SpecificationWorkItemStatus.items
+ if status not in [item.name.lower() for item in valid_statuses]:
+ raise LaunchpadValidationError('Unknown status: %s' % status)
+ status = valid_statuses[status.upper()]
+
+ return {'title': title, 'status': status, 'assignee': assignee}
+
+ def parse(self, text):
+ sequence = 0
+ milestone = None
+ work_items = []
+ milestone_re = re.compile('^work items(.*)\s*:\s*$', re.I)
+ for line in text.splitlines():
+ if line.strip() == '':
+ continue
+ milestone_match = milestone_re.search(line)
+ if milestone_match:
+ milestone_part = milestone_match.group(1).strip()
+ milestone = milestone_part.split()[-1]
+ else:
+ new_work_item = self.parseLine(line)
+ new_work_item['milestone'] = milestone
+ new_work_item['sequence'] = sequence
+ sequence += 1
+ work_items.append(new_work_item)
+ return work_items
+
+ def validate(self, value):
+ self.parseAndValidate(value)
+
+ def parseAndValidate(self, text):
+ work_items = self.parse(text)
+ for work_item in work_items:
+ work_item['assignee'] = self.getAssignee(work_item['assignee'])
+ work_item['milestone'] = self.getMilestone(work_item['milestone'])
+ return work_items
+
+ def getAssignee(self, assignee_name):
+ if assignee_name is None:
+ return None
+ from lp.registry.interfaces.person import IPersonSet
+ assignee = getUtility(IPersonSet).getByName(assignee_name)
+ if assignee is None:
+ raise LaunchpadValidationError("Unknown person name: %s" % assignee_name)
+ return assignee
+
+ def getMilestone(self, milestone_name):
+ if milestone_name is None:
+ return None
+
+ target = self.context.target
+
+ milestone = None
+ from lp.registry.interfaces.distribution import IDistribution
+ from lp.registry.interfaces.milestone import IMilestoneSet
+ from lp.registry.interfaces.product import IProduct
+ if IProduct.providedBy(target):
+ milestone = getUtility(IMilestoneSet).getByNameAndProduct(
+ milestone_name, target)
+ elif IDistribution.providedBy(target):
+ milestone = getUtility(IMilestoneSet).getByNameAndDistribution(
+ milestone_name, target)
+ else:
+ raise AssertionError("Unexpected target type.")
+
+ if milestone is None:
+ raise LaunchpadValidationError("The milestone '%s' is not valid "
+ "for the target '%s'." % \
+ (milestone_name, target.name))
+ return milestone
=== modified file 'lib/lp/services/fields/tests/test_fields.py'
--- lib/lp/services/fields/tests/test_fields.py 2012-01-01 02:58:52 +0000
+++ lib/lp/services/fields/tests/test_fields.py 2012-02-27 20:09:20 +0000
@@ -14,6 +14,7 @@
from zope.schema.interfaces import TooShort
from lp.app.validators import LaunchpadValidationError
+from lp.blueprints.enums import SpecificationWorkItemStatus
from lp.registry.interfaces.nameblacklist import INameBlacklistSet
from lp.registry.interfaces.person import (
CLOSED_TEAM_POLICY,
@@ -26,6 +27,7 @@
FormattableDate,
is_public_person_or_closed_team,
StrippableText,
+ WorkItemsText,
)
from lp.testing import (
login_person,
@@ -108,6 +110,256 @@
self.assertEqual(None, field.validate(u' a '))
+class TestWorkItemsTextAssigneeAndMilestone(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestWorkItemsTextAssigneeAndMilestone, self).setUp()
+ self.field = WorkItemsText(__name__='test')
+
+ def test_unknown_assignee_is_rejected(self):
+ person_name = 'test-person'
+ self.assertRaises(
+ LaunchpadValidationError, self.field.getAssignee, person_name)
+
+ def test_validate_valid_assignee(self):
+ assignee = self.factory.makePerson()
+ self.assertEqual(assignee, self.field.getAssignee(assignee.name))
+
+ def test_validate_unset_assignee(self):
+ self.assertIs(None, self.field.getAssignee(None))
+
+ def test_validate_unset_milestone(self):
+ self.assertIs(None, self.field.getMilestone(None))
+
+ def test_validate_unknown_milestone(self):
+ specification = self.factory.makeSpecification()
+ field = self.field.bind(specification)
+ self.assertRaises(
+ LaunchpadValidationError, field.getMilestone, 'does-not-exist')
+
+ def test_validate_valid_product_milestone(self):
+ milestone = self.factory.makeMilestone()
+ specification = self.factory.makeSpecification(
+ product=milestone.product)
+ field = self.field.bind(specification)
+ self.assertEqual(milestone, field.getMilestone(milestone.name))
+
+ def test_validate_valid_distro_milestone(self):
+ distro = self.factory.makeDistribution()
+ milestone = self.factory.makeMilestone(distribution=distro)
+ specification = self.factory.makeSpecification(
+ distribution=milestone.distribution)
+ field = self.field.bind(specification)
+ self.assertEqual(milestone, field.getMilestone(milestone.name))
+
+ def test_validate_invalid_milestone(self):
+ milestone_name = 'test-milestone'
+ self.factory.makeMilestone(name=milestone_name)
+ # Milestone exists but is not a target for this spec.
+ specification = self.factory.makeSpecification(product=None)
+ field = self.field.bind(specification)
+ self.assertRaises(
+ LaunchpadValidationError, field.getMilestone, milestone_name)
+
+
+class TestWorkItemsText(TestCase):
+
+ def setUp(self):
+ super(TestWorkItemsText, self).setUp()
+ self.field = WorkItemsText(__name__='test')
+
+ def test_validate_raises_LaunchpadValidationError(self):
+ self.assertRaises(
+ LaunchpadValidationError, self.field.validate,
+ 'This is not a valid work item.')
+
+ def test_single_line_parsing(self):
+ work_items_title = 'Test this work item'
+ parsed = self.field.parseLine('%s: TODO' % (work_items_title))
+ self.assertEqual(parsed['title'], work_items_title)
+ self.assertEqual(parsed['status'], SpecificationWorkItemStatus.TODO)
+
+ def test_url_and_colon_in_title(self):
+ work_items_title = 'Test this: which is a url: http://www.linaro.org/'
+ parsed = self.field.parseLine('%s: TODO' % (work_items_title))
+ self.assertEqual(parsed['title'], work_items_title)
+
+ def test_silly_caps_status_parsing(self):
+ parsed_upper = self.field.parseLine('Test this work item: TODO ')
+ self.assertEqual(parsed_upper['status'],
+ SpecificationWorkItemStatus.TODO)
+ parsed_lower = self.field.parseLine('Test this work item: todo')
+ self.assertEqual(parsed_lower['status'],
+ SpecificationWorkItemStatus.TODO)
+ parsed_camel = self.field.parseLine('Test this work item: ToDo')
+ self.assertEqual(parsed_camel['status'],
+ SpecificationWorkItemStatus.TODO)
+
+ def test_parseLine_without_status_fails(self):
+ # We should require an explicit status to avoid the problem of work
+ # items with a url but no status.
+ self.assertRaises(
+ LaunchpadValidationError, self.field.parseLine,
+ 'Missing status')
+
+ def test_parseLine_without_title_fails(self):
+ self.assertRaises(
+ LaunchpadValidationError, self.field.parseLine,
+ ':TODO')
+
+ def test_parseLine_without_title_with_assignee_fails(self):
+ self.assertRaises(
+ LaunchpadValidationError, self.field.parseLine,
+ '[test-person] :TODO')
+
+ def test_multi_line_parsing(self):
+ title_1 = 'Work item 1'
+ title_2 = 'Work item 2'
+ work_items_text = "%s: TODO\n%s: POSTPONED" % (title_1, title_2)
+ parsed = self.field.parse(work_items_text)
+ self.assertEqual(
+ parsed, [{'title': title_1,
+ 'status': SpecificationWorkItemStatus.TODO,
+ 'assignee': None, 'milestone': None, 'sequence': 0},
+ {'title': title_2,
+ 'status': SpecificationWorkItemStatus.POSTPONED,
+ 'assignee': None, 'milestone': None, 'sequence': 1}])
+
+ def test_parse_assignee(self):
+ title = 'Work item 1'
+ assignee = 'test-person'
+ work_items_text = "[%s]%s: TODO" % (assignee, title)
+ parsed = self.field.parseLine(work_items_text)
+ self.assertEqual(parsed['assignee'], assignee)
+
+ def test_parse_assignee_with_space(self):
+ title = 'Work item 1'
+ assignee = 'test-person'
+ work_items_text = "[%s] %s: TODO" % (assignee, title)
+ parsed = self.field.parseLine(work_items_text)
+ self.assertEqual(parsed['assignee'], assignee)
+
+ def test_parseLine_with_missing_closing_bracket_for_assignee(self):
+ self.assertRaises(
+ LaunchpadValidationError, self.field.parseLine,
+ "[test-person A single work item: TODO")
+
+ def test_parseLine_with_invalid_status(self):
+ self.assertRaises(
+ LaunchpadValidationError, self.field.parseLine,
+ 'Invalid status: FOO')
+
+ def test_parseLine_todo_status(self):
+ status = SpecificationWorkItemStatus.TODO.name
+ work_items_text = "Just a work item: %s" % status
+ parsed = self.field.parseLine(work_items_text)
+ self.assertEqual(parsed['status'].name, status)
+
+ def test_parseLine_done_status(self):
+ status = SpecificationWorkItemStatus.DONE.name
+ work_items_text = "Just a work item: %s" % status
+ parsed = self.field.parseLine(work_items_text)
+ self.assertEqual(parsed['status'].name, status)
+
+ def test_parseLine_postponed_status(self):
+ status = SpecificationWorkItemStatus.POSTPONED.name
+ work_items_text = "Just a work item: %s" % status
+ parsed = self.field.parseLine(work_items_text)
+ self.assertEqual(parsed['status'].name, status)
+
+ def test_parseLine_inprogress_status(self):
+ status = SpecificationWorkItemStatus.INPROGRESS.name
+ work_items_text = "Just a work item: %s" % status
+ parsed = self.field.parseLine(work_items_text)
+ self.assertEqual(parsed['status'].name, status)
+
+ def test_parseLine_blocked_status(self):
+ status = SpecificationWorkItemStatus.BLOCKED.name
+ work_items_text = "Just a work item: %s" % status
+ parsed = self.field.parseLine(work_items_text)
+ self.assertEqual(parsed['status'].name, status)
+
+ def test_parse_empty_line_raises(self):
+ self.assertRaises(
+ AssertionError, self.field.parseLine, " \t \t ")
+
+ def test_parse_empty_lines_have_no_meaning(self):
+ parsed = self.field.parse("\n\n\n\n\n\n\n\n")
+ self.assertEqual(parsed, [])
+
+ def test_parse_milestone(self):
+ milestone = '2012.02'
+ title = "Work item for a milestone"
+ work_items_text = "Work items for %s:\n%s: TODO" % (milestone, title)
+ parsed = self.field.parse(work_items_text)
+ self.assertEqual(parsed, [{'title': title,
+ 'status': SpecificationWorkItemStatus.TODO,
+ 'assignee': None, 'milestone': milestone, 'sequence': 0}])
+
+ def test_parse_multi_milestones(self):
+ milestone_1 = '2012.02'
+ milestone_2 = '2012.03'
+ title_1 = "Work item for a milestone"
+ title_2 = "Work item for a later milestone"
+ work_items_text = ("Work items for %s:\n%s: POSTPONED\n\nWork items "
+ "for %s:\n%s: TODO" % (milestone_1, title_1,
+ milestone_2, title_2))
+ parsed = self.field.parse(work_items_text)
+ self.assertEqual(parsed,
+ [{'title': title_1,
+ 'status': SpecificationWorkItemStatus.POSTPONED,
+ 'assignee': None, 'milestone': milestone_1,
+ 'sequence': 0},
+ {'title': title_2,
+ 'status': SpecificationWorkItemStatus.TODO,
+ 'assignee': None, 'milestone': milestone_2,
+ 'sequence': 1}])
+
+ def test_parse_orphaned_work_items(self):
+ # Work items not in a milestone block belong to the latest specified
+ # milestone.
+ milestone_1 = '2012.02'
+ milestone_2 = '2012.03'
+ title_1 = "Work item for a milestone"
+ title_2 = "Work item for a later milestone"
+ title_3 = "A work item preceeded by a blank line"
+ work_items_text = (
+ "Work items for %s:\n%s: POSTPONED\n\nWork items for %s:\n%s: "
+ "TODO\n\n%s: TODO" % (milestone_1, title_1, milestone_2, title_2,
+ title_3))
+ parsed = self.field.parse(work_items_text)
+ self.assertEqual(parsed,
+ [{'title': title_1,
+ 'status': SpecificationWorkItemStatus.POSTPONED,
+ 'assignee': None, 'milestone': milestone_1,
+ 'sequence': 0},
+ {'title': title_2,
+ 'status': SpecificationWorkItemStatus.TODO,
+ 'assignee': None, 'milestone': milestone_2,
+ 'sequence': 1},
+ {'title': title_3,
+ 'status': SpecificationWorkItemStatus.TODO,
+ 'assignee': None, 'milestone': milestone_2,
+ 'sequence': 2}])
+
+ def test_sequence_single_workitem(self):
+ parsed = self.field.parse("A single work item: TODO")
+ self.assertEqual(0, parsed[0]['sequence'])
+
+ def test_only_workitems_get_sequence(self):
+ parsed = self.field.parse("A single work item: TODO\n"
+ "A second work item: TODO\n"
+ "\n"
+ "Work items for 2012.02:\n"
+ "Work item for a milestone: TODO\n")
+ self.assertEqual([(wi['title'], wi['sequence']) for wi in parsed],
+ [("A single work item", 0), ("A second work item", 1),
+ ("Work item for a milestone", 2)])
+
+
+
class TestBlacklistableContentNameField(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
Follow ups