class ISpecification(ISpecificationPublic, ISpecificationView, ISpecificationEditRestricted, ISpecificationDriverRestricted, IBugLinkTarget): """A Specification.""" export_as_webservice_entry(as_of="beta") @mutator_for(ISpecificationView['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() @operation_for_version('devel') def linkBug(bug): """Link a bug to this specification. :param bug: IBug to link. """ @operation_parameters(bug=Reference(schema=Interface)) # Really IBug @export_write_operation() @operation_for_version('devel') def unlinkBug(bug): """Unlink a bug to this specification.
def setUp(self): super(TestWorkItemsText, self).setUp() self.field = WorkItemsText(__name__='test')
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'], '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'], 'TODO') parsed_lower = self.field.parseLine('Test this work item: todo') self.assertEqual(parsed_lower['status'], 'TODO') parsed_camel = self.field.parseLine('Test this work item: ToDo') self.assertEqual(parsed_camel['status'], '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_assignee_and_bracket(self): title = "Work item with one ] bracket" work_items_text = ("Work items:\n" "[person] %s: TODO" % title) parsed = self.field.parse(work_items_text) self.assertEqual( parsed, [{'title': title, 'status': 'TODO', 'assignee': 'person', 'milestone': None, 'sequence': 0}]) def test_assignee_and_brackets(self): title = "Work item with two [2] brackets" work_items_text = ("Work items:\n" "[person] %s: TODO" % title) parsed = self.field.parse(work_items_text) self.assertEqual( parsed, [{'title': title, 'status': 'TODO', 'assignee': 'person', 'milestone': None, 'sequence': 0}]) def test_no_assignee_and_brackets(self): title = "Work item with [] brackets" work_items_text = ("Work items:\n" "%s: TODO" % title) parsed = self.field.parse(work_items_text) self.assertEqual( parsed, [{'title': title, 'status': 'TODO', 'assignee': None, 'milestone': None, 'sequence': 0}]) def test_parse_none(self): # When empty text box is submitted, None is being passed in instead. work_items_text = None parsed = self.field.parse(work_items_text) self.assertEqual([], parsed) def test_multi_line_parsing(self): title_1 = 'Work item 1' title_2 = 'Work item 2' work_items_text = "Work items:\n%s: TODO\n%s: POSTPONED" % (title_1, title_2) parsed = self.field.parse(work_items_text) self.assertEqual( parsed, [{'title': title_1, 'status': 'TODO', 'assignee': None, 'milestone': None, 'sequence': 0}, {'title': title_2, 'status': 'POSTPONED', 'assignee': None, 'milestone': None, 'sequence': 1}]) def test_multi_line_parsing_different_milestones(self): title_1 = 'Work item 1' title_2 = 'Work item 2' work_items_text = ("Work items:\n%s: TODO\nWork items for test-ms:\n" "%s: POSTPONED" % (title_1, title_2)) parsed = self.field.parse(work_items_text) self.assertEqual( parsed, [{'title': title_1, 'status': 'TODO', 'assignee': None, 'milestone': None, 'sequence': 0}, {'title': title_2, 'status': 'POSTPONED', 'assignee': None, 'milestone': 'test-ms', 'sequence': 1}]) def test_multi_line_parsing_different_milestones_reversed(self): title_1 = 'Work item 1' title_2 = 'Work item 2' work_items_text = ("Work items for test-ms:\n%s: TODO\nWork items:\n" "%s: POSTPONED" % (title_1, title_2)) parsed = self.field.parse(work_items_text) self.assertEqual( parsed, [{'title': title_1, 'status': 'TODO', 'assignee': None, 'milestone': 'test-ms', 'sequence': 0}, {'title': title_2, 'status': '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_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_status(self): work_items_text = "A single work item: TODO" parsed = self.field.parse(work_items_text) self.assertEqual(parsed[0]['status'], 'TODO') 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': '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': 'POSTPONED', 'assignee': None, 'milestone': milestone_1, 'sequence': 0}, {'title': title_2, 'status': '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': 'POSTPONED', 'assignee': None, 'milestone': milestone_1, 'sequence': 0}, {'title': title_2, 'status': 'TODO', 'assignee': None, 'milestone': milestone_2, 'sequence': 1}, {'title': title_3, 'status': '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 TestWorkItemsTextValidation(TestCaseWithFactory): layer = DatabaseFunctionalLayer def setUp(self): super(TestWorkItemsTextValidation, self).setUp() self.field = WorkItemsText(__name__='test') def test_parseandvalidate(self): status = SpecificationWorkItemStatus.TODO assignee = self.factory.makePerson() milestone = self.factory.makeMilestone() title = 'A work item' specification = self.factory.makeSpecification( product=milestone.product) field = self.field.bind(specification) work_items_text = ( "Work items for %s:\n" "[%s]%s: %s" % (milestone.name, assignee.name, title, status.name)) work_item = field.parseAndValidate(work_items_text)[0] self.assertEqual({'assignee': assignee, 'milestone': milestone, 'sequence': 0, 'status': status, 'title': title}, work_item) 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) def test_validate_invalid_status(self): self.assertRaises( LaunchpadValidationError, self.field.getStatus, 'Invalid status: FOO') def test_validate_valid_statuses(self): statuses = [SpecificationWorkItemStatus.TODO, SpecificationWorkItemStatus.DONE, SpecificationWorkItemStatus.POSTPONED, SpecificationWorkItemStatus.INPROGRESS, SpecificationWorkItemStatus.BLOCKED] for status in statuses: validated_status = self.field.getStatus(status.name) self.assertEqual(validated_status, status)
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'], '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'], 'TODO') parsed_lower = self.field.parseLine('Test this work item: todo') self.assertEqual(parsed_lower['status'], 'TODO') parsed_camel = self.field.parseLine('Test this work item: ToDo') self.assertEqual(parsed_camel['status'], '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_assignee_and_bracket(self): title = "Work item with one ] bracket" work_items_text = ("Work items:\n" "[person] %s: TODO" % title) parsed = self.field.parse(work_items_text) self.assertEqual(parsed, [{ 'title': title, 'status': 'TODO', 'assignee': 'person', 'milestone': None, 'sequence': 0 }]) def test_assignee_and_brackets(self): title = "Work item with two [2] brackets" work_items_text = ("Work items:\n" "[person] %s: TODO" % title) parsed = self.field.parse(work_items_text) self.assertEqual(parsed, [{ 'title': title, 'status': 'TODO', 'assignee': 'person', 'milestone': None, 'sequence': 0 }]) def test_no_assignee_and_brackets(self): title = "Work item with [] brackets" work_items_text = ("Work items:\n" "%s: TODO" % title) parsed = self.field.parse(work_items_text) self.assertEqual(parsed, [{ 'title': title, 'status': 'TODO', 'assignee': None, 'milestone': None, 'sequence': 0 }]) def test_parse_none(self): # When empty text box is submitted, None is being passed in instead. work_items_text = None parsed = self.field.parse(work_items_text) self.assertEqual([], parsed) def test_multi_line_parsing(self): title_1 = 'Work item 1' title_2 = 'Work item 2' work_items_text = "Work items:\n%s: TODO\n%s: POSTPONED" % (title_1, title_2) parsed = self.field.parse(work_items_text) self.assertEqual(parsed, [{ 'title': title_1, 'status': 'TODO', 'assignee': None, 'milestone': None, 'sequence': 0 }, { 'title': title_2, 'status': 'POSTPONED', 'assignee': None, 'milestone': None, 'sequence': 1 }]) def test_multi_line_parsing_different_milestones(self): title_1 = 'Work item 1' title_2 = 'Work item 2' work_items_text = ("Work items:\n%s: TODO\nWork items for test-ms:\n" "%s: POSTPONED" % (title_1, title_2)) parsed = self.field.parse(work_items_text) self.assertEqual(parsed, [{ 'title': title_1, 'status': 'TODO', 'assignee': None, 'milestone': None, 'sequence': 0 }, { 'title': title_2, 'status': 'POSTPONED', 'assignee': None, 'milestone': 'test-ms', 'sequence': 1 }]) def test_multi_line_parsing_different_milestones_reversed(self): title_1 = 'Work item 1' title_2 = 'Work item 2' work_items_text = ("Work items for test-ms:\n%s: TODO\nWork items:\n" "%s: POSTPONED" % (title_1, title_2)) parsed = self.field.parse(work_items_text) self.assertEqual(parsed, [{ 'title': title_1, 'status': 'TODO', 'assignee': None, 'milestone': 'test-ms', 'sequence': 0 }, { 'title': title_2, 'status': '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_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_status(self): work_items_text = "A single work item: TODO" parsed = self.field.parse(work_items_text) self.assertEqual(parsed[0]['status'], 'TODO') 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': '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': 'POSTPONED', 'assignee': None, 'milestone': milestone_1, 'sequence': 0 }, { 'title': title_2, 'status': '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': 'POSTPONED', 'assignee': None, 'milestone': milestone_1, 'sequence': 0 }, { 'title': title_2, 'status': 'TODO', 'assignee': None, 'milestone': milestone_2, 'sequence': 1 }, { 'title': title_3, 'status': '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 TestWorkItemsTextValidation(TestCaseWithFactory): layer = DatabaseFunctionalLayer def setUp(self): super(TestWorkItemsTextValidation, self).setUp() self.field = WorkItemsText(__name__='test') def test_parseandvalidate(self): status = SpecificationWorkItemStatus.TODO assignee = self.factory.makePerson() milestone = self.factory.makeMilestone() title = 'A work item' specification = self.factory.makeSpecification( product=milestone.product) field = self.field.bind(specification) work_items_text = ("Work items for %s:\n" "[%s]%s: %s" % (milestone.name, assignee.name, title, status.name)) work_item = field.parseAndValidate(work_items_text)[0] self.assertEqual( { 'assignee': assignee, 'milestone': milestone, 'sequence': 0, 'status': status, 'title': title }, work_item) 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) def test_validate_invalid_status(self): self.assertRaises(LaunchpadValidationError, self.field.getStatus, 'Invalid status: FOO') def test_validate_valid_statuses(self): statuses = [ SpecificationWorkItemStatus.TODO, SpecificationWorkItemStatus.DONE, SpecificationWorkItemStatus.POSTPONED, SpecificationWorkItemStatus.INPROGRESS, SpecificationWorkItemStatus.BLOCKED ] for status in statuses: validated_status = self.field.getStatus(status.name) self.assertEqual(validated_status, status)
class ISpecificationView(IHasOwner, IHasLinkedBranches): """Specification's attributes and methods that require the permission launchpad.LimitedView. """ name = exported(SpecNameField( title=_('Name'), required=True, readonly=False, description=_( "May contain lower-case letters, numbers, and dashes. " "It will be used in the specification url. " "Examples: mozilla-type-ahead-find, postgres-smart-serial.")), as_of="devel") title = exported(Title( title=_('Title'), required=True, description=_( "Describe the feature as clearly as possible in up to 70 " "characters. This title is displayed in every feature " "list or report.")), as_of="devel") specurl = exported( SpecURLField( title=_('Specification URL'), required=False, description=_( "The URL of the specification. This is usually a wiki page."), constraint=valid_webref), exported_as="specification_url", as_of="devel", ) summary = exported(Summary( title=_('Summary'), required=True, description=_( "A single-paragraph description of the feature. " "This will also be displayed in most feature listings.")), as_of="devel") definition_status = exported(Choice( title=_('Definition Status'), readonly=True, vocabulary=SpecificationDefinitionStatus, default=SpecificationDefinitionStatus.NEW, description=_( "The current status of the process to define the " "feature and get approval for the implementation plan.")), as_of="devel") assignee = exported(PublicPersonChoice( title=_('Assignee'), required=False, description=_("The person responsible for implementing the feature."), vocabulary='ValidPersonOrTeam'), as_of="devel") assigneeID = Attribute('db assignee value') drafter = exported(PublicPersonChoice( title=_('Drafter'), required=False, description=_( "The person responsible for drafting the specification."), vocabulary='ValidPersonOrTeam'), as_of="devel") drafterID = Attribute('db drafter value') approver = exported(PublicPersonChoice( title=_('Approver'), required=False, description=_( "The person responsible for approving the specification, " "and for reviewing the code when it's ready to be landed."), vocabulary='ValidPersonOrTeam'), as_of="devel") approverID = Attribute('db approver value') priority = exported(Choice(title=_('Priority'), vocabulary=SpecificationPriority, default=SpecificationPriority.UNDEFINED, required=True), as_of="devel") datecreated = exported( Datetime(title=_('Date Created'), required=True, readonly=True), as_of="devel", exported_as="date_created", ) owner = exported(PublicPersonChoice(title=_('Owner'), required=True, readonly=True, vocabulary='ValidPersonOrTeam'), as_of="devel") product = Choice(title=_('Project'), required=False, vocabulary='Product') distribution = Choice(title=_('Distribution'), required=False, vocabulary='Distribution') # Exported as readonly for simplicity, but could be exported as read-write # using setTarget() as the mutator. target = exported( ReferenceChoice( title=_('For'), required=True, vocabulary='DistributionOrProduct', description=_( "The project for which this proposal is being made."), schema=ISpecificationTarget), as_of="devel", readonly=True, ) productseries = Choice( title=_('Series Goal'), required=False, vocabulary='FilteredProductSeries', description=_( "Choose a series in which you would like to deliver this " "feature. Selecting '(nothing selected)' will clear the goal.")) distroseries = Choice( title=_('Series Goal'), required=False, vocabulary='FilteredDistroSeries', description=_( "Choose a series in which you would like to deliver this " "feature. Selecting '(nothing selected)' will clear the goal.")) # milestone milestone = exported(ReferenceChoice( title=_('Milestone'), required=False, vocabulary='Milestone', description=_( "The milestone in which we would like this feature to be " "delivered."), schema=IMilestone), as_of="devel") # nomination to a series for release management # XXX: It'd be nice to export goal as read-only, but it's tricky because # users will need to be aware of goalstatus as what's returned by .goal # may not be the accepted goal. goal = Attribute("The series for which this feature is a goal.") goalstatus = Choice( title=_('Goal Acceptance'), vocabulary=SpecificationGoalStatus, default=SpecificationGoalStatus.PROPOSED, description=_( "Whether or not the drivers have accepted this feature as " "a goal for the targeted series.")) goal_proposer = Attribute("The person who nominated the spec for " "this series.") date_goal_proposed = Attribute("The date of the nomination.") goal_decider = Attribute("The person who approved or declined " "the spec a a goal.") date_goal_decided = Attribute("The date the spec was approved " "or declined as a goal.") work_items = List(description=_( "All non-deleted work items for this spec, sorted by " "their 'sequence'"), value_type=Reference(schema=ISpecificationWorkItem), readonly=True) whiteboard = exported(Text( title=_('Status Whiteboard'), required=False, description=_("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, description=_("Check this to indicate that the drafter and assignee " "have satisfied the approver that they are headed in " "the right basic direction with this specification.")), as_of="devel") man_days = Int( title=_("Estimated Developer Days"), required=False, default=None, description=_( "An estimate of the " "number of developer days it will take to implement this feature. " "Please only provide an estimate if you are relatively confident " "in the number.")) implementation_status = exported(Choice( title=_("Implementation Status"), required=True, readonly=True, default=SpecificationImplementationStatus.UNKNOWN, vocabulary=SpecificationImplementationStatus, description=_("The state of progress being made on the actual " "implementation or delivery of this feature.")), as_of="devel") superseded_by = Choice( title=_("Superseded by"), required=False, default=None, vocabulary='Specification', description=_( "The specification " "which supersedes this one. Note that selecting a specification " "here and pressing Continue will change the specification " "status to Superseded.")) # lifecycle starter = exported(PublicPersonChoice( title=_('Starter'), required=False, readonly=True, description=_( 'The person who first set the state of the ' 'spec to the values that we consider mark it as started.'), vocabulary='ValidPersonOrTeam'), as_of="devel") date_started = exported(Datetime( title=_('Date Started'), required=False, readonly=True, description=_('The date when this spec was marked started.')), as_of="devel") completer = exported(PublicPersonChoice( title=_('Starter'), required=False, readonly=True, description=_( 'The person who finally set the state of the ' 'spec to the values that we consider mark it as complete.'), vocabulary='ValidPersonOrTeam'), as_of="devel") date_completed = exported(Datetime( title=_('Date Completed'), required=False, readonly=True, description=_( 'The date when this spec was marked ' 'complete. Note that complete also includes "obsolete" and ' 'superseded. Essentially, it is the state where no more work ' 'will be done on the feature.')), as_of="devel") # joins subscriptions = Attribute('The set of subscriptions to this spec.') subscribers = Attribute('The set of subscribers to this spec.') sprints = Attribute('The sprints at which this spec is discussed.') sprint_links = Attribute('The entries that link this spec to sprints.') dependencies = exported( CollectionField( title=_('Specs on which this one depends.'), value_type=Reference(schema=Interface), # ISpecification, really. readonly=True), as_of="devel") linked_branches = exported( CollectionField( title=_("Branches associated with this spec, usually " "branches on which this spec is being implemented."), value_type=Reference(schema=Interface), # ISpecificationBranch readonly=True), as_of="devel") def getDependencies(): """Specs on which this one depends.""" def getBlockedSpecs(): """Specs for which this spec is a dependency.""" # emergent properties informational = Attribute('Is True if this spec is purely informational ' 'and requires no implementation.') is_complete = exported(Bool( title=_('Is started'), readonly=True, required=True, description=_( 'Is True if this spec is already completely implemented. ' 'Note that it is True for informational specs, since ' 'they describe general functionality rather than specific ' 'code to be written. It is also true of obsolete and ' 'superseded specs, since there is no longer any need ' 'to schedule work for them.')), as_of="devel") is_incomplete = Attribute( 'Is True if this work still needs to ' 'be done. Is in fact always the opposite of is_complete.') is_blocked = Attribute('Is True if this spec depends on another spec ' 'which is still incomplete.') is_started = exported(Bool( title=_('Is started'), readonly=True, required=True, description=_( 'Is True if the spec is in a state which ' 'we consider to be "started". This looks at the delivery ' 'attribute, and also considers informational specs to be ' 'started when they are approved.')), as_of="devel") lifecycle_status = exported(Choice( title=_('Lifecycle Status'), vocabulary=SpecificationLifecycleStatus, default=SpecificationLifecycleStatus.NOTSTARTED, readonly=True), as_of="devel") def all_deps(): """All the dependencies, including dependencies of dependencies. If a user is provided, filters to only dependencies the user can see. """ def all_blocked(): """All specs blocked on this, and those blocked on the blocked ones. If a user is provided, filters to only blocked dependencies the user can see. """ def validateMove(target): """Check that the specification can be moved to the target.""" def getSprintSpecification(sprintname): """Get the record that links this spec to the named sprint.""" def notificationRecipientAddresses(): """Return the list of email addresses that receive notifications.""" has_accepted_goal = exported(Bool( title=_('Series goal is accepted'), readonly=True, required=True, description=_( 'Is true if this specification has been ' 'proposed as a goal for a specific series, ' 'and the drivers of that series have accepted the goal.')), as_of="devel") # lifecycle management def updateLifecycleStatus(user): """Mark the specification as started, and/or complete, if appropriate. This will verify that the state of the specification is in fact "complete" (there is a completeness test in Specification.is_complete) and then record the completer and the date_completed. If the spec is not completed, then it ensures that nothing is recorded about its completion. It returns a SpecificationLifecycleStatus dbschema showing the overall state of the specification IF the state has changed. """ # event-related methods def getDelta(old_spec, user): """Return a dictionary of things that changed between this spec and the old_spec. This method is primarily used by event subscription code, to determine what has changed during an ObjectModifiedEvent. """ # subscription-related methods def subscription(person): """Return the subscription for this person to this spec, or None.""" @operation_parameters(person=Reference(IPerson, title=_('Person'), required=True), essential=copy_field( ISpecificationSubscription['essential'], required=False)) @call_with(subscribed_by=REQUEST_USER) @export_write_operation() @operation_for_version('devel') def subscribe(person, subscribed_by=None, essential=False): """Subscribe this person to the feature specification.""" @operation_parameters(person=Reference(IPerson, title=_('Person'), required=False)) @call_with(unsubscribed_by=REQUEST_USER) @export_write_operation() @operation_for_version('devel') def unsubscribe(person, unsubscribed_by): """Remove the person's subscription to this spec.""" def getSubscriptionByName(name): """Return a subscription based on the person's name, or None.""" def isSubscribed(person): """Is person subscribed to this spec? Returns True if the user is explicitly subscribed to this spec (no matter what the type of subscription), otherwise False. If person is None, the return value is always False. """ # sprints def linkSprint(sprint, user): """Put this spec on the agenda of the sprint.""" def unlinkSprint(sprint): """Remove this spec from the agenda of the sprint.""" # dependencies def createDependency(specification): """Create a dependency for this spec on the spec provided.""" def removeDependency(specification): """Remove any dependency of this spec on the spec provided.""" # branches def getBranchLink(branch): """Return the SpecificationBranch link for the branch, or None.""" def getLinkedBugTasks(user): """Return the bug tasks that are relevant to this blueprint. When multiple tasks are on a bug, if one of the tasks is for the target, then only that task is returned. Otherwise the default bug task is returned. :param user: The user doing the search. """ def getAllowedInformationTypes(who): """Get a list of acceptable `InformationType`s for this spec."""