| # Copyright 2016 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """The Monorail issue tracker uses ProtoRPC for storing business objects.""" |
| |
| from __future__ import print_function |
| from __future__ import division |
| from __future__ import absolute_import |
| |
| from protorpc import messages |
| |
| |
| class FieldValue(messages.Message): |
| """Holds a single custom field value in an issue. |
| |
| Multi-valued custom fields will have multiple such FieldValues on a given |
| issue. Note that enumerated type custom fields are represented as key-value |
| labels. |
| """ |
| field_id = messages.IntegerField(1, required=True) |
| # Only one of the following fields will hve any value. |
| int_value = messages.IntegerField(2) |
| str_value = messages.StringField(3) |
| user_id = messages.IntegerField(4) |
| date_value = messages.IntegerField(6) |
| url_value = messages.StringField(7) |
| |
| derived = messages.BooleanField(5, default=False) |
| |
| # None if field is not a phse field. |
| phase_id = messages.IntegerField(8) |
| |
| |
| class ApprovalStatus(messages.Enum): |
| """Statuses that an approval field could be set to.""" |
| NEEDS_REVIEW = 1 |
| NA = 2 |
| REVIEW_REQUESTED = 3 |
| REVIEW_STARTED = 4 |
| NEED_INFO = 5 |
| APPROVED = 6 |
| NOT_APPROVED = 7 |
| NOT_SET = 8 |
| |
| |
| class ApprovalValue(messages.Message): |
| """Holds a single approval field value in an issue.""" |
| approval_id = messages.IntegerField(1) |
| status = messages.EnumField(ApprovalStatus, 2, default='NOT_SET') |
| setter_id = messages.IntegerField(3) |
| set_on = messages.IntegerField(4) |
| approver_ids = messages.IntegerField(5, repeated=True) |
| phase_id = messages.IntegerField(7) |
| |
| |
| class ApprovalDelta(messages.Message): |
| """In-memory representation of requested changes to an issue's approval.""" |
| status = messages.EnumField(ApprovalStatus, 1) |
| set_on = messages.IntegerField(2) |
| setter_id = messages.IntegerField(3) |
| approver_ids_add = messages.IntegerField(4, repeated=True) |
| approver_ids_remove = messages.IntegerField(5, repeated=True) |
| subfield_vals_add = messages.MessageField(FieldValue, 6, repeated=True) |
| subfield_vals_remove = messages.MessageField(FieldValue, 7, repeated=True) |
| subfields_clear = messages.IntegerField(8, repeated=True) |
| # Stores Approval's Enum subfield changes. |
| labels_add = messages.StringField(9, repeated=True) |
| labels_remove = messages.StringField(10, repeated=True) |
| |
| |
| class Phase(messages.Message): |
| """Holds a single launch review phase.""" |
| phase_id = messages.IntegerField(1) |
| name = messages.StringField(2) |
| rank = messages.IntegerField(4) |
| |
| |
| class DanglingIssueRef(messages.Message): |
| """Holds a reference to an issue on Codesite or an external tracker.""" |
| project = messages.StringField(1, required=True) |
| issue_id = messages.IntegerField(2, required=True) |
| ext_issue_identifier = messages.StringField(3, required=False) |
| |
| |
| class Issue(messages.Message): |
| """Holds all the current metadata about an issue. |
| |
| The most frequent searches can work by consulting solely the issue metadata. |
| Display of the issue list is done solely with this issue metadata. |
| Displaying one issue in detail with description and comments requires |
| more info from other objects. |
| |
| The issue_id field is the unique primary key for retrieving issues. Local ID |
| is a small integer that counts up in each project. |
| |
| Summary, Status, Owner, CC, reporter, and opened_timestamp are hard |
| fields that are always there. All other metadata is stored as |
| labels or custom fields. |
| Next available tag: 63. |
| """ |
| # Globally unique issue ID. |
| issue_id = messages.IntegerField(42) |
| # project_name is not stored in the DB, only the project_id is stored. |
| # project_name is used in RAM to simplify formatting logic in lots of places. |
| project_name = messages.StringField(1, required=True) |
| project_id = messages.IntegerField(50) |
| local_id = messages.IntegerField(2, required=True) |
| summary = messages.StringField(3, default='') |
| status = messages.StringField(4, default='') |
| owner_id = messages.IntegerField(5) |
| cc_ids = messages.IntegerField(6, repeated=True) |
| labels = messages.StringField(7, repeated=True) |
| component_ids = messages.IntegerField(39, repeated=True) |
| |
| # Denormalized count of stars on this Issue. |
| star_count = messages.IntegerField(8, required=True, default=0) |
| reporter_id = messages.IntegerField(9, required=True, default=0) |
| # Time that the issue was opened, in seconds since the Epoch. |
| opened_timestamp = messages.IntegerField(10, required=True, default=0) |
| |
| # This should be set when an issue is closed and cleared when a |
| # closed issue is reopened. Measured in seconds since the Epoch. |
| closed_timestamp = messages.IntegerField(12, default=0) |
| |
| # This should be updated every time an issue is modified. Measured |
| # in seconds since the Epoch. |
| modified_timestamp = messages.IntegerField(13, default=0) |
| |
| # These timestamps are updated whenever owner, status, or components |
| # change, including when altered by a filter rule. |
| owner_modified_timestamp = messages.IntegerField(19, default=0) |
| status_modified_timestamp = messages.IntegerField(20, default=0) |
| component_modified_timestamp = messages.IntegerField(21, default=0) |
| |
| # Enhanced version of modified_timestamp that also captures changes to |
| # subresources of issues like stars, comments, and attachments. |
| # See: go/monorail-enhanced-modified-time |
| migration_modified_timestamp = messages.IntegerField(62, default=0) |
| |
| # Issue IDs of issues that this issue is blocked on. |
| blocked_on_iids = messages.IntegerField(16, repeated=True) |
| |
| # Rank values of issue relations that are blocking this issue. The issue |
| # with id blocked_on_iids[i] has rank value blocked_on_ranks[i] |
| blocked_on_ranks = messages.IntegerField(54, repeated=True) |
| |
| # Issue IDs of issues that this issue is blocking. |
| blocking_iids = messages.IntegerField(17, repeated=True) |
| |
| # References to 'dangling' (still in codesite) issue relations. |
| dangling_blocked_on_refs = messages.MessageField( |
| DanglingIssueRef, 52, repeated=True) |
| dangling_blocking_refs = messages.MessageField( |
| DanglingIssueRef, 53, repeated=True) |
| |
| # Issue ID of issue that this issue was merged into most recently. When it |
| # is missing or 0, it is considered to be not merged into any other issue. |
| merged_into = messages.IntegerField(18) |
| # Use this when an issue is a duplicate of an issue in an external tracker. |
| merged_into_external = messages.StringField(61) |
| |
| # Default derived via rules, used iff status == ''. |
| derived_status = messages.StringField(30, default='') |
| # Default derived via rules, used iff owner_id == 0. |
| derived_owner_id = messages.IntegerField(31, default=0) |
| # Additional CCs derived via rules. |
| derived_cc_ids = messages.IntegerField(32, repeated=True) |
| # Additional labels derived via rules. |
| derived_labels = messages.StringField(33, repeated=True) |
| # Additional notification email addresses derived via rules. |
| derived_notify_addrs = messages.StringField(34, repeated=True) |
| # Additional components derived via rules. |
| derived_component_ids = messages.IntegerField(40, repeated=True) |
| # Software development process warnings and errors generated by filter rules. |
| # TODO(jrobbins): these are not yet stored in the DB, they are only in RAM. |
| derived_warnings = messages.StringField(55, repeated=True) |
| derived_errors = messages.StringField(56, repeated=True) |
| |
| # Soft delete of the entire issue. |
| deleted = messages.BooleanField(35, default=False) |
| |
| # Total number of attachments in the issue |
| attachment_count = messages.IntegerField(36, default=0) |
| |
| # Total number of comments on the issue (not counting the initial comment |
| # created when the issue is created). |
| comment_count = messages.IntegerField(37, default=0) |
| |
| # Custom field values (other than enums) |
| field_values = messages.MessageField(FieldValue, 41, repeated=True) |
| |
| is_spam = messages.BooleanField(51, default=False) |
| # assume_stale is used in RAM to ensure that a value saved to the DB was |
| # loaded from the DB in the same request handler (not via the cache). |
| assume_stale = messages.BooleanField(57, default=True) |
| |
| phases = messages.MessageField(Phase, 59, repeated=True) |
| approval_values = messages.MessageField(ApprovalValue, 60, repeated=True) |
| |
| |
| class FieldID(messages.Enum): |
| """Possible fields that can be updated in an Amendment.""" |
| # The spelling of these names must match enum values in tracker.sql. |
| SUMMARY = 1 |
| STATUS = 2 |
| OWNER = 3 |
| CC = 4 |
| LABELS = 5 |
| BLOCKEDON = 6 |
| BLOCKING = 7 |
| MERGEDINTO = 8 |
| PROJECT = 9 |
| COMPONENTS = 10 |
| CUSTOM = 11 |
| WARNING = 12 |
| ERROR = 13 |
| |
| |
| class IssueDelta(messages.Message): |
| """In-memory representation of requested changes to an issue. |
| |
| Next available tag: 23 |
| """ |
| status = messages.StringField(1) |
| owner_id = messages.IntegerField(2) |
| cc_ids_add = messages.IntegerField(3, repeated=True) |
| cc_ids_remove = messages.IntegerField(4, repeated=True) |
| comp_ids_add = messages.IntegerField(5, repeated=True) |
| comp_ids_remove = messages.IntegerField(6, repeated=True) |
| labels_add = messages.StringField(7, repeated=True) |
| labels_remove = messages.StringField(8, repeated=True) |
| field_vals_add = messages.MessageField(FieldValue, 9, repeated=True) |
| field_vals_remove = messages.MessageField(FieldValue, 10, repeated=True) |
| fields_clear = messages.IntegerField(11, repeated=True) |
| blocked_on_add = messages.IntegerField(12, repeated=True) |
| blocked_on_remove = messages.IntegerField(13, repeated=True) |
| blocking_add = messages.IntegerField(14, repeated=True) |
| blocking_remove = messages.IntegerField(15, repeated=True) |
| merged_into = messages.IntegerField(16) |
| merged_into_external = messages.StringField(22) |
| summary = messages.StringField(17) |
| ext_blocked_on_add = messages.StringField(18, repeated=True) |
| ext_blocked_on_remove = messages.StringField(19, repeated=True) |
| ext_blocking_add = messages.StringField(20, repeated=True) |
| ext_blocking_remove = messages.StringField(21, repeated=True) |
| |
| |
| class Amendment(messages.Message): |
| """Holds info about one issue field change.""" |
| field = messages.EnumField(FieldID, 11, required=True) |
| # User-visible string describing the change |
| newvalue = messages.StringField(12) |
| # Newvalue could have + or - characters to indicate that labels and CCs |
| # were added or removed |
| # Users added to owner or cc field |
| added_user_ids = messages.IntegerField(29, repeated=True) |
| # Users removed from owner or cc |
| removed_user_ids = messages.IntegerField(30, repeated=True) |
| custom_field_name = messages.StringField(31) |
| # When having newvalue be a +/- string doesn't make sense (e.g. status), |
| # store the old value here so that it can still be displayed. |
| oldvalue = messages.StringField(32) |
| # New Components value add to the issue |
| added_component_ids = messages.IntegerField(33, repeated=True) |
| # Old Components value removed from the issue |
| removed_component_ids = messages.IntegerField(34, repeated=True) |
| |
| |
| class Attachment(messages.Message): |
| """Holds info about one attachment.""" |
| attachment_id = messages.IntegerField(21, required=True) |
| # Client-side filename |
| filename = messages.StringField(22, required=True) |
| filesize = messages.IntegerField(23, required=True) |
| # File mime-type, or at least our best guess. |
| mimetype = messages.StringField(24, required=True) |
| deleted = messages.BooleanField(27, default=False) |
| gcs_object_id = messages.StringField(29, required=False) |
| |
| |
| class IssueComment(messages.Message): |
| # TODO(lukasperaza): update first comment to is_description=True |
| """Holds one issue description or one additional comment on an issue. |
| |
| The IssueComment with the lowest timestamp is the issue description, |
| if there is no IssueComment with is_description=True; otherwise, the |
| IssueComment with is_description=True and the highest timestamp is |
| the issue description. |
| Next available tag: 56 |
| """ |
| id = messages.IntegerField(32) |
| # Issue ID of the issue that was commented on. |
| issue_id = messages.IntegerField(31, required=True) |
| project_id = messages.IntegerField(50) |
| # User who entered the comment |
| user_id = messages.IntegerField(4, required=True, default=0) |
| # id of the APPROVAL_TYPE fielddef, if this is an approval comment. |
| approval_id = messages.IntegerField(54) |
| # Time when comment was entered (seconds). |
| timestamp = messages.IntegerField(5, required=True) |
| # Text of the comment |
| content = messages.StringField(6, required=True) |
| # Audit trail of changes made w/ this comment |
| amendments = messages.MessageField(Amendment, 10, repeated=True) |
| |
| # Soft delete that can be undeleted. |
| # Deleted comments should not be shown to average users. |
| # If deleted, deleted_by contains the user id of user who deleted. |
| deleted_by = messages.IntegerField(13) |
| |
| attachments = messages.MessageField(Attachment, 20, repeated=True) |
| |
| # Sequence number of the comment |
| # The field is optional for compatibility with code existing before |
| # this field was added. |
| # In practice, issue_svc sets this for all comments in GetCommentsForIssue. |
| sequence = messages.IntegerField(26) |
| |
| # The body text of the inbound email that caused this issue comment |
| # to be automatically entered. If this field is non-empty, it means |
| # that the comment was added via an inbound email. Headers and attachments |
| # are not included. |
| inbound_message = messages.StringField(28) |
| |
| is_spam = messages.BooleanField(51, default=False) |
| |
| is_description = messages.BooleanField(52, default=False) |
| description_num = messages.StringField(53) |
| |
| # User ID of script that imported the comment on behalf of a user. |
| importer_id = messages.IntegerField(55, default=0) |
| |
| |
| class SavedQuery(messages.Message): |
| """Store a saved query, for either a project or a user.""" |
| query_id = messages.IntegerField(1) |
| name = messages.StringField(2) |
| base_query_id = messages.IntegerField(3) |
| query = messages.StringField(4, required=True) |
| |
| # For personal cross-project queries. |
| executes_in_project_ids = messages.IntegerField(5, repeated=True) |
| |
| # For user saved queries. |
| subscription_mode = messages.StringField(6) |
| |
| |
| class NotifyTriggers(messages.Enum): |
| """Issue tracker events that can trigger notification emails.""" |
| NEVER = 0 |
| ANY_COMMENT = 1 |
| # TODO(jrobbins): ANY_CHANGE, OPENED_CLOSED, ETC. |
| |
| |
| class FieldTypes(messages.Enum): |
| """Types of custom fields that Monorail supports.""" |
| ENUM_TYPE = 1 |
| INT_TYPE = 2 |
| STR_TYPE = 3 |
| USER_TYPE = 4 |
| DATE_TYPE = 5 |
| BOOL_TYPE = 6 |
| URL_TYPE = 7 |
| APPROVAL_TYPE = 8 |
| # TODO(jrobbins): more types, see tracker.sql for all TODOs. |
| |
| |
| class DateAction(messages.Enum): |
| """What to do when a date field value arrives.""" |
| NO_ACTION = 0 |
| PING_OWNER_ONLY = 1 |
| PING_PARTICIPANTS = 2 |
| |
| |
| class FieldDef(messages.Message): |
| """This PB stores info about one custom field definition.""" |
| field_id = messages.IntegerField(1, required=True) |
| project_id = messages.IntegerField(2, required=True) |
| field_name = messages.StringField(3, required=True) |
| field_type = messages.EnumField(FieldTypes, 4, required=True) |
| applicable_type = messages.StringField(11) |
| applicable_predicate = messages.StringField(10) |
| is_required = messages.BooleanField(5, default=False) |
| is_niche = messages.BooleanField(19, default=False) |
| is_multivalued = messages.BooleanField(6, default=False) |
| docstring = messages.StringField(7) |
| is_deleted = messages.BooleanField(8, default=False) |
| admin_ids = messages.IntegerField(9, repeated=True) |
| editor_ids = messages.IntegerField(24, repeated=True) |
| |
| # validation details for int_type |
| min_value = messages.IntegerField(12) |
| max_value = messages.IntegerField(13) |
| # validation details for str_type |
| regex = messages.StringField(14) |
| # validation details for user_type |
| needs_member = messages.BooleanField(15, default=False) |
| needs_perm = messages.StringField(16) |
| |
| # semantics for user_type fields |
| grants_perm = messages.StringField(17) |
| notify_on = messages.EnumField(NotifyTriggers, 18) |
| |
| # semantics for date_type fields |
| date_action = messages.EnumField(DateAction, 20) |
| |
| # field_id of the approval this FieldDef belongs to |
| approval_id = messages.IntegerField(21) |
| |
| # These fields should only be associated with issue phases |
| is_phase_field = messages.BooleanField(22, default=False) |
| |
| # boolean that indicates if this field is restricted |
| is_restricted_field = messages.BooleanField(23, default=False) |
| |
| |
| class ComponentDef(messages.Message): |
| """This stores info about a component in a project.""" |
| component_id = messages.IntegerField(1, required=True) |
| project_id = messages.IntegerField(2, required=True) |
| path = messages.StringField(3, required=True) |
| docstring = messages.StringField(4) |
| admin_ids = messages.IntegerField(5, repeated=True) |
| cc_ids = messages.IntegerField(6, repeated=True) |
| deprecated = messages.BooleanField(7, default=False) |
| created = messages.IntegerField(8) |
| creator_id = messages.IntegerField(9) |
| modified = messages.IntegerField(10) |
| modifier_id = messages.IntegerField(11) |
| label_ids = messages.IntegerField(12, repeated=True) |
| |
| |
| class FilterRule(messages.Message): |
| """Filter rules implement semantics as project-specific if-then rules.""" |
| predicate = messages.StringField(10, required=True) |
| |
| # If the predicate is satisfied, these actions set some of the derived_* |
| # fields on the issue: labels, status, owner, or CCs. |
| add_labels = messages.StringField(20, repeated=True) |
| default_status = messages.StringField(21) |
| default_owner_id = messages.IntegerField(22) |
| add_cc_ids = messages.IntegerField(23, repeated=True) |
| add_notify_addrs = messages.StringField(24, repeated=True) |
| warning = messages.StringField(25) |
| error = messages.StringField(26) |
| |
| |
| class StatusDef(messages.Message): |
| """Definition of one well-known issue status.""" |
| status = messages.StringField(11, required=True) |
| means_open = messages.BooleanField(12, default=False) |
| status_docstring = messages.StringField(13) |
| deprecated = messages.BooleanField(14, default=False) |
| |
| |
| class LabelDef(messages.Message): |
| """Definition of one well-known issue label.""" |
| label = messages.StringField(21, required=True) |
| label_docstring = messages.StringField(22) |
| deprecated = messages.BooleanField(23, default=False) |
| |
| |
| class ApprovalDef(messages.Message): |
| """Definition of an approval type field def.""" |
| # Note: approval_id is semantically required |
| approval_id = messages.IntegerField(1) |
| approver_ids = messages.IntegerField(4, repeated=True) |
| survey = messages.StringField(5) |
| |
| # Next available tag: 48 |
| class TemplateDef(messages.Message): |
| """Definition of one issue template.""" |
| template_id = messages.IntegerField(57) |
| name = messages.StringField(31, required=True) |
| content = messages.StringField(32, required=True) |
| summary = messages.StringField(33) |
| summary_must_be_edited = messages.BooleanField(34, default=False) |
| owner_id = messages.IntegerField(35) |
| status = messages.StringField(36) |
| # Note: labels field is considered to have been set iff summary was set. |
| labels = messages.StringField(37, repeated=True) |
| # This controls what is listed in the template drop-down menu. Users |
| # could still select any template by editing the URL, and that's OK. |
| members_only = messages.BooleanField(38, default=False) |
| # If no owner_id is specified, and owner_defaults_to_member is |
| # true, then when an issue is entered by a member, fill in the initial |
| # owner field with the signed in user's name. |
| owner_defaults_to_member = messages.BooleanField(39, default=True) |
| admin_ids = messages.IntegerField(41, repeated=True) |
| |
| # Custom field values (other than enums) |
| field_values = messages.MessageField(FieldValue, 42, repeated=True) |
| # Components. |
| component_ids = messages.IntegerField(43, repeated=True) |
| component_required = messages.BooleanField(44, default=False) |
| phases = messages.MessageField(Phase, 46, repeated=True) |
| approval_values = messages.MessageField(ApprovalValue, 47, repeated=True) |
| |
| |
| class ProjectIssueConfig(messages.Message): |
| """This holds all configuration info for one project. |
| |
| That includes canned queries, well-known issue statuses, |
| and well-known issue labels. |
| |
| "Well-known" means that they are always offered to the user in |
| drop-downs, even if there are currently no open issues that have |
| that label or status value. Deleting a well-known value from the |
| configuration does not change any issues that may still reference |
| that old label, and users are still free to use it. |
| |
| Exclusive label prefixes mean that a given issue may only have one |
| label that begins with that prefix. E.g., Priority should be |
| exclusive so that no issue can be labeled with both Priority-High |
| and Priority-Low. |
| Next available tag: 62 |
| """ |
| |
| project_id = messages.IntegerField(60) |
| well_known_statuses = messages.MessageField(StatusDef, 10, repeated=True) |
| # If an issue's status is being set to one of these, show "Merge with:". |
| statuses_offer_merge = messages.StringField(14, repeated=True) |
| |
| well_known_labels = messages.MessageField(LabelDef, 20, repeated=True) |
| exclusive_label_prefixes = messages.StringField(2, repeated=True) |
| |
| approval_defs = messages.MessageField(ApprovalDef, 61, repeated=True) |
| |
| field_defs = messages.MessageField(FieldDef, 5, repeated=True) |
| component_defs = messages.MessageField(ComponentDef, 6, repeated=True) |
| |
| default_template_for_developers = messages.IntegerField(3, required=True) |
| default_template_for_users = messages.IntegerField(4, required=True) |
| |
| # These options control the default appearance of the issue list or grid |
| # for non-members. |
| default_col_spec = messages.StringField(50, default='') |
| default_sort_spec = messages.StringField(51, default='') |
| default_x_attr = messages.StringField(52, default='') |
| default_y_attr = messages.StringField(53, default='') |
| |
| # These options control the default appearance of the issue list or grid |
| # for project members. |
| member_default_query = messages.StringField(57, default='') |
| |
| # This bool controls whether users are able to enter odd-ball |
| # labels and status values, or whether they are limited to only the |
| # well-known labels and status values defined on the admin subtab. |
| restrict_to_known = messages.BooleanField(16, default=False) |
| |
| # Allow special projects to have a custom URL for the "New issue" link. |
| custom_issue_entry_url = messages.StringField(56) |