Merge branch 'main' into avm99963-monorail
Merged commit 34d8229ae2b51fb1a15bd208e6fe6185c94f6266
GitOrigin-RevId: 7ee0917f93a577e475f8e09526dd144d245593f4
diff --git a/mrproto/__init__.py b/mrproto/__init__.py
new file mode 100644
index 0000000..68130d5
--- /dev/null
+++ b/mrproto/__init__.py
@@ -0,0 +1,3 @@
+# 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.
diff --git a/mrproto/api_clients_config.proto b/mrproto/api_clients_config.proto
new file mode 100644
index 0000000..5ae7561
--- /dev/null
+++ b/mrproto/api_clients_config.proto
@@ -0,0 +1,41 @@
+// 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.
+
+// Schemas for monorail api client configs.
+// Command to generate api_clients_config_pb2.py: in monorail/ directory:
+// protoc ./mrproto/api_clients_config.proto --proto_path=./mrproto/ --python_out=./mrproto
+
+
+syntax = "proto2";
+
+package monorail;
+
+message ProjectPermission {
+ enum Role {
+ committer = 1;
+ contributor = 2;
+ }
+
+ optional string project = 1;
+ optional Role role = 2 [default = contributor];
+ repeated string extra_permissions = 3;
+}
+
+// Next available tag: 11
+message Client {
+ optional string client_email = 1;
+ optional string display_name = 2;
+ optional string client_id = 3;
+ repeated string allowed_origins = 10;
+ optional string description = 4;
+ repeated ProjectPermission project_permissions = 5;
+ optional int32 period_limit = 6 [default = 100000];
+ optional int32 lifetime_limit = 7 [default = 1000000];
+ repeated string contacts = 8;
+ optional int32 qpm_limit = 9 [default = 100];
+}
+
+message ClientCfg {
+ repeated Client clients = 1;
+}
diff --git a/mrproto/api_clients_config_pb2.py b/mrproto/api_clients_config_pb2.py
new file mode 100644
index 0000000..1a1d4bd
--- /dev/null
+++ b/mrproto/api_clients_config_pb2.py
@@ -0,0 +1,260 @@
+# -*- coding: utf-8 -*-
+# 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.
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api_clients_config.proto
+
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='api_clients_config.proto',
+ package='monorail',
+ syntax='proto2',
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ serialized_pb=b'\n\x18\x61pi_clients_config.proto\x12\x08monorail\"\xa4\x01\n\x11ProjectPermission\x12\x0f\n\x07project\x18\x01 \x01(\t\x12;\n\x04role\x18\x02 \x01(\x0e\x32 .monorail.ProjectPermission.Role:\x0b\x63ontributor\x12\x19\n\x11\x65xtra_permissions\x18\x03 \x03(\t\"&\n\x04Role\x12\r\n\tcommitter\x10\x01\x12\x0f\n\x0b\x63ontributor\x10\x02\"\x98\x02\n\x06\x43lient\x12\x14\n\x0c\x63lient_email\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12\x11\n\tclient_id\x18\x03 \x01(\t\x12\x17\n\x0f\x61llowed_origins\x18\n \x03(\t\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\x38\n\x13project_permissions\x18\x05 \x03(\x0b\x32\x1b.monorail.ProjectPermission\x12\x1c\n\x0cperiod_limit\x18\x06 \x01(\x05:\x06\x31\x30\x30\x30\x30\x30\x12\x1f\n\x0elifetime_limit\x18\x07 \x01(\x05:\x07\x31\x30\x30\x30\x30\x30\x30\x12\x10\n\x08\x63ontacts\x18\x08 \x03(\t\x12\x16\n\tqpm_limit\x18\t \x01(\x05:\x03\x31\x30\x30\".\n\tClientCfg\x12!\n\x07\x63lients\x18\x01 \x03(\x0b\x32\x10.monorail.Client'
+)
+
+
+
+_PROJECTPERMISSION_ROLE = _descriptor.EnumDescriptor(
+ name='Role',
+ full_name='monorail.ProjectPermission.Role',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='committer', index=0, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='contributor', index=1, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=165,
+ serialized_end=203,
+)
+_sym_db.RegisterEnumDescriptor(_PROJECTPERMISSION_ROLE)
+
+
+_PROJECTPERMISSION = _descriptor.Descriptor(
+ name='ProjectPermission',
+ full_name='monorail.ProjectPermission',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project', full_name='monorail.ProjectPermission.project', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=b"".decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='role', full_name='monorail.ProjectPermission.role', index=1,
+ number=2, type=14, cpp_type=8, label=1,
+ has_default_value=True, default_value=2,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='extra_permissions', full_name='monorail.ProjectPermission.extra_permissions', index=2,
+ number=3, type=9, cpp_type=9, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ _PROJECTPERMISSION_ROLE,
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto2',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=39,
+ serialized_end=203,
+)
+
+
+_CLIENT = _descriptor.Descriptor(
+ name='Client',
+ full_name='monorail.Client',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='client_email', full_name='monorail.Client.client_email', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=b"".decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='display_name', full_name='monorail.Client.display_name', index=1,
+ number=2, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=b"".decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='client_id', full_name='monorail.Client.client_id', index=2,
+ number=3, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=b"".decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='allowed_origins', full_name='monorail.Client.allowed_origins', index=3,
+ number=10, type=9, cpp_type=9, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='description', full_name='monorail.Client.description', index=4,
+ number=4, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=b"".decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='project_permissions', full_name='monorail.Client.project_permissions', index=5,
+ number=5, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='period_limit', full_name='monorail.Client.period_limit', index=6,
+ number=6, type=5, cpp_type=1, label=1,
+ has_default_value=True, default_value=100000,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='lifetime_limit', full_name='monorail.Client.lifetime_limit', index=7,
+ number=7, type=5, cpp_type=1, label=1,
+ has_default_value=True, default_value=1000000,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='contacts', full_name='monorail.Client.contacts', index=8,
+ number=8, type=9, cpp_type=9, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='qpm_limit', full_name='monorail.Client.qpm_limit', index=9,
+ number=9, type=5, cpp_type=1, label=1,
+ has_default_value=True, default_value=100,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto2',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=206,
+ serialized_end=486,
+)
+
+
+_CLIENTCFG = _descriptor.Descriptor(
+ name='ClientCfg',
+ full_name='monorail.ClientCfg',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='clients', full_name='monorail.ClientCfg.clients', index=0,
+ number=1, type=11, cpp_type=10, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto2',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=488,
+ serialized_end=534,
+)
+
+_PROJECTPERMISSION.fields_by_name['role'].enum_type = _PROJECTPERMISSION_ROLE
+_PROJECTPERMISSION_ROLE.containing_type = _PROJECTPERMISSION
+_CLIENT.fields_by_name['project_permissions'].message_type = _PROJECTPERMISSION
+_CLIENTCFG.fields_by_name['clients'].message_type = _CLIENT
+DESCRIPTOR.message_types_by_name['ProjectPermission'] = _PROJECTPERMISSION
+DESCRIPTOR.message_types_by_name['Client'] = _CLIENT
+DESCRIPTOR.message_types_by_name['ClientCfg'] = _CLIENTCFG
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+ProjectPermission = _reflection.GeneratedProtocolMessageType('ProjectPermission', (_message.Message,), {
+ 'DESCRIPTOR' : _PROJECTPERMISSION,
+ '__module__' : 'api_clients_config_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ProjectPermission)
+ })
+_sym_db.RegisterMessage(ProjectPermission)
+
+Client = _reflection.GeneratedProtocolMessageType('Client', (_message.Message,), {
+ 'DESCRIPTOR' : _CLIENT,
+ '__module__' : 'api_clients_config_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.Client)
+ })
+_sym_db.RegisterMessage(Client)
+
+ClientCfg = _reflection.GeneratedProtocolMessageType('ClientCfg', (_message.Message,), {
+ 'DESCRIPTOR' : _CLIENTCFG,
+ '__module__' : 'api_clients_config_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ClientCfg)
+ })
+_sym_db.RegisterMessage(ClientCfg)
+
+
+# @@protoc_insertion_point(module_scope)
diff --git a/mrproto/api_pb2_v1.py b/mrproto/api_pb2_v1.py
new file mode 100644
index 0000000..0bf1378
--- /dev/null
+++ b/mrproto/api_pb2_v1.py
@@ -0,0 +1,651 @@
+# 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.
+
+"""Protocol buffers for Monorail API."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from endpoints import ResourceContainer
+from protorpc import messages
+from protorpc import message_types
+
+from mrproto import usergroup_pb2
+
+
+########################## Helper Message ##########################
+
+
+class ErrorMessage(messages.Message):
+ """Request error."""
+ code = messages.IntegerField(
+ 1, required=True, variant=messages.Variant.INT32)
+ reason = messages.StringField(2, required=True)
+ message = messages.StringField(3, required=True)
+
+
+class Status(messages.Message):
+ """Issue status."""
+ status = messages.StringField(1, required=True)
+ meansOpen = messages.BooleanField(2, required=True)
+ description = messages.StringField(3)
+
+
+class Label(messages.Message):
+ """Issue label."""
+ label = messages.StringField(1, required=True)
+ description = messages.StringField(2)
+
+
+class Prompt(messages.Message):
+ """Default issue template values."""
+ name = messages.StringField(1, required=True)
+ title = messages.StringField(2)
+ description = messages.StringField(3)
+ titleMustBeEdited = messages.BooleanField(4)
+ status = messages.StringField(5)
+ labels = messages.StringField(6, repeated=True)
+ membersOnly = messages.BooleanField(7)
+ defaultToMember = messages.BooleanField(8)
+ componentRequired = messages.BooleanField(9)
+
+
+class Role(messages.Enum):
+ """User role."""
+ owner = 1
+ member = 2
+ contributor = 3
+
+
+class IssueState(messages.Enum):
+ """Issue state."""
+ closed = 0
+ open = 1
+
+
+class CannedQuery(messages.Enum):
+ """Canned query to search issues."""
+ all = 0
+ new = 1
+ open = 2
+ owned = 3
+ reported = 4
+ starred = 5
+ to_verify = 6
+
+
+class AtomPerson(messages.Message):
+ """Atomic person."""
+ name = messages.StringField(1, required=True)
+ htmlLink = messages.StringField(2)
+ kind = messages.StringField(3)
+ last_visit_days_ago = messages.IntegerField(4)
+ email_bouncing = messages.BooleanField(5)
+ vacation_message = messages.StringField(6)
+
+
+class Attachment(messages.Message):
+ """Issue attachment."""
+ attachmentId = messages.IntegerField(
+ 1, variant=messages.Variant.INT64, required=True)
+ fileName = messages.StringField(2, required=True)
+ fileSize = messages.IntegerField(
+ 3, required=True, variant=messages.Variant.INT32)
+ mimetype = messages.StringField(4, required=True)
+ isDeleted = messages.BooleanField(5)
+
+
+class IssueRef(messages.Message):
+ "Issue reference."
+ issueId = messages.IntegerField(
+ 1, required=True, variant=messages.Variant.INT32)
+ projectId = messages.StringField(2)
+ kind = messages.StringField(3)
+
+
+class FieldValueOperator(messages.Enum):
+ """Operator of field values."""
+ add = 1
+ remove = 2
+ clear = 3
+
+
+class FieldValue(messages.Message):
+ """Custom field values."""
+ fieldName = messages.StringField(1, required=True)
+ fieldValue = messages.StringField(2)
+ derived = messages.BooleanField(3, default=False)
+ operator = messages.EnumField(FieldValueOperator, 4, default='add')
+ phaseName = messages.StringField(5)
+ approvalName = messages.StringField(6)
+
+
+class Update(messages.Message):
+ """Issue update."""
+ summary = messages.StringField(1)
+ status = messages.StringField(2)
+ owner = messages.StringField(3)
+ labels = messages.StringField(4, repeated=True)
+ cc = messages.StringField(5, repeated=True)
+ blockedOn = messages.StringField(6, repeated=True)
+ blocking = messages.StringField(7, repeated=True)
+ mergedInto = messages.StringField(8)
+ kind = messages.StringField(9)
+ components = messages.StringField(10, repeated=True)
+ moveToProject = messages.StringField(11)
+ fieldValues = messages.MessageField(FieldValue, 12, repeated=True)
+ is_description = messages.BooleanField(13)
+
+
+class ApprovalUpdate(messages.Message):
+ """Approval update."""
+ approvers = messages.StringField(1, repeated=True)
+ status = messages.StringField(2)
+ kind = messages.StringField(3)
+ fieldValues = messages.MessageField(FieldValue, 4, repeated=True)
+
+
+class ProjectIssueConfig(messages.Message):
+ """Issue configuration of project."""
+ kind = messages.StringField(1)
+ restrictToKnown = messages.BooleanField(2)
+ defaultColumns = messages.StringField(3, repeated=True)
+ defaultSorting = messages.StringField(4, repeated=True)
+ statuses = messages.MessageField(Status, 5, repeated=True)
+ labels = messages.MessageField(Label, 6, repeated=True)
+ prompts = messages.MessageField(Prompt, 7, repeated=True)
+ defaultPromptForMembers = messages.IntegerField(
+ 8, variant=messages.Variant.INT32)
+ defaultPromptForNonMembers = messages.IntegerField(
+ 9, variant=messages.Variant.INT32)
+ usersCanSetLabels = messages.BooleanField(10)
+
+
+class Phase(messages.Message):
+ """Issue phase details."""
+ phaseName = messages.StringField(1)
+ rank = messages.IntegerField(2)
+
+
+class IssueCommentWrapper(messages.Message):
+ """Issue comment details."""
+ attachments = messages.MessageField(Attachment, 1, repeated=True)
+ author = messages.MessageField(AtomPerson, 2)
+ canDelete = messages.BooleanField(3)
+ content = messages.StringField(4)
+ deletedBy = messages.MessageField(AtomPerson, 5)
+ id = messages.IntegerField(6, variant=messages.Variant.INT32)
+ published = message_types.DateTimeField(7)
+ updates = messages.MessageField(Update, 8)
+ kind = messages.StringField(9)
+ is_description = messages.BooleanField(10)
+
+
+class ApprovalCommentWrapper(messages.Message):
+ """Approval comment details."""
+ attachments = messages.MessageField(Attachment, 1, repeated=True)
+ author = messages.MessageField(AtomPerson, 2)
+ canDelete = messages.BooleanField(3)
+ content = messages.StringField(4)
+ deletedBy = messages.MessageField(AtomPerson, 5)
+ id = messages.IntegerField(6, variant=messages.Variant.INT32)
+ published = message_types.DateTimeField(7)
+ approvalUpdates = messages.MessageField(ApprovalUpdate, 8)
+ kind = messages.StringField(9)
+ is_description = messages.BooleanField(10)
+
+
+class ApprovalStatus(messages.Enum):
+ """Allowed Approval Statuses."""
+ needsReview = 1
+ nA = 2
+ reviewRequested = 3
+ reviewStarted = 4
+ needInfo = 5
+ approved = 6
+ notApproved = 7
+ notSet = 8
+
+
+class Approval(messages.Message):
+ """Approval Value details"""
+ approvalName = messages.StringField(1)
+ approvers = messages.MessageField(AtomPerson, 2, repeated=True)
+ status = messages.EnumField(ApprovalStatus, 3)
+ setter = messages.MessageField(AtomPerson, 4)
+ setOn = message_types.DateTimeField(5)
+ phaseName = messages.StringField(6)
+
+
+class IssueWrapper(messages.Message):
+ """Issue details."""
+ author = messages.MessageField(AtomPerson, 1)
+ blockedOn = messages.MessageField(IssueRef, 2, repeated=True)
+ blocking = messages.MessageField(IssueRef, 3, repeated=True)
+ canComment = messages.BooleanField(4)
+ canEdit = messages.BooleanField(5)
+ cc = messages.MessageField(AtomPerson, 6, repeated=True)
+ closed = message_types.DateTimeField(7)
+ description = messages.StringField(8)
+ id = messages.IntegerField(9, variant=messages.Variant.INT32)
+ kind = messages.StringField(10)
+ labels = messages.StringField(11, repeated=True)
+ owner = messages.MessageField(AtomPerson, 12)
+ published = message_types.DateTimeField(13)
+ starred = messages.BooleanField(14)
+ stars = messages.IntegerField(15, variant=messages.Variant.INT32)
+ state = messages.EnumField(IssueState, 16)
+ status = messages.StringField(17, required=True)
+ summary = messages.StringField(18, required=True)
+ title = messages.StringField(19)
+ updated = message_types.DateTimeField(20)
+ components = messages.StringField(21, repeated=True)
+ projectId = messages.StringField(22, required=True)
+ mergedInto = messages.MessageField(IssueRef, 23)
+ fieldValues = messages.MessageField(FieldValue, 24, repeated=True)
+ owner_modified = message_types.DateTimeField(25)
+ status_modified = message_types.DateTimeField(26)
+ component_modified = message_types.DateTimeField(27)
+ approvalValues = messages.MessageField(Approval, 28, repeated=True)
+ phases = messages.MessageField(Phase, 29, repeated=True)
+ migrated_id = messages.StringField(30)
+
+class ProjectWrapper(messages.Message):
+ """Project details."""
+ kind = messages.StringField(1)
+ name = messages.StringField(2)
+ externalId = messages.StringField(3, required=True)
+ htmlLink = messages.StringField(4, required=True)
+ summary = messages.StringField(5)
+ description = messages.StringField(6)
+ versionControlSystem = messages.StringField(7)
+ repositoryUrls = messages.StringField(8, repeated=True)
+ issuesConfig = messages.MessageField(ProjectIssueConfig, 9)
+ role = messages.EnumField(Role, 10)
+ members = messages.MessageField(AtomPerson, 11, repeated=True)
+
+
+class UserGroupSettingsWrapper(messages.Message):
+ """User group settings."""
+ groupName = messages.StringField(1, required=True)
+ who_can_view_members = messages.EnumField(
+ usergroup_pb2.MemberVisibility, 2,
+ default=usergroup_pb2.MemberVisibility.MEMBERS)
+ ext_group_type = messages.EnumField(usergroup_pb2.GroupType, 3)
+ last_sync_time = messages.IntegerField(
+ 4, default=0, variant=messages.Variant.INT32)
+
+
+class GroupCitizens(messages.Message):
+ """Group members and owners."""
+ groupOwners = messages.StringField(1, repeated=True)
+ groupMembers = messages.StringField(2, repeated=True)
+
+
+########################## Comments Message ##########################
+
+# pylint: disable=pointless-string-statement
+
+"""Request to delete/undelete an issue's comments."""
+ISSUES_COMMENTS_DELETE_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+ message_types.VoidMessage,
+ projectId=messages.StringField(1, required=True),
+ issueId=messages.IntegerField(
+ 2, required=True, variant=messages.Variant.INT32),
+ commentId=messages.IntegerField(
+ 3, required=True, variant=messages.Variant.INT32)
+)
+
+
+class IssuesCommentsDeleteResponse(messages.Message):
+ """Response message of request to delete/undelete an issue's comments."""
+ error = messages.MessageField(ErrorMessage, 1)
+
+
+"""Request to insert an issue's comments."""
+ISSUES_COMMENTS_INSERT_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+ IssueCommentWrapper,
+ projectId=messages.StringField(1, required=True),
+ issueId=messages.IntegerField(
+ 2, required=True, variant=messages.Variant.INT32),
+ sendEmail=messages.BooleanField(3)
+)
+
+
+class IssuesCommentsInsertResponse(messages.Message):
+ """Response message of request to insert an issue's comments."""
+ error = messages.MessageField(ErrorMessage, 1)
+ id = messages.IntegerField(2, variant=messages.Variant.INT32)
+ kind = messages.StringField(3)
+ author = messages.MessageField(AtomPerson, 4)
+ content = messages.StringField(5)
+ published = message_types.DateTimeField(6)
+ updates = messages.MessageField(Update, 7)
+ canDelete = messages.BooleanField(8)
+
+
+"""Request to list an issue's comments."""
+ISSUES_COMMENTS_LIST_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+ message_types.VoidMessage,
+ projectId=messages.StringField(1, required=True),
+ issueId=messages.IntegerField(
+ 2, required=True, variant=messages.Variant.INT32),
+ maxResults=messages.IntegerField(
+ 3, default=100, variant=messages.Variant.INT32),
+ startIndex=messages.IntegerField(
+ 4, default=0, variant=messages.Variant.INT32)
+)
+
+
+class IssuesCommentsListResponse(messages.Message):
+ """Response message of request to list an issue's comments."""
+ error = messages.MessageField(ErrorMessage, 1)
+ items = messages.MessageField(IssueCommentWrapper, 2, repeated=True)
+ totalResults = messages.IntegerField(3, variant=messages.Variant.INT32)
+ kind = messages.StringField(4)
+
+########################## ApprovalComments Message ################
+
+"""Request to insert an issue approval's comments."""
+APPROVALS_COMMENTS_INSERT_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+ ApprovalCommentWrapper,
+ projectId=messages.StringField(1, required=True),
+ issueId=messages.IntegerField(
+ 2, required=True, variant=messages.Variant.INT32),
+ approvalName=messages.StringField(3, required=True),
+ sendEmail=messages.BooleanField(4)
+)
+
+
+class ApprovalsCommentsInsertResponse(messages.Message):
+ """Response message of request to insert an isuse's comments."""
+ error = messages.MessageField(ErrorMessage, 1)
+ id = messages.IntegerField(2, variant=messages.Variant.INT32)
+ kind = messages.StringField(3)
+ author = messages.MessageField(AtomPerson, 4)
+ content = messages.StringField(5)
+ published = message_types.DateTimeField(6)
+ approvalUpdates = messages.MessageField(ApprovalUpdate, 7)
+ canDelete = messages.BooleanField(8)
+ approvalName = messages.StringField(9)
+
+
+"""Requests to list an approval's comments."""
+APPROVALS_COMMENTS_LIST_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+ message_types.VoidMessage,
+ projectId=messages.StringField(1, required=True),
+ issueId=messages.IntegerField(
+ 2, required=True, variant=messages.Variant.INT32),
+ approvalName=messages.StringField(3, required=True),
+ maxResults=messages.IntegerField(
+ 4, default=100, variant=messages.Variant.INT32),
+ startIndex=messages.IntegerField(
+ 5, default=0, variant=messages.Variant.INT32)
+)
+
+
+class ApprovalsCommentsListResponse(messages.Message):
+ """Response message of request to list an approval's comments."""
+ error = messages.MessageField(ErrorMessage, 1)
+ items = messages.MessageField(ApprovalCommentWrapper, 2, repeated=True)
+ totalResults = messages.IntegerField(3, variant=messages.Variant.INT32)
+ kind = messages.StringField(4)
+
+########################## Users Message ##########################
+
+"""Request to get a user."""
+USERS_GET_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+ message_types.VoidMessage,
+ userId=messages.StringField(1, required=True),
+ ownerProjectsOnly=messages.BooleanField(2, default=False)
+)
+
+
+class UsersGetResponse(messages.Message):
+ """Response message of request to get a user."""
+ error = messages.MessageField(ErrorMessage, 1)
+ id = messages.StringField(2)
+ kind = messages.StringField(3)
+ projects = messages.MessageField(ProjectWrapper, 4, repeated=True)
+
+
+########################## Issues Message ##########################
+
+"""Request to get an issue."""
+ISSUES_GET_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+ message_types.VoidMessage,
+ projectId=messages.StringField(1, required=True),
+ issueId=messages.IntegerField(
+ 2, required=True, variant=messages.Variant.INT32)
+)
+
+
+"""Request to insert an issue."""
+ISSUES_INSERT_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+ IssueWrapper,
+ projectId=messages.StringField(1, required=True),
+ sendEmail=messages.BooleanField(2, default=True)
+)
+
+
+class IssuesGetInsertResponse(messages.Message):
+ """Response message of request to get/insert an issue."""
+ error = messages.MessageField(ErrorMessage, 1)
+ kind = messages.StringField(2)
+ id = messages.IntegerField(3, variant=messages.Variant.INT32)
+ title = messages.StringField(4)
+ summary = messages.StringField(5)
+ stars = messages.IntegerField(6, variant=messages.Variant.INT32)
+ starred = messages.BooleanField(7)
+ status = messages.StringField(8)
+ state = messages.EnumField(IssueState, 9)
+ labels = messages.StringField(10, repeated=True)
+ author = messages.MessageField(AtomPerson, 11)
+ owner = messages.MessageField(AtomPerson, 12)
+ cc = messages.MessageField(AtomPerson, 13, repeated=True)
+ updated = message_types.DateTimeField(14)
+ published = message_types.DateTimeField(15)
+ closed = message_types.DateTimeField(16)
+ blockedOn = messages.MessageField(IssueRef, 17, repeated=True)
+ blocking = messages.MessageField(IssueRef, 18, repeated=True)
+ projectId = messages.StringField(19)
+ canComment = messages.BooleanField(20)
+ canEdit = messages.BooleanField(21)
+ components = messages.StringField(22, repeated=True)
+ mergedInto = messages.MessageField(IssueRef, 23)
+ fieldValues = messages.MessageField(FieldValue, 24, repeated=True)
+ owner_modified = message_types.DateTimeField(25)
+ status_modified = message_types.DateTimeField(26)
+ component_modified = message_types.DateTimeField(27)
+ approvalValues = messages.MessageField(Approval, 28, repeated=True)
+ phases = messages.MessageField(Phase, 29, repeated=True)
+ migrated_id = messages.StringField(30)
+
+
+"""Request to list issues."""
+ISSUES_LIST_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+ message_types.VoidMessage,
+ projectId=messages.StringField(1, required=True),
+ additionalProject=messages.StringField(2, repeated=True),
+ can=messages.EnumField(CannedQuery, 3, default='all'),
+ label=messages.StringField(4),
+ maxResults=messages.IntegerField(
+ 5, default=100, variant=messages.Variant.INT32),
+ owner=messages.StringField(6),
+ publishedMax=messages.IntegerField(7, variant=messages.Variant.INT64),
+ publishedMin=messages.IntegerField(8, variant=messages.Variant.INT64),
+ q=messages.StringField(9),
+ sort=messages.StringField(10),
+ startIndex=messages.IntegerField(
+ 11, default=0, variant=messages.Variant.INT32),
+ status=messages.StringField(12),
+ updatedMax=messages.IntegerField(13, variant=messages.Variant.INT64),
+ updatedMin=messages.IntegerField(14, variant=messages.Variant.INT64)
+)
+
+
+class IssuesListResponse(messages.Message):
+ """Response message of request to list issues."""
+ error = messages.MessageField(ErrorMessage, 1)
+ items = messages.MessageField(IssueWrapper, 2, repeated=True)
+ totalResults = messages.IntegerField(3, variant=messages.Variant.INT32)
+ kind = messages.StringField(4)
+
+
+"""Request to list group settings."""
+GROUPS_SETTINGS_LIST_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+ message_types.VoidMessage,
+ importedGroupsOnly=messages.BooleanField(1, default=False)
+)
+
+
+class GroupsSettingsListResponse(messages.Message):
+ """Response message of request to list group settings."""
+ error = messages.MessageField(ErrorMessage, 1)
+ groupSettings = messages.MessageField(
+ UserGroupSettingsWrapper, 2, repeated=True)
+
+
+"""Request to create a group."""
+GROUPS_CREATE_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+ message_types.VoidMessage,
+ groupName = messages.StringField(1, required=True),
+ who_can_view_members = messages.EnumField(
+ usergroup_pb2.MemberVisibility, 2,
+ default=usergroup_pb2.MemberVisibility.MEMBERS, required=True),
+ ext_group_type = messages.EnumField(usergroup_pb2.GroupType, 3)
+)
+
+
+class GroupsCreateResponse(messages.Message):
+ """Response message of request to create a group."""
+ error = messages.MessageField(ErrorMessage, 1)
+ groupID = messages.IntegerField(
+ 2, variant=messages.Variant.INT32)
+
+
+"""Request to get a group."""
+GROUPS_GET_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+ message_types.VoidMessage,
+ groupName = messages.StringField(1, required=True)
+)
+
+
+class GroupsGetResponse(messages.Message):
+ """Response message of request to create a group."""
+ error = messages.MessageField(ErrorMessage, 1)
+ groupID = messages.IntegerField(
+ 2, variant=messages.Variant.INT32)
+ groupSettings = messages.MessageField(
+ UserGroupSettingsWrapper, 3)
+ groupOwners = messages.StringField(4, repeated=True)
+ groupMembers = messages.StringField(5, repeated=True)
+
+
+"""Request to update a group."""
+GROUPS_UPDATE_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+ GroupCitizens,
+ groupName = messages.StringField(1, required=True),
+ who_can_view_members = messages.EnumField(
+ usergroup_pb2.MemberVisibility, 2),
+ ext_group_type = messages.EnumField(usergroup_pb2.GroupType, 3),
+ last_sync_time = messages.IntegerField(
+ 4, default=0, variant=messages.Variant.INT32),
+ friend_projects = messages.StringField(5, repeated=True),
+)
+
+
+class GroupsUpdateResponse(messages.Message):
+ """Response message of request to update a group."""
+ error = messages.MessageField(ErrorMessage, 1)
+
+
+########################## Component Message ##########################
+
+class Component(messages.Message):
+ """Component PB."""
+ componentId = messages.IntegerField(
+ 1, required=True, variant=messages.Variant.INT32)
+ projectName = messages.StringField(2, required=True)
+ componentPath = messages.StringField(3, required=True)
+ description = messages.StringField(4)
+ admin = messages.StringField(5, repeated=True)
+ cc = messages.StringField(6, repeated=True)
+ deprecated = messages.BooleanField(7, default=False)
+ created = message_types.DateTimeField(8)
+ creator = messages.StringField(9)
+ modified = message_types.DateTimeField(10)
+ modifier = messages.StringField(11)
+
+
+"""Request to get components of a project."""
+COMPONENTS_LIST_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+ message_types.VoidMessage,
+ projectId=messages.StringField(1, required=True),
+)
+
+
+class ComponentsListResponse(messages.Message):
+ """Response to list components."""
+ components = messages.MessageField(
+ Component, 1, repeated=True)
+
+
+class ComponentCreateRequestBody(messages.Message):
+ """Request body to create a component."""
+ parentPath = messages.StringField(1)
+ description = messages.StringField(2)
+ admin = messages.StringField(3, repeated=True)
+ cc = messages.StringField(4, repeated=True)
+ deprecated = messages.BooleanField(5, default=False)
+
+
+"""Request to create component of a project."""
+COMPONENTS_CREATE_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+ ComponentCreateRequestBody,
+ projectId=messages.StringField(1, required=True),
+ componentName=messages.StringField(2, required=True),
+)
+
+
+"""Request to delete a component."""
+COMPONENTS_DELETE_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+ message_types.VoidMessage,
+ projectId=messages.StringField(1, required=True),
+ componentPath=messages.StringField(2, required=True),
+)
+
+
+class ComponentUpdateFieldID(messages.Enum):
+ """Possible fields that can be updated in a component."""
+ LEAF_NAME = 1
+ DESCRIPTION = 2
+ ADMIN = 3
+ CC = 4
+ DEPRECATED = 5
+
+
+class ComponentUpdate(messages.Message):
+ """Component update."""
+ # 'field' allows a field to be cleared
+ field = messages.EnumField(ComponentUpdateFieldID, 1, required=True)
+ leafName = messages.StringField(2)
+ description = messages.StringField(3)
+ admin = messages.StringField(4, repeated=True)
+ cc = messages.StringField(5, repeated=True)
+ deprecated = messages.BooleanField(6)
+
+
+class ComponentUpdateRequestBody(messages.Message):
+ """Request body to update a component."""
+ updates = messages.MessageField(ComponentUpdate, 1, repeated=True)
+
+
+"""Request to update a component."""
+COMPONENTS_UPDATE_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+ ComponentUpdateRequestBody,
+ projectId=messages.StringField(1, required=True),
+ componentPath=messages.StringField(2, required=True),
+)
diff --git a/mrproto/ast_pb2.py b/mrproto/ast_pb2.py
new file mode 100644
index 0000000..9ad9edf
--- /dev/null
+++ b/mrproto/ast_pb2.py
@@ -0,0 +1,110 @@
+# 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.
+
+"""Protocol buffers for user queries parsed into abstract syntax trees.
+
+A user issue query can look like [Type=Defect owner:jrobbins "memory leak"].
+In that simple form, all the individual search conditions are simply ANDed
+together. In the code, a list of conditions to be ANDed is called a
+conjunction.
+
+Monorail also supports a quick-or feature: [Type=Defect,Enhancement]. That
+will match any issue that has labels Type-Defect or Type-Enhancement, or both.
+
+Monorail supports a top-level "OR" keyword that can
+be used to logically OR a series of conjunctions. For example:
+[Type=Defect stars>10 OR Type=Enhancement stars>50].
+
+Parentheses groups and "OR" statements are preprocessed before the final
+QueryAST is constructed.
+
+So, QueryAST is always exactly two levels: the overall tree
+consists of a list of conjunctions, and each conjunction consists of a list
+of conditions.
+
+A condition can look like [stars>10] or [summary:memory] or
+[Type=Defect,Enhancement]. Each condition has a single comparison operator.
+Most conditions refer to a single field definition, but in the case of
+cross-project search a single condition can have a list of field definitions
+from the different projects being searched. Each condition can have a list
+of constant values to compare against. The values may be all strings or all
+integers.
+
+Some conditions are procesed by the SQL database and others by the GAE
+search API. All conditions are passed to each module and it is up to
+the module to decide which conditions to handle and which to ignore.
+"""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from protorpc import messages
+
+from mrproto import tracker_pb2
+
+
+# This is a special field_name for a FieldDef that means to do a fulltext
+# search for words that occur in any part of the issue.
+ANY_FIELD = 'any_field'
+
+
+class QueryOp(messages.Enum):
+ """Enumeration of possible query condition operators."""
+ EQ = 1
+ NE = 2
+ LT = 3
+ GT = 4
+ LE = 5
+ GE = 6
+ TEXT_HAS = 7
+ NOT_TEXT_HAS = 8
+ IS_DEFINED = 11
+ IS_NOT_DEFINED = 12
+ KEY_HAS = 13
+
+
+class TokenType(messages.Enum):
+ """Enumeration of query tokens used for parentheses parsing."""
+ SUBQUERY = 1
+ LEFT_PAREN = 2
+ RIGHT_PAREN = 3
+ OR = 4
+
+
+class QueryToken(messages.Message):
+ """Data structure to represent a single token for parentheses parsing."""
+ token_type = messages.EnumField(TokenType, 1, required=True)
+ value = messages.StringField(2)
+
+
+class Condition(messages.Message):
+ """Representation of one query condition. E.g., [Type=Defect,Task]."""
+ op = messages.EnumField(QueryOp, 1, required=True)
+ field_defs = messages.MessageField(tracker_pb2.FieldDef, 2, repeated=True)
+ str_values = messages.StringField(3, repeated=True)
+ int_values = messages.IntegerField(4, repeated=True)
+ # The suffix of a search field
+ # eg. the 'approver' in 'UXReview-approver:user@mail.com'
+ key_suffix = messages.StringField(5)
+ # The name of the phase this field value should belong to.
+ phase_name = messages.StringField(6)
+
+
+class Conjunction(messages.Message):
+ """A list of conditions that are implicitly ANDed together."""
+ conds = messages.MessageField(Condition, 1, repeated=True)
+
+
+class QueryAST(messages.Message):
+ """Abstract syntax tree for the user's query."""
+ conjunctions = messages.MessageField(Conjunction, 1, repeated=True)
+
+
+def MakeCond(op, field_defs, str_values, int_values,
+ key_suffix=None, phase_name=None):
+ """Shorthand function to construct a Condition PB."""
+ return Condition(
+ op=op, field_defs=field_defs, str_values=str_values,
+ int_values=int_values, key_suffix=key_suffix, phase_name=phase_name)
diff --git a/mrproto/features_pb2.py b/mrproto/features_pb2.py
new file mode 100644
index 0000000..cc0dc44
--- /dev/null
+++ b/mrproto/features_pb2.py
@@ -0,0 +1,85 @@
+# 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.
+
+"""Protocol buffers for Monorail features."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from features import features_constants
+from protorpc import messages
+
+
+class Hotlist(messages.Message):
+ """This protocol buffer holds all the metadata associated with a hotlist."""
+ # A numeric identifier for this hotlist.
+ hotlist_id = messages.IntegerField(1, required=True)
+
+ # The short identifier for this hotlist.
+ name = messages.StringField(2, required=True)
+
+ # A one-line summary (human-readable) of the hotlist.
+ summary = messages.StringField(3, default='')
+
+ # A detailed description of the hotlist.
+ description = messages.StringField(4, default='')
+
+ # Hotlists can be marked private to prevent unwanted users from seeing them.
+ is_private = messages.BooleanField(5, default=False)
+
+ # Note that these lists are disjoint (a user ID will not appear twice).
+ owner_ids = messages.IntegerField(6, repeated=True)
+ editor_ids = messages.IntegerField(8, repeated=True)
+ follower_ids = messages.IntegerField(9, repeated=True)
+
+
+ class HotlistItem(messages.Message):
+ """Nested message for a hotlist to issue relation."""
+ issue_id = messages.IntegerField(1, required=True)
+ rank = messages.IntegerField(2, required=True)
+ adder_id = messages.IntegerField(3)
+ date_added = messages.IntegerField(4)
+ note = messages.StringField(5, default='')
+
+ items = messages.MessageField(HotlistItem, 10, repeated=True)
+
+ # The default columns to show on hotlist issues page
+ default_col_spec = messages.StringField(
+ 11, default=features_constants.DEFAULT_COL_SPEC)
+
+def MakeHotlist(name, hotlist_item_fields=None, **kwargs):
+ """Returns a hotlist protocol buffer with the given attributes.
+ Args:
+ hotlist_item_fields: tuple of (iid, rank, user, date, note)
+ kwargs should only include the following:
+ hotlist_id, summary, description, is_private, owner_ids, editor_ids,
+ follower_ids, default_col_spec"""
+ hotlist = Hotlist(name=name, **kwargs)
+
+ if hotlist_item_fields is not None:
+ for iid, rank, user, date, note in hotlist_item_fields:
+ hotlist.items.append(Hotlist.HotlistItem(
+ issue_id=iid, rank=rank, adder_id=user, date_added=date, note=note))
+
+ return hotlist
+
+
+# For any issues that were added to hotlists before we started storing that
+# timestamp, just use the launch date of the feature as a default.
+ADDED_TS_FEATURE_LAUNCH_TS = 1484350000 # Jan 13, 2017
+
+
+def MakeHotlistItem(
+ issue_id, rank=None, adder_id=None, date_added=None, note=None):
+ item = Hotlist.HotlistItem(
+ issue_id=issue_id,
+ date_added=date_added or ADDED_TS_FEATURE_LAUNCH_TS)
+ if rank is not None:
+ item.rank = rank
+ if adder_id is not None:
+ item.adder_id = adder_id
+ if note is not None:
+ item.note = note
+ return item
diff --git a/mrproto/project_pb2.py b/mrproto/project_pb2.py
new file mode 100644
index 0000000..0edac8d
--- /dev/null
+++ b/mrproto/project_pb2.py
@@ -0,0 +1,238 @@
+# 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.
+
+"""Protocol buffers for Monorail projects."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from protorpc import messages
+
+# Project state affects permissions in that project, and project deletion.
+# It is edited on the project admin page. If it is anything other that LIVE
+# it triggers a notice at the top of every project page.
+# For more info, see the "Project deletion in Monorail" design doc.
+class ProjectState(messages.Enum):
+ """Enum for states in the project lifecycle."""
+ # Project is visible and indexed. This is the typical state.
+ #
+ # If moved_to is set, this project is live but has been moved
+ # to another location, so redirects will be used or links shown.
+ LIVE = 1
+
+ # Project owner has requested the project be archived. Project is
+ # read-only to members only, off-limits to non-members. Issues
+ # can be searched when in the project, but should not appear in
+ # site-wide searches. The project name is still in-use by this
+ # project.
+ #
+ # If a delete_time is set, then the project is doomed: (1) the
+ # state can only be changed by a site admin, and (2) the project
+ # will automatically transition to DELETABLE after that time is
+ # reached.
+ ARCHIVED = 2
+
+ # Project can be deleted at any time. The project name should
+ # have already been changed to a generated string, so it's
+ # impossible to navigate to this project, and the original name
+ # can be reused by a new project.
+ DELETABLE = 3
+
+
+# Project access affects permissions in that project.
+# It is edited on the project admin page.
+class ProjectAccess(messages.Enum):
+ """Enum for possible project access levels."""
+ # Anyone may view this project, even anonymous users.
+ ANYONE = 1
+
+ # Only project members may view the project.
+ MEMBERS_ONLY = 3
+
+
+# A Project PB represents a project in Monorail, which is a workspace for
+# project members to collaborate on issues.
+# A project is created on the project creation page, searched on the project
+# list page, and edited on the project admin page.
+# Next message: 74
+class Project(messages.Message):
+ """This protocol buffer holds all the metadata associated with a project."""
+ state = messages.EnumField(ProjectState, 1, required=True)
+ access = messages.EnumField(ProjectAccess, 18, default=ProjectAccess.ANYONE)
+
+ # The short identifier for this project. This value is lower-cased,
+ # and must be between 3 and 20 characters (inclusive). Alphanumeric
+ # and dashes are allowed, and it must start with an alpha character.
+ # Project names must be unique.
+ project_name = messages.StringField(2, required=True)
+
+ # A numeric identifier for this project.
+ project_id = messages.IntegerField(3, required=True)
+
+ # A one-line summary (human-readable) name of the project.
+ summary = messages.StringField(4, default='')
+
+ # A detailed description of the project.
+ description = messages.StringField(5, default='')
+
+ # Description of why this project has the state set as it is.
+ # This is used for administrative purposes to notify Owners that we
+ # are going to delete their project unless they can provide a good
+ # reason to not do so.
+ state_reason = messages.StringField(9)
+
+ # Time (in seconds) at which an ARCHIVED project may automatically
+ # be changed to state DELETABLE. The state change is done by a
+ # cron job.
+ delete_time = messages.IntegerField(10)
+
+ # Note that these lists are disjoint (a user ID will not appear twice).
+ owner_ids = messages.IntegerField(11, repeated=True)
+ committer_ids = messages.IntegerField(12, repeated=True)
+ contributor_ids = messages.IntegerField(15, repeated=True)
+
+ class ExtraPerms(messages.Message):
+ """Nested message for each member's extra permissions in a project."""
+ member_id = messages.IntegerField(1, required=True)
+ # Each custom perm is a single word [a-zA-Z0-9].
+ perms = messages.StringField(2, repeated=True)
+
+ extra_perms = messages.MessageField(ExtraPerms, 16, repeated=True)
+
+ # Project owners may choose to have ALL issue change notifications go to a
+ # mailing list (in addition to going directly to the users interested
+ # in that issue).
+ issue_notify_address = messages.StringField(14)
+
+ # These fields keep track of the cumulative size of all issue attachments
+ # in a given project. Normally, the number of bytes used is compared
+ # to a constant defined in the web application. However, if a custom
+ # quota is specified here, it will be used instead. An issue attachment
+ # will fail if its size would put the project over its quota. Not all
+ # projects have these fields: they are only set when the first attachment
+ # is uploaded.
+ attachment_bytes_used = messages.IntegerField(38, default=0)
+ # If quota is not set, default from tracker_constants.py is used.
+ attachment_quota = messages.IntegerField(39)
+
+ # NOTE: open slots 40, 41
+
+ # Recent_activity is a timestamp (in seconds since the Epoch) of the
+ # last time that an issue was entered, updated, or commented on.
+ recent_activity = messages.IntegerField(42, default=0)
+
+ # NOTE: open slots 43...
+
+ # Timestamp (in seconds since the Epoch) of the most recent change
+ # to this project that would invalidate cached content. It is set
+ # whenever project membership is edited, or any component config PB
+ # is edited. HTTP requests for auto-complete feeds include this
+ # value in the URL.
+ cached_content_timestamp = messages.IntegerField(53, default=0)
+
+ # If set, this project has been moved elsewhere. This can
+ # be an absolute URL, the name of another project on the same site.
+ moved_to = messages.StringField(60)
+
+ # Enable inbound email processing for issues.
+ process_inbound_email = messages.BooleanField(63, default=False)
+
+ # Limit removal of Restrict-* labels to project owners.
+ only_owners_remove_restrictions = messages.BooleanField(64, default=False)
+
+ # A per-project read-only lock. This lock (1) is meant to be
+ # long-lived (lasting as long as migration operations, project
+ # deletion, or anything else might take and (2) is meant to only
+ # limit user mutations; whether or not it limits automated actions
+ # that would change project data (such as workflow items) is
+ # determined based on the action.
+ #
+ # This lock is implemented as a user-visible string describing the
+ # reason for the project being in a read-only state. An absent or empty
+ # value indicates that the project is read-write; a present and
+ # non-empty value indicates that the project is read-only for the
+ # reason described.
+ read_only_reason = messages.StringField(65)
+
+ # This option is rarely used, but it makes sense for projects that aim for
+ # hub-and-spoke collaboration bewtween a vendor organization (like Google)
+ # and representatives of partner companies who are not supposed to know
+ # about each other.
+ # When true, it prevents project committers, contributors, and visitors
+ # from seeing the list of project members on the project summary page,
+ # on the People list page, and in autocomplete for issue owner and Cc.
+ # Project owners can always see the complete list of project members.
+ only_owners_see_contributors = messages.BooleanField(66, default=False)
+
+ # This configures the URLs generated when autolinking revision numbers.
+ # E.g., gitiles, viewvc, or crrev.com.
+ revision_url_format = messages.StringField(67)
+
+ # The home page of the Project.
+ home_page = messages.StringField(68)
+ # The url to redirect to for wiki/documentation links.
+ docs_url = messages.StringField(71)
+ # The url to redirect to for wiki/documentation links.
+ source_url = messages.StringField(72)
+ # The GCS object ID of the Project's logo.
+ logo_gcs_id = messages.StringField(69)
+ # The uploaded file name of the Project's logo.
+ logo_file_name = messages.StringField(70)
+
+ # Always send the full content of update in notifications.
+ issue_notify_always_detailed = messages.BooleanField(73, default=False)
+
+
+# This PB documents some of the duties of some of the members
+# in a given project. This info is displayed on the project People page.
+class ProjectCommitments(messages.Message):
+ project_id = messages.IntegerField(50)
+
+ class MemberCommitment(messages.Message):
+ member_id = messages.IntegerField(11, required=True)
+ notes = messages.StringField(13)
+
+ commitments = messages.MessageField(MemberCommitment, 2, repeated=True)
+
+
+def MakeProject(
+ project_name, project_id=None, state=ProjectState.LIVE,
+ access=ProjectAccess.ANYONE, summary=None, description=None,
+ moved_to=None, cached_content_timestamp=None,
+ owner_ids=None, committer_ids=None, contributor_ids=None,
+ read_only_reason=None, home_page=None, docs_url=None, source_url=None,
+ logo_gcs_id=None, logo_file_name=None):
+ """Returns a project protocol buffer with the given attributes."""
+ project = Project(
+ project_name=project_name, access=access, state=state)
+ if project_id:
+ project.project_id = project_id
+ if moved_to:
+ project.moved_to = moved_to
+ if cached_content_timestamp:
+ project.cached_content_timestamp = cached_content_timestamp
+ if summary:
+ project.summary = summary
+ if description:
+ project.description = description
+ if home_page:
+ project.home_page = home_page
+ if docs_url:
+ project.docs_url = docs_url
+ if source_url:
+ project.source_url = source_url
+ if logo_gcs_id:
+ project.logo_gcs_id = logo_gcs_id
+ if logo_file_name:
+ project.logo_file_name = logo_file_name
+
+ project.owner_ids.extend(owner_ids or [])
+ project.committer_ids.extend(committer_ids or [])
+ project.contributor_ids.extend(contributor_ids or [])
+
+ if read_only_reason is not None:
+ project.read_only_reason = read_only_reason
+
+ return project
diff --git a/mrproto/secrets.proto b/mrproto/secrets.proto
new file mode 100644
index 0000000..bab34b5
--- /dev/null
+++ b/mrproto/secrets.proto
@@ -0,0 +1,36 @@
+// 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.
+
+// This file defines protobufs needed for handling Monorail secrets.
+
+syntax = "proto3";
+
+package monorail.secrets;
+
+
+// Next available tag: 7
+message ListRequestContents {
+ // The parent resource of the requested resources.
+ string parent = 1;
+ // The requested page size for listing the resources.
+ int32 page_size = 2;
+ // The requested sort order of the list of resources.
+ string order_by = 3;
+ // The query that may be used to filter which resources to show.
+ string query = 4;
+ // The resource names of projects to query within.
+ repeated string projects = 5;
+ // The string that may be used to filter which resources to show.
+ // See AIP-160.
+ string filter = 6;
+}
+
+
+// Next available tag: 3
+message PageTokenContents {
+ // The index of where the requested resource list should start.
+ int32 start = 1;
+ // An encrypted ListRequestContents message.
+ bytes encrypted_list_request_contents = 2;
+}
diff --git a/mrproto/secrets_pb2.py b/mrproto/secrets_pb2.py
new file mode 100644
index 0000000..0b64ced
--- /dev/null
+++ b/mrproto/secrets_pb2.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+# 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.
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: mrproto/secrets.proto
+
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='mrproto/secrets.proto',
+ package='monorail.secrets',
+ syntax='proto3',
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ serialized_pb=b'\n\x13proto/secrets.proto\x12\x10monorail.secrets\"{\n\x13ListRequestContents\x12\x0e\n\x06parent\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x10\n\x08order_by\x18\x03 \x01(\t\x12\r\n\x05query\x18\x04 \x01(\t\x12\x10\n\x08projects\x18\x05 \x03(\t\x12\x0e\n\x06\x66ilter\x18\x06 \x01(\t\"K\n\x11PageTokenContents\x12\r\n\x05start\x18\x01 \x01(\x05\x12\'\n\x1f\x65ncrypted_list_request_contents\x18\x02 \x01(\x0c\x62\x06proto3'
+)
+
+
+
+
+_LISTREQUESTCONTENTS = _descriptor.Descriptor(
+ name='ListRequestContents',
+ full_name='monorail.secrets.ListRequestContents',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='parent', full_name='monorail.secrets.ListRequestContents.parent', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=b"".decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='page_size', full_name='monorail.secrets.ListRequestContents.page_size', index=1,
+ number=2, type=5, cpp_type=1, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='order_by', full_name='monorail.secrets.ListRequestContents.order_by', index=2,
+ number=3, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=b"".decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='query', full_name='monorail.secrets.ListRequestContents.query', index=3,
+ number=4, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=b"".decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='projects', full_name='monorail.secrets.ListRequestContents.projects', index=4,
+ number=5, type=9, cpp_type=9, label=3,
+ has_default_value=False, default_value=[],
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='filter', full_name='monorail.secrets.ListRequestContents.filter', index=5,
+ number=6, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=b"".decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=41,
+ serialized_end=164,
+)
+
+
+_PAGETOKENCONTENTS = _descriptor.Descriptor(
+ name='PageTokenContents',
+ full_name='monorail.secrets.PageTokenContents',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='start', full_name='monorail.secrets.PageTokenContents.start', index=0,
+ number=1, type=5, cpp_type=1, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='encrypted_list_request_contents', full_name='monorail.secrets.PageTokenContents.encrypted_list_request_contents', index=1,
+ number=2, type=12, cpp_type=9, label=1,
+ has_default_value=False, default_value=b"",
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=166,
+ serialized_end=241,
+)
+
+DESCRIPTOR.message_types_by_name['ListRequestContents'] = _LISTREQUESTCONTENTS
+DESCRIPTOR.message_types_by_name['PageTokenContents'] = _PAGETOKENCONTENTS
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+ListRequestContents = _reflection.GeneratedProtocolMessageType('ListRequestContents', (_message.Message,), {
+ 'DESCRIPTOR' : _LISTREQUESTCONTENTS,
+ '__module__' : 'mrproto.secrets_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.secrets.ListRequestContents)
+ })
+_sym_db.RegisterMessage(ListRequestContents)
+
+PageTokenContents = _reflection.GeneratedProtocolMessageType('PageTokenContents', (_message.Message,), {
+ 'DESCRIPTOR' : _PAGETOKENCONTENTS,
+ '__module__' : 'mrproto.secrets_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.secrets.PageTokenContents)
+ })
+_sym_db.RegisterMessage(PageTokenContents)
+
+
+# @@protoc_insertion_point(module_scope)
diff --git a/mrproto/site_pb2.py b/mrproto/site_pb2.py
new file mode 100644
index 0000000..820417e
--- /dev/null
+++ b/mrproto/site_pb2.py
@@ -0,0 +1,25 @@
+# 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.
+
+"""Protocol buffers for Monorail site-wide features."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from protorpc import messages
+
+
+class UserTypeRestriction(messages.Enum):
+ """An enum for site-wide settings about who can take an action."""
+ # Anyone may do it.
+ ANYONE = 1
+
+ # Only domain admins may do it.
+ ADMIN_ONLY = 2
+
+ # No one may do it, the feature is basically disabled.
+ NO_ONE = 3
+
+ # TODO(jrobbins): implement same-domain users
diff --git a/mrproto/test/__init__.py b/mrproto/test/__init__.py
new file mode 100644
index 0000000..68130d5
--- /dev/null
+++ b/mrproto/test/__init__.py
@@ -0,0 +1,3 @@
+# 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.
diff --git a/mrproto/test/ast_pb2_test.py b/mrproto/test/ast_pb2_test.py
new file mode 100644
index 0000000..91bb8ec
--- /dev/null
+++ b/mrproto/test/ast_pb2_test.py
@@ -0,0 +1,27 @@
+# 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.
+
+"""Tests for ast_pb2 functions."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from mrproto import ast_pb2
+from mrproto import tracker_pb2
+
+
+class ASTPb2Test(unittest.TestCase):
+
+ def testCond(self):
+ fd = tracker_pb2.FieldDef(field_id=1, field_name='Size')
+ cond = ast_pb2.MakeCond(
+ ast_pb2.QueryOp.EQ, [fd], ['XL'], [], key_suffix='-approver')
+ self.assertEqual(ast_pb2.QueryOp.EQ, cond.op)
+ self.assertEqual([fd], cond.field_defs)
+ self.assertEqual(['XL'], cond.str_values)
+ self.assertEqual([], cond.int_values)
+ self.assertEqual(cond.key_suffix, '-approver')
diff --git a/mrproto/test/features_pb2_test.py b/mrproto/test/features_pb2_test.py
new file mode 100644
index 0000000..fd145dc
--- /dev/null
+++ b/mrproto/test/features_pb2_test.py
@@ -0,0 +1,55 @@
+# 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.
+
+"""Tests for features_pb2 functions."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from mrproto import features_pb2
+
+
+class FeaturesPb2Test(unittest.TestCase):
+
+ def testMakeHotlist_Defaults(self):
+ hotlist = features_pb2.MakeHotlist('summer-issues')
+ self.assertEqual('summer-issues', hotlist.name)
+ self.assertEqual([], hotlist.items)
+
+ def testMakeHotlist_Everything(self):
+ ts = 20011111111111
+ hotlist = features_pb2.MakeHotlist(
+ 'summer-issues', [(1000, 1, 444, ts, ''), (1001, 2, 333, ts, ''),
+ (1009, None, None, ts, '')],
+ description='desc')
+ self.assertEqual('summer-issues', hotlist.name)
+ self.assertEqual(
+ [features_pb2.MakeHotlistItem(
+ 1000, rank=1, adder_id=444, date_added=ts, note=''),
+ features_pb2.MakeHotlistItem(
+ 1001, rank=2, adder_id=333, date_added=ts, note=''),
+ features_pb2.MakeHotlistItem(1009, date_added=ts, note=''),
+ ],
+ hotlist.items)
+ self.assertEqual('desc', hotlist.description)
+
+ def testMakeHotlistItem(self):
+ ts = 20011111111111
+ item_1 = features_pb2.MakeHotlistItem(
+ 1000, rank=1, adder_id=111, date_added=ts, note='short note')
+ self.assertEqual(1000, item_1.issue_id)
+ self.assertEqual(1, item_1.rank)
+ self.assertEqual(111, item_1.adder_id)
+ self.assertEqual(ts, item_1.date_added)
+ self.assertEqual('short note', item_1.note)
+
+ item_2 = features_pb2.MakeHotlistItem(1001)
+ self.assertEqual(1001, item_2.issue_id)
+ self.assertEqual(None, item_2.rank)
+ self.assertEqual(None, item_2.adder_id)
+ self.assertEqual('', item_2.note)
+ self.assertEqual(features_pb2.ADDED_TS_FEATURE_LAUNCH_TS, item_2.date_added)
diff --git a/mrproto/test/project_pb2_test.py b/mrproto/test/project_pb2_test.py
new file mode 100644
index 0000000..c9f8e61
--- /dev/null
+++ b/mrproto/test/project_pb2_test.py
@@ -0,0 +1,52 @@
+# 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.
+
+"""Tests for project_pb2 functions."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from mrproto import project_pb2
+
+
+class ProjectPb2Test(unittest.TestCase):
+
+ def testMakeProject_Defaults(self):
+ project = project_pb2.MakeProject('proj')
+ self.assertEqual('proj', project.project_name)
+ self.assertEqual(project_pb2.ProjectState.LIVE, project.state)
+ self.assertEqual(project_pb2.ProjectAccess.ANYONE, project.access)
+ self.assertFalse(project.read_only_reason)
+
+ def testMakeProject_Everything(self):
+ project = project_pb2.MakeProject(
+ 'proj', project_id=789, state=project_pb2.ProjectState.ARCHIVED,
+ access=project_pb2.ProjectAccess.MEMBERS_ONLY, summary='sum',
+ description='desc', moved_to='example.com',
+ cached_content_timestamp=1234567890, owner_ids=[111],
+ committer_ids=[222], contributor_ids=[333],
+ read_only_reason='being migrated',
+ home_page='example.com', docs_url='example.com/docs',
+ source_url='example.com/src', logo_gcs_id='logo_id',
+ logo_file_name='logo.gif')
+ self.assertEqual('proj', project.project_name)
+ self.assertEqual(789, project.project_id)
+ self.assertEqual(project_pb2.ProjectState.ARCHIVED, project.state)
+ self.assertEqual(project_pb2.ProjectAccess.MEMBERS_ONLY, project.access)
+ self.assertEqual('sum', project.summary)
+ self.assertEqual('desc', project.description)
+ self.assertEqual('example.com', project.moved_to)
+ self.assertEqual(1234567890, project.cached_content_timestamp)
+ self.assertEqual([111], project.owner_ids)
+ self.assertEqual([222], project.committer_ids)
+ self.assertEqual([333], project.contributor_ids)
+ self.assertEqual('being migrated', project.read_only_reason)
+ self.assertEqual('example.com', project.home_page)
+ self.assertEqual('example.com/docs', project.docs_url)
+ self.assertEqual('example.com/src', project.source_url)
+ self.assertEqual('logo_id', project.logo_gcs_id)
+ self.assertEqual('logo.gif', project.logo_file_name)
diff --git a/mrproto/test/user_pb2_test.py b/mrproto/test/user_pb2_test.py
new file mode 100644
index 0000000..fcfed71
--- /dev/null
+++ b/mrproto/test/user_pb2_test.py
@@ -0,0 +1,28 @@
+# 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.
+
+"""Tests for user_pb2 functions."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from mrproto import user_pb2
+
+
+class UserPb2Test(unittest.TestCase):
+
+ def testUser_Defaults(self):
+ user = user_pb2.MakeUser(111)
+ self.assertEqual(111, user.user_id)
+ self.assertFalse(user.obscure_email)
+ self.assertIsNone(user.email)
+
+ def testUser_Everything(self):
+ user = user_pb2.MakeUser(111, email='user@example.com', obscure_email=True)
+ self.assertEqual(111, user.user_id)
+ self.assertTrue(user.obscure_email)
+ self.assertEqual('user@example.com', user.email)
diff --git a/mrproto/test/usergroup_pb2_test.py b/mrproto/test/usergroup_pb2_test.py
new file mode 100644
index 0000000..26f7166
--- /dev/null
+++ b/mrproto/test/usergroup_pb2_test.py
@@ -0,0 +1,36 @@
+# 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.
+
+"""Tests for usergroup_pb2 functions."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from mrproto import usergroup_pb2
+
+
+class UserGroupPb2Test(unittest.TestCase):
+
+ def testMakeSettings_Defaults(self):
+ usergroup = usergroup_pb2.MakeSettings('anyone')
+ self.assertEqual(
+ usergroup_pb2.MemberVisibility.ANYONE,
+ usergroup.who_can_view_members)
+ self.assertIsNone(usergroup.ext_group_type)
+ self.assertEqual(0, usergroup.last_sync_time)
+ self.assertEqual([], usergroup.friend_projects)
+
+ def testMakeSettings_Everything(self):
+ usergroup = usergroup_pb2.MakeSettings(
+ 'Members', ext_group_type_str='mdb',
+ last_sync_time=1234567890, friend_projects=[789])
+ self.assertEqual(
+ usergroup_pb2.MemberVisibility.MEMBERS,
+ usergroup.who_can_view_members)
+ self.assertEqual(usergroup_pb2.GroupType.MDB, usergroup.ext_group_type)
+ self.assertEqual(1234567890, usergroup.last_sync_time)
+ self.assertEqual([789], usergroup.friend_projects)
diff --git a/mrproto/tracker_pb2.py b/mrproto/tracker_pb2.py
new file mode 100644
index 0000000..f5820e6
--- /dev/null
+++ b/mrproto/tracker_pb2.py
@@ -0,0 +1,553 @@
+# 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)
diff --git a/mrproto/user_pb2.py b/mrproto/user_pb2.py
new file mode 100644
index 0000000..de1aa6e
--- /dev/null
+++ b/mrproto/user_pb2.py
@@ -0,0 +1,96 @@
+# 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.
+
+"""Protocol buffers for Monorail users."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from protorpc import messages
+
+
+class IssueUpdateNav(messages.Enum):
+ """Pref for where a project member goes after an issue update."""
+ UP_TO_LIST = 0 # Back to issue list or grid view.
+ STAY_SAME_ISSUE = 1 # Show the same issue with the update.
+ NEXT_IN_LIST = 2 # Triage mode: go to next issue, if any.
+
+
+class User(messages.Message):
+ """In-memory busines object for representing users."""
+ user_id = messages.IntegerField(1) # TODO(jrobbins): make it required.
+
+ # Is this user a site administer?
+ is_site_admin = messages.BooleanField(4, required=True, default=False)
+
+ # User notification preferences. These preferences describe when
+ # a user is sent a email notification after an issue has changed.
+ # The user is notified if either of the following is true:
+ # 1. notify_issue_change is True and the user is named in the
+ # issue's Owner or CC field.
+ # 2. notify_starred_issue_change is True and the user has starred
+ # the issue.
+ notify_issue_change = messages.BooleanField(5, default=True)
+ notify_starred_issue_change = messages.BooleanField(6, default=True)
+ # Opt-in to email subject lines like "proj:123: issue summary".
+ email_compact_subject = messages.BooleanField(14, default=False)
+ # Opt-out of "View Issue" button in Gmail inbox.
+ email_view_widget = messages.BooleanField(15, default=True)
+ # Opt-in to ping emails from issues that the user starred.
+ notify_starred_ping = messages.BooleanField(16, default=False)
+
+ # This user has been banned, and this string describes why. All access
+ # to Monorail pages should be disabled.
+ banned = messages.StringField(7, default='')
+
+ # Fields 8-13 are no longer used: they were User action counts and limits.
+
+ after_issue_update = messages.EnumField(
+ IssueUpdateNav, 29, default=IssueUpdateNav.STAY_SAME_ISSUE)
+
+ # Should we obfuscate the user's email address and require solving a captcha
+ # to reveal it entirely? The default value corresponds to requiring users to
+ # opt into publishing their identities, but our code ensures that the
+ # opposite takes place for Gmail accounts.
+ obscure_email = messages.BooleanField(26, default=True)
+
+ # The email address chosen by the user to reveal on the site.
+ email = messages.StringField(27)
+
+ # Sticky state for show/hide widget on people details page.
+ keep_people_perms_open = messages.BooleanField(33, default=False)
+
+ deleted = messages.BooleanField(39, default=False)
+ deleted_timestamp = messages.IntegerField(40, default=0)
+
+ preview_on_hover = messages.BooleanField(42, default=True)
+
+ last_visit_timestamp = messages.IntegerField(45, default=0)
+ email_bounce_timestamp = messages.IntegerField(46, default=0)
+ vacation_message = messages.StringField(47)
+
+ linked_parent_id = messages.IntegerField(48)
+ linked_child_ids = messages.IntegerField(49, repeated=True)
+
+
+class UserPrefValue(messages.Message):
+ """Holds a single non-default user pref."""
+ name = messages.StringField(1, required=True)
+ value = messages.StringField(2)
+
+
+class UserPrefs(messages.Message):
+ """In-memory business object for representing user preferences."""
+ user_id = messages.IntegerField(1, required=True)
+ prefs = messages.MessageField(UserPrefValue, 2, repeated=True)
+
+
+
+def MakeUser(user_id, email=None, obscure_email=False):
+ """Create and return a new user record in RAM."""
+ user = User(user_id=user_id, obscure_email=bool(obscure_email))
+ if email:
+ user.email = email
+ return user
diff --git a/mrproto/usergroup_pb2.py b/mrproto/usergroup_pb2.py
new file mode 100644
index 0000000..5b37640
--- /dev/null
+++ b/mrproto/usergroup_pb2.py
@@ -0,0 +1,54 @@
+# 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.
+
+"""Protocol buffers for Monorail usergroups."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from protorpc import messages
+
+
+class MemberVisibility(messages.Enum):
+ """Enum controlling who can see the members of a user group."""
+ OWNERS = 0
+ MEMBERS = 1
+ ANYONE = 2
+
+
+class GroupType(messages.Enum):
+ """Type of external group to import."""
+ CHROME_INFRA_AUTH = 0
+ MDB = 1
+ BAGGINS = 3
+ COMPUTED = 4
+
+
+class UserGroupSettings(messages.Message):
+ """In-memory busines object for representing user group settings."""
+ who_can_view_members = messages.EnumField(
+ MemberVisibility, 1, default=MemberVisibility.MEMBERS)
+ ext_group_type = messages.EnumField(GroupType, 2)
+ last_sync_time = messages.IntegerField(
+ 3, default=0, variant=messages.Variant.INT32)
+ friend_projects = messages.IntegerField(
+ 4, repeated=True, variant=messages.Variant.INT32)
+ notify_members = messages.BooleanField(5, default=True)
+ notify_group = messages.BooleanField(6, default=False)
+# TODO(jrobbins): add settings to control who can join, etc.
+
+
+def MakeSettings(who_can_view_members_str, ext_group_type_str=None,
+ last_sync_time=0, friend_projects=None, notify_members=True,
+ notify_group=False):
+ """Create and return a new user record in RAM."""
+ settings = UserGroupSettings(
+ who_can_view_members=MemberVisibility(who_can_view_members_str.upper()),
+ notify_members=notify_members, notify_group=notify_group)
+ if ext_group_type_str:
+ settings.ext_group_type = GroupType(ext_group_type_str.upper())
+ settings.last_sync_time = last_sync_time
+ settings.friend_projects = friend_projects or []
+ return settings