Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/api/README.md b/api/README.md
new file mode 100644
index 0000000..dfb4cca
--- /dev/null
+++ b/api/README.md
@@ -0,0 +1,51 @@
+# Monorail pRPC API
+
+This directory holds all the source for the Monorail pRPC API. This API is
+implemented using `.proto` files to describe a `gRPC` interface (services,
+methods, and request/response messages). It then uses a shim which
+converts the
+[`gRPC` server](http://www.grpc.io/docs/tutorials/basic/python.html)
+(which doesn't work on AppEngine, due to lack of support for HTTP/2) into a
+[`pRPC` server](https://godoc.org/github.com/luci/luci-go/grpc/prpc) which
+supports communication over HTTP/1.1, as well as text and JSON IO.
+
+## Getting Started
+
+In order to make API requests, your client needs to either:
+
+- Present an OAuth token generated by an allowed client ID or email.
+- Provide a XSRF token.
+- [For local dev only] Send a test account header, as used by `test_call`
+ described below.
+
+## Making requests
+
+You can make anonymous requests to a server running locally like this:
+
+```bash
+$ ./api/test_call monorail.Users GetUser '{"email": "test@example.com"}'
+```
+
+Requests that require a signed-in user can be tested locally like this:
+
+```bash
+$ ./api/test_call monorail.Issues GetIssue \
+ '{"issue_ref": {"project_name": "rutabaga", "local_id": 1}}' \
+ --test-account=test@example.com
+```
+
+## API Documentation
+
+All methods, request parameters, and responses are documented in
+[./api_proto](./api_proto).
+
+# Development
+
+## Regenerating Python from Protocol Buffers
+
+In order to regenerate the python server and client stubs from the `.proto`
+files, run this command:
+
+```bash
+$ make prpc_proto_v0
+```
diff --git a/api/__init__.py b/api/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/api/__init__.py
diff --git a/api/api_proto/__init__.py b/api/api_proto/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/api/api_proto/__init__.py
diff --git a/api/api_proto/common.proto b/api/api_proto/common.proto
new file mode 100644
index 0000000..0ff0750
--- /dev/null
+++ b/api/api_proto/common.proto
@@ -0,0 +1,109 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+// This file defines small protobufs that are included as parts of
+// multiple services or *_objects.proto PBs.
+//
+// "Ref" objects contain enough information for a UI to display
+// something to the user, and identifying info so that the client can
+// request more info from the server.
+
+syntax = "proto3";
+
+package monorail;
+
+
+// Next available tag: 3
+message ComponentRef {
+ string path = 1;
+ bool is_derived = 2;
+}
+
+
+// Next available tag: 9
+enum FieldType {
+ NO_TYPE = 0;
+ ENUM_TYPE = 1;
+ INT_TYPE = 2;
+ STR_TYPE = 3;
+ USER_TYPE = 4;
+ DATE_TYPE = 5;
+ BOOL_TYPE = 6;
+ URL_TYPE = 7;
+ APPROVAL_TYPE = 8;
+}
+
+
+// Next available tag: 5
+message FieldRef {
+ // TODO(crbug.com/monorail/4062): Don't use field IDs to identify fields.
+ uint64 field_id = 1;
+ string field_name = 2;
+ FieldType type = 3;
+ string approval_name = 4;
+}
+
+
+// Next available tag: 3
+message LabelRef {
+ string label = 1;
+ bool is_derived = 2;
+}
+
+
+// Next available tag: 4
+message StatusRef {
+ string status = 1;
+ bool means_open = 2;
+ bool is_derived = 3;
+}
+
+
+// Next available tag: 4
+message IssueRef {
+ string project_name = 1;
+ uint32 local_id = 2;
+ string ext_identifier = 3; // For referencing external issues, e.g. b/1234.
+}
+
+
+// Next available tag: 4
+message UserRef {
+ uint64 user_id = 1;
+ string display_name = 2; // email, or obscured like "usern...@example.com".
+ bool is_derived = 3;
+}
+
+
+// Next available tag: 4
+message HotlistRef {
+ // TODO(4131): Don't use hotlist IDs to identify hotlists.
+ uint64 hotlist_id = 1;
+ string name = 2;
+ UserRef owner = 3;
+}
+
+
+// Next available tag: 3
+message ValueAndWhy {
+ string value = 1;
+ string why = 2;
+}
+
+
+// Next available tag: 3
+message Pagination {
+ uint32 max_items = 1;
+ uint32 start = 2;
+}
+
+
+// Next available tag: 5
+message SavedQuery {
+ uint64 query_id = 1;
+ string name = 2;
+ string query = 3;
+ repeated string project_names = 4;
+}
diff --git a/api/api_proto/common_pb2.py b/api/api_proto/common_pb2.py
new file mode 100644
index 0000000..1d31b51
--- /dev/null
+++ b/api/api_proto/common_pb2.py
@@ -0,0 +1,610 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/api_proto/common.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf.internal import enum_type_wrapper
+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/api_proto/common.proto',
+ package='monorail',
+ syntax='proto3',
+ serialized_options=None,
+ serialized_pb=_b('\n\x1a\x61pi/api_proto/common.proto\x12\x08monorail\"0\n\x0c\x43omponentRef\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x12\n\nis_derived\x18\x02 \x01(\x08\"j\n\x08\x46ieldRef\x12\x10\n\x08\x66ield_id\x18\x01 \x01(\x04\x12\x12\n\nfield_name\x18\x02 \x01(\t\x12!\n\x04type\x18\x03 \x01(\x0e\x32\x13.monorail.FieldType\x12\x15\n\rapproval_name\x18\x04 \x01(\t\"-\n\x08LabelRef\x12\r\n\x05label\x18\x01 \x01(\t\x12\x12\n\nis_derived\x18\x02 \x01(\x08\"C\n\tStatusRef\x12\x0e\n\x06status\x18\x01 \x01(\t\x12\x12\n\nmeans_open\x18\x02 \x01(\x08\x12\x12\n\nis_derived\x18\x03 \x01(\x08\"J\n\x08IssueRef\x12\x14\n\x0cproject_name\x18\x01 \x01(\t\x12\x10\n\x08local_id\x18\x02 \x01(\r\x12\x16\n\x0e\x65xt_identifier\x18\x03 \x01(\t\"D\n\x07UserRef\x12\x0f\n\x07user_id\x18\x01 \x01(\x04\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12\x12\n\nis_derived\x18\x03 \x01(\x08\"P\n\nHotlistRef\x12\x12\n\nhotlist_id\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\x12 \n\x05owner\x18\x03 \x01(\x0b\x32\x11.monorail.UserRef\")\n\x0bValueAndWhy\x12\r\n\x05value\x18\x01 \x01(\t\x12\x0b\n\x03why\x18\x02 \x01(\t\".\n\nPagination\x12\x11\n\tmax_items\x18\x01 \x01(\r\x12\r\n\x05start\x18\x02 \x01(\r\"R\n\nSavedQuery\x12\x10\n\x08query_id\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05query\x18\x03 \x01(\t\x12\x15\n\rproject_names\x18\x04 \x03(\t*\x91\x01\n\tFieldType\x12\x0b\n\x07NO_TYPE\x10\x00\x12\r\n\tENUM_TYPE\x10\x01\x12\x0c\n\x08INT_TYPE\x10\x02\x12\x0c\n\x08STR_TYPE\x10\x03\x12\r\n\tUSER_TYPE\x10\x04\x12\r\n\tDATE_TYPE\x10\x05\x12\r\n\tBOOL_TYPE\x10\x06\x12\x0c\n\x08URL_TYPE\x10\x07\x12\x11\n\rAPPROVAL_TYPE\x10\x08\x62\x06proto3')
+)
+
+_FIELDTYPE = _descriptor.EnumDescriptor(
+ name='FieldType',
+ full_name='monorail.FieldType',
+ filename=None,
+ file=DESCRIPTOR,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='NO_TYPE', index=0, number=0,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='ENUM_TYPE', index=1, number=1,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='INT_TYPE', index=2, number=2,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='STR_TYPE', index=3, number=3,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='USER_TYPE', index=4, number=4,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='DATE_TYPE', index=5, number=5,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='BOOL_TYPE', index=6, number=6,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='URL_TYPE', index=7, number=7,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='APPROVAL_TYPE', index=8, number=8,
+ serialized_options=None,
+ type=None),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=718,
+ serialized_end=863,
+)
+_sym_db.RegisterEnumDescriptor(_FIELDTYPE)
+
+FieldType = enum_type_wrapper.EnumTypeWrapper(_FIELDTYPE)
+NO_TYPE = 0
+ENUM_TYPE = 1
+INT_TYPE = 2
+STR_TYPE = 3
+USER_TYPE = 4
+DATE_TYPE = 5
+BOOL_TYPE = 6
+URL_TYPE = 7
+APPROVAL_TYPE = 8
+
+
+
+_COMPONENTREF = _descriptor.Descriptor(
+ name='ComponentRef',
+ full_name='monorail.ComponentRef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='path', full_name='monorail.ComponentRef.path', 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),
+ _descriptor.FieldDescriptor(
+ name='is_derived', full_name='monorail.ComponentRef.is_derived', index=1,
+ number=2, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=40,
+ serialized_end=88,
+)
+
+
+_FIELDREF = _descriptor.Descriptor(
+ name='FieldRef',
+ full_name='monorail.FieldRef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='field_id', full_name='monorail.FieldRef.field_id', index=0,
+ number=1, type=4, cpp_type=4, 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),
+ _descriptor.FieldDescriptor(
+ name='field_name', full_name='monorail.FieldRef.field_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),
+ _descriptor.FieldDescriptor(
+ name='type', full_name='monorail.FieldRef.type', index=2,
+ number=3, type=14, cpp_type=8, 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),
+ _descriptor.FieldDescriptor(
+ name='approval_name', full_name='monorail.FieldRef.approval_name', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=90,
+ serialized_end=196,
+)
+
+
+_LABELREF = _descriptor.Descriptor(
+ name='LabelRef',
+ full_name='monorail.LabelRef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='label', full_name='monorail.LabelRef.label', 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),
+ _descriptor.FieldDescriptor(
+ name='is_derived', full_name='monorail.LabelRef.is_derived', index=1,
+ number=2, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=198,
+ serialized_end=243,
+)
+
+
+_STATUSREF = _descriptor.Descriptor(
+ name='StatusRef',
+ full_name='monorail.StatusRef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='status', full_name='monorail.StatusRef.status', 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),
+ _descriptor.FieldDescriptor(
+ name='means_open', full_name='monorail.StatusRef.means_open', index=1,
+ number=2, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='is_derived', full_name='monorail.StatusRef.is_derived', index=2,
+ number=3, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=245,
+ serialized_end=312,
+)
+
+
+_ISSUEREF = _descriptor.Descriptor(
+ name='IssueRef',
+ full_name='monorail.IssueRef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.IssueRef.project_name', 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),
+ _descriptor.FieldDescriptor(
+ name='local_id', full_name='monorail.IssueRef.local_id', index=1,
+ number=2, type=13, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='ext_identifier', full_name='monorail.IssueRef.ext_identifier', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=314,
+ serialized_end=388,
+)
+
+
+_USERREF = _descriptor.Descriptor(
+ name='UserRef',
+ full_name='monorail.UserRef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='user_id', full_name='monorail.UserRef.user_id', index=0,
+ number=1, type=4, cpp_type=4, 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),
+ _descriptor.FieldDescriptor(
+ name='display_name', full_name='monorail.UserRef.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),
+ _descriptor.FieldDescriptor(
+ name='is_derived', full_name='monorail.UserRef.is_derived', index=2,
+ number=3, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=390,
+ serialized_end=458,
+)
+
+
+_HOTLISTREF = _descriptor.Descriptor(
+ name='HotlistRef',
+ full_name='monorail.HotlistRef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='hotlist_id', full_name='monorail.HotlistRef.hotlist_id', index=0,
+ number=1, type=4, cpp_type=4, 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),
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.HotlistRef.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),
+ _descriptor.FieldDescriptor(
+ name='owner', full_name='monorail.HotlistRef.owner', index=2,
+ number=3, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=460,
+ serialized_end=540,
+)
+
+
+_VALUEANDWHY = _descriptor.Descriptor(
+ name='ValueAndWhy',
+ full_name='monorail.ValueAndWhy',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='value', full_name='monorail.ValueAndWhy.value', 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),
+ _descriptor.FieldDescriptor(
+ name='why', full_name='monorail.ValueAndWhy.why', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=542,
+ serialized_end=583,
+)
+
+
+_PAGINATION = _descriptor.Descriptor(
+ name='Pagination',
+ full_name='monorail.Pagination',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='max_items', full_name='monorail.Pagination.max_items', index=0,
+ number=1, type=13, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='start', full_name='monorail.Pagination.start', index=1,
+ number=2, type=13, cpp_type=3, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=585,
+ serialized_end=631,
+)
+
+
+_SAVEDQUERY = _descriptor.Descriptor(
+ name='SavedQuery',
+ full_name='monorail.SavedQuery',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='query_id', full_name='monorail.SavedQuery.query_id', index=0,
+ number=1, type=4, cpp_type=4, 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),
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.SavedQuery.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),
+ _descriptor.FieldDescriptor(
+ name='query', full_name='monorail.SavedQuery.query', 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),
+ _descriptor.FieldDescriptor(
+ name='project_names', full_name='monorail.SavedQuery.project_names', index=3,
+ number=4, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=633,
+ serialized_end=715,
+)
+
+_FIELDREF.fields_by_name['type'].enum_type = _FIELDTYPE
+_HOTLISTREF.fields_by_name['owner'].message_type = _USERREF
+DESCRIPTOR.message_types_by_name['ComponentRef'] = _COMPONENTREF
+DESCRIPTOR.message_types_by_name['FieldRef'] = _FIELDREF
+DESCRIPTOR.message_types_by_name['LabelRef'] = _LABELREF
+DESCRIPTOR.message_types_by_name['StatusRef'] = _STATUSREF
+DESCRIPTOR.message_types_by_name['IssueRef'] = _ISSUEREF
+DESCRIPTOR.message_types_by_name['UserRef'] = _USERREF
+DESCRIPTOR.message_types_by_name['HotlistRef'] = _HOTLISTREF
+DESCRIPTOR.message_types_by_name['ValueAndWhy'] = _VALUEANDWHY
+DESCRIPTOR.message_types_by_name['Pagination'] = _PAGINATION
+DESCRIPTOR.message_types_by_name['SavedQuery'] = _SAVEDQUERY
+DESCRIPTOR.enum_types_by_name['FieldType'] = _FIELDTYPE
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+ComponentRef = _reflection.GeneratedProtocolMessageType('ComponentRef', (_message.Message,), dict(
+ DESCRIPTOR = _COMPONENTREF,
+ __module__ = 'api.api_proto.common_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ComponentRef)
+ ))
+_sym_db.RegisterMessage(ComponentRef)
+
+FieldRef = _reflection.GeneratedProtocolMessageType('FieldRef', (_message.Message,), dict(
+ DESCRIPTOR = _FIELDREF,
+ __module__ = 'api.api_proto.common_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.FieldRef)
+ ))
+_sym_db.RegisterMessage(FieldRef)
+
+LabelRef = _reflection.GeneratedProtocolMessageType('LabelRef', (_message.Message,), dict(
+ DESCRIPTOR = _LABELREF,
+ __module__ = 'api.api_proto.common_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.LabelRef)
+ ))
+_sym_db.RegisterMessage(LabelRef)
+
+StatusRef = _reflection.GeneratedProtocolMessageType('StatusRef', (_message.Message,), dict(
+ DESCRIPTOR = _STATUSREF,
+ __module__ = 'api.api_proto.common_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.StatusRef)
+ ))
+_sym_db.RegisterMessage(StatusRef)
+
+IssueRef = _reflection.GeneratedProtocolMessageType('IssueRef', (_message.Message,), dict(
+ DESCRIPTOR = _ISSUEREF,
+ __module__ = 'api.api_proto.common_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.IssueRef)
+ ))
+_sym_db.RegisterMessage(IssueRef)
+
+UserRef = _reflection.GeneratedProtocolMessageType('UserRef', (_message.Message,), dict(
+ DESCRIPTOR = _USERREF,
+ __module__ = 'api.api_proto.common_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.UserRef)
+ ))
+_sym_db.RegisterMessage(UserRef)
+
+HotlistRef = _reflection.GeneratedProtocolMessageType('HotlistRef', (_message.Message,), dict(
+ DESCRIPTOR = _HOTLISTREF,
+ __module__ = 'api.api_proto.common_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.HotlistRef)
+ ))
+_sym_db.RegisterMessage(HotlistRef)
+
+ValueAndWhy = _reflection.GeneratedProtocolMessageType('ValueAndWhy', (_message.Message,), dict(
+ DESCRIPTOR = _VALUEANDWHY,
+ __module__ = 'api.api_proto.common_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ValueAndWhy)
+ ))
+_sym_db.RegisterMessage(ValueAndWhy)
+
+Pagination = _reflection.GeneratedProtocolMessageType('Pagination', (_message.Message,), dict(
+ DESCRIPTOR = _PAGINATION,
+ __module__ = 'api.api_proto.common_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.Pagination)
+ ))
+_sym_db.RegisterMessage(Pagination)
+
+SavedQuery = _reflection.GeneratedProtocolMessageType('SavedQuery', (_message.Message,), dict(
+ DESCRIPTOR = _SAVEDQUERY,
+ __module__ = 'api.api_proto.common_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.SavedQuery)
+ ))
+_sym_db.RegisterMessage(SavedQuery)
+
+
+# @@protoc_insertion_point(module_scope)
diff --git a/api/api_proto/common_prpc_pb2.py b/api/api_proto/common_prpc_pb2.py
new file mode 100644
index 0000000..fb73302
--- /dev/null
+++ b/api/api_proto/common_prpc_pb2.py
@@ -0,0 +1,4 @@
+# Generated by the pRPC protocol buffer compiler plugin. DO NOT EDIT!
+# source: api/api_proto/common.proto
+
+from google.protobuf import descriptor_pb2
diff --git a/api/api_proto/features.proto b/api/api_proto/features.proto
new file mode 100644
index 0000000..7d7f2fc
--- /dev/null
+++ b/api/api_proto/features.proto
@@ -0,0 +1,229 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+syntax = "proto3";
+
+package monorail;
+
+import "api/api_proto/common.proto";
+import "api/api_proto/features_objects.proto";
+
+
+service Features {
+ rpc ListHotlistsByUser (ListHotlistsByUserRequest) returns (ListHotlistsByUserResponse) {}
+ rpc ListHotlistsByIssue (ListHotlistsByIssueRequest) returns (ListHotlistsByIssueResponse) {}
+ rpc ListRecentlyVisitedHotlists (ListRecentlyVisitedHotlistsRequest) returns (ListRecentlyVisitedHotlistsResponse) {}
+ rpc ListStarredHotlists (ListStarredHotlistsRequest) returns (ListStarredHotlistsResponse) {}
+ rpc GetHotlistStarCount (GetHotlistStarCountRequest) returns (GetHotlistStarCountResponse) {}
+ rpc StarHotlist (StarHotlistRequest) returns (StarHotlistResponse) {}
+ rpc GetHotlist (GetHotlistRequest) returns (GetHotlistResponse) {}
+ rpc ListHotlistItems (ListHotlistItemsRequest) returns (ListHotlistItemsResponse) {}
+ rpc CreateHotlist (CreateHotlistRequest) returns (CreateHotlistResponse) {}
+ rpc CheckHotlistName (CheckHotlistNameRequest) returns (CheckHotlistNameResponse) {}
+ rpc RemoveIssuesFromHotlists (RemoveIssuesFromHotlistsRequest) returns (RemoveIssuesFromHotlistsResponse) {}
+ rpc AddIssuesToHotlists (AddIssuesToHotlistsRequest) returns (AddIssuesToHotlistsResponse) {}
+ rpc RerankHotlistIssues (RerankHotlistIssuesRequest) returns (RerankHotlistIssuesResponse) {}
+ rpc UpdateHotlistIssueNote (UpdateHotlistIssueNoteRequest) returns (UpdateHotlistIssueNoteResponse) {}
+ rpc DeleteHotlist (DeleteHotlistRequest) returns (DeleteHotlistResponse) {}
+ rpc PredictComponent (PredictComponentRequest) returns (PredictComponentResponse) {}
+}
+
+
+// Next available tag: 3
+message ListHotlistsByUserRequest {
+ UserRef user = 2;
+}
+
+
+// Next available tag: 2
+message ListHotlistsByUserResponse {
+ repeated Hotlist hotlists = 1;
+}
+
+
+// Next available tag: 3
+message ListHotlistsByIssueRequest {
+ IssueRef issue = 2;
+}
+
+
+// Next available tag: 2
+message ListHotlistsByIssueResponse {
+ repeated Hotlist hotlists = 1;
+}
+
+
+// Next available tag: 2
+message ListRecentlyVisitedHotlistsRequest {
+}
+
+
+// Next available tag: 2
+message ListRecentlyVisitedHotlistsResponse {
+ repeated Hotlist hotlists = 1;
+}
+
+
+// Next available tag: 2
+message ListStarredHotlistsRequest {
+}
+
+
+// Next available tag: 2
+message ListStarredHotlistsResponse {
+ repeated Hotlist hotlists = 1;
+}
+
+
+// Next available tag: 3
+message GetHotlistStarCountRequest {
+ HotlistRef hotlist_ref = 2;
+}
+
+
+// Next available tag: 2
+message GetHotlistStarCountResponse {
+ uint32 star_count = 1;
+}
+
+
+// Next available tag: 4
+message StarHotlistRequest {
+ HotlistRef hotlist_ref = 2;
+ bool starred = 3;
+}
+
+
+// Next available tag: 2
+message StarHotlistResponse {
+ uint32 star_count = 1;
+}
+
+// Next available tag: 2
+message GetHotlistRequest {
+ HotlistRef hotlist_ref = 1;
+}
+
+// Next available tag: 2
+message GetHotlistResponse {
+ Hotlist hotlist = 1;
+}
+
+
+// Next available tag: 7
+message ListHotlistItemsRequest {
+ HotlistRef hotlist_ref = 2;
+ Pagination pagination = 3;
+ uint32 can = 4;
+ string sort_spec = 5;
+ string group_by_spec = 6;
+}
+
+
+// Next available tag: 2
+message ListHotlistItemsResponse {
+ repeated HotlistItem items = 1;
+}
+
+
+// Next available tag: 7
+message CreateHotlistRequest {
+ string name = 2;
+ string summary = 3;
+ string description = 4;
+ repeated UserRef editor_refs = 5;
+ repeated IssueRef issue_refs = 6;
+ bool is_private = 7;
+}
+
+
+// Next available tag: 1
+message CreateHotlistResponse {
+}
+
+
+// Next available tag: 3
+message CheckHotlistNameRequest {
+ string name = 2;
+}
+
+
+// Next available tag: 1
+message CheckHotlistNameResponse {
+ string error = 1;
+}
+
+
+// Next available tag: 4
+message RemoveIssuesFromHotlistsRequest {
+ repeated HotlistRef hotlist_refs = 2;
+ repeated IssueRef issue_refs = 3;
+}
+
+
+// Next available tag: 1
+message RemoveIssuesFromHotlistsResponse {
+}
+
+
+// Next available tag: 5
+message AddIssuesToHotlistsRequest {
+ repeated HotlistRef hotlist_refs = 2;
+ repeated IssueRef issue_refs = 3;
+ string note = 4;
+}
+
+
+// Next available tag: 1
+message AddIssuesToHotlistsResponse {
+}
+
+// Next available tag: 5
+message RerankHotlistIssuesRequest{
+ HotlistRef hotlist_ref = 1;
+ repeated IssueRef moved_refs = 2;
+ IssueRef target_ref = 3;
+ bool split_above = 4;
+}
+
+// Next available tag: 1
+message RerankHotlistIssuesResponse{
+}
+
+// Next available tag: 5
+message UpdateHotlistIssueNoteRequest {
+ HotlistRef hotlist_ref = 2;
+ IssueRef issue_ref = 3;
+ string note = 4;
+}
+
+
+// Next available tag: 1
+message UpdateHotlistIssueNoteResponse {
+}
+
+
+// Next available tag: 2
+message DeleteHotlistRequest {
+ HotlistRef hotlist_ref = 1;
+}
+
+
+// Next available tag: 1
+message DeleteHotlistResponse {
+}
+
+
+// Next available tag: 4
+message PredictComponentRequest {
+ string text = 2;
+ string project_name = 3;
+}
+
+
+// Next available tag: 2
+message PredictComponentResponse {
+ ComponentRef component_ref = 1;
+}
diff --git a/api/api_proto/features_objects.proto b/api/api_proto/features_objects.proto
new file mode 100644
index 0000000..14b61ac
--- /dev/null
+++ b/api/api_proto/features_objects.proto
@@ -0,0 +1,43 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+syntax = "proto3";
+
+package monorail;
+
+import "api/api_proto/common.proto";
+import "api/api_proto/issue_objects.proto";
+
+
+// Next available tag: 9
+message Hotlist {
+ UserRef owner_ref = 1;
+ repeated UserRef editor_refs = 5;
+ repeated UserRef follower_refs = 6;
+ string name = 2;
+ string summary = 3;
+ string description = 4;
+ string default_col_spec = 7;
+ bool is_private = 8;
+}
+
+
+// Next available tag: 6
+message HotlistItem {
+ Issue issue = 1;
+ uint32 rank = 2;
+ UserRef adder_ref = 3;
+ uint32 added_timestamp = 4;
+ string note = 5;
+}
+
+
+// Next available tag: 5
+message HotlistPeopleDelta {
+ UserRef new_owner_ref = 1;
+ repeated UserRef add_editor_refs = 2;
+ repeated UserRef add_follower_refs = 3;
+ repeated UserRef remove_user_refs = 4;
+}
\ No newline at end of file
diff --git a/api/api_proto/features_objects_pb2.py b/api/api_proto/features_objects_pb2.py
new file mode 100644
index 0000000..63d8091
--- /dev/null
+++ b/api/api_proto/features_objects_pb2.py
@@ -0,0 +1,257 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/api_proto/features_objects.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+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()
+
+
+from api.api_proto import common_pb2 as api_dot_api__proto_dot_common__pb2
+from api.api_proto import issue_objects_pb2 as api_dot_api__proto_dot_issue__objects__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='api/api_proto/features_objects.proto',
+ package='monorail',
+ syntax='proto3',
+ serialized_options=None,
+ serialized_pb=_b('\n$api/api_proto/features_objects.proto\x12\x08monorail\x1a\x1a\x61pi/api_proto/common.proto\x1a!api/api_proto/issue_objects.proto\"\xe3\x01\n\x07Hotlist\x12$\n\towner_ref\x18\x01 \x01(\x0b\x32\x11.monorail.UserRef\x12&\n\x0b\x65\x64itor_refs\x18\x05 \x03(\x0b\x32\x11.monorail.UserRef\x12(\n\rfollower_refs\x18\x06 \x03(\x0b\x32\x11.monorail.UserRef\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0f\n\x07summary\x18\x03 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\x18\n\x10\x64\x65\x66\x61ult_col_spec\x18\x07 \x01(\t\x12\x12\n\nis_private\x18\x08 \x01(\x08\"\x88\x01\n\x0bHotlistItem\x12\x1e\n\x05issue\x18\x01 \x01(\x0b\x32\x0f.monorail.Issue\x12\x0c\n\x04rank\x18\x02 \x01(\r\x12$\n\tadder_ref\x18\x03 \x01(\x0b\x32\x11.monorail.UserRef\x12\x17\n\x0f\x61\x64\x64\x65\x64_timestamp\x18\x04 \x01(\r\x12\x0c\n\x04note\x18\x05 \x01(\t\"\xc5\x01\n\x12HotlistPeopleDelta\x12(\n\rnew_owner_ref\x18\x01 \x01(\x0b\x32\x11.monorail.UserRef\x12*\n\x0f\x61\x64\x64_editor_refs\x18\x02 \x03(\x0b\x32\x11.monorail.UserRef\x12,\n\x11\x61\x64\x64_follower_refs\x18\x03 \x03(\x0b\x32\x11.monorail.UserRef\x12+\n\x10remove_user_refs\x18\x04 \x03(\x0b\x32\x11.monorail.UserRefb\x06proto3')
+ ,
+ dependencies=[api_dot_api__proto_dot_common__pb2.DESCRIPTOR,api_dot_api__proto_dot_issue__objects__pb2.DESCRIPTOR,])
+
+
+
+
+_HOTLIST = _descriptor.Descriptor(
+ name='Hotlist',
+ full_name='monorail.Hotlist',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='owner_ref', full_name='monorail.Hotlist.owner_ref', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='editor_refs', full_name='monorail.Hotlist.editor_refs', index=1,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='follower_refs', full_name='monorail.Hotlist.follower_refs', index=2,
+ number=6, 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),
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.Hotlist.name', index=3,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='summary', full_name='monorail.Hotlist.summary', index=4,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='description', full_name='monorail.Hotlist.description', index=5,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='default_col_spec', full_name='monorail.Hotlist.default_col_spec', index=6,
+ number=7, 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),
+ _descriptor.FieldDescriptor(
+ name='is_private', full_name='monorail.Hotlist.is_private', index=7,
+ number=8, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=114,
+ serialized_end=341,
+)
+
+
+_HOTLISTITEM = _descriptor.Descriptor(
+ name='HotlistItem',
+ full_name='monorail.HotlistItem',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue', full_name='monorail.HotlistItem.issue', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='rank', full_name='monorail.HotlistItem.rank', index=1,
+ number=2, type=13, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='adder_ref', full_name='monorail.HotlistItem.adder_ref', index=2,
+ number=3, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='added_timestamp', full_name='monorail.HotlistItem.added_timestamp', index=3,
+ number=4, type=13, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='note', full_name='monorail.HotlistItem.note', index=4,
+ number=5, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=344,
+ serialized_end=480,
+)
+
+
+_HOTLISTPEOPLEDELTA = _descriptor.Descriptor(
+ name='HotlistPeopleDelta',
+ full_name='monorail.HotlistPeopleDelta',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='new_owner_ref', full_name='monorail.HotlistPeopleDelta.new_owner_ref', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='add_editor_refs', full_name='monorail.HotlistPeopleDelta.add_editor_refs', index=1,
+ number=2, 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),
+ _descriptor.FieldDescriptor(
+ name='add_follower_refs', full_name='monorail.HotlistPeopleDelta.add_follower_refs', index=2,
+ number=3, 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),
+ _descriptor.FieldDescriptor(
+ name='remove_user_refs', full_name='monorail.HotlistPeopleDelta.remove_user_refs', index=3,
+ number=4, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=483,
+ serialized_end=680,
+)
+
+_HOTLIST.fields_by_name['owner_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_HOTLIST.fields_by_name['editor_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_HOTLIST.fields_by_name['follower_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_HOTLISTITEM.fields_by_name['issue'].message_type = api_dot_api__proto_dot_issue__objects__pb2._ISSUE
+_HOTLISTITEM.fields_by_name['adder_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_HOTLISTPEOPLEDELTA.fields_by_name['new_owner_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_HOTLISTPEOPLEDELTA.fields_by_name['add_editor_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_HOTLISTPEOPLEDELTA.fields_by_name['add_follower_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_HOTLISTPEOPLEDELTA.fields_by_name['remove_user_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+DESCRIPTOR.message_types_by_name['Hotlist'] = _HOTLIST
+DESCRIPTOR.message_types_by_name['HotlistItem'] = _HOTLISTITEM
+DESCRIPTOR.message_types_by_name['HotlistPeopleDelta'] = _HOTLISTPEOPLEDELTA
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+Hotlist = _reflection.GeneratedProtocolMessageType('Hotlist', (_message.Message,), dict(
+ DESCRIPTOR = _HOTLIST,
+ __module__ = 'api.api_proto.features_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.Hotlist)
+ ))
+_sym_db.RegisterMessage(Hotlist)
+
+HotlistItem = _reflection.GeneratedProtocolMessageType('HotlistItem', (_message.Message,), dict(
+ DESCRIPTOR = _HOTLISTITEM,
+ __module__ = 'api.api_proto.features_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.HotlistItem)
+ ))
+_sym_db.RegisterMessage(HotlistItem)
+
+HotlistPeopleDelta = _reflection.GeneratedProtocolMessageType('HotlistPeopleDelta', (_message.Message,), dict(
+ DESCRIPTOR = _HOTLISTPEOPLEDELTA,
+ __module__ = 'api.api_proto.features_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.HotlistPeopleDelta)
+ ))
+_sym_db.RegisterMessage(HotlistPeopleDelta)
+
+
+# @@protoc_insertion_point(module_scope)
diff --git a/api/api_proto/features_pb2.py b/api/api_proto/features_pb2.py
new file mode 100644
index 0000000..c415a35
--- /dev/null
+++ b/api/api_proto/features_pb2.py
@@ -0,0 +1,1543 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/api_proto/features.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+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()
+
+
+from api.api_proto import common_pb2 as api_dot_api__proto_dot_common__pb2
+from api.api_proto import features_objects_pb2 as api_dot_api__proto_dot_features__objects__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='api/api_proto/features.proto',
+ package='monorail',
+ syntax='proto3',
+ serialized_options=None,
+ serialized_pb=_b('\n\x1c\x61pi/api_proto/features.proto\x12\x08monorail\x1a\x1a\x61pi/api_proto/common.proto\x1a$api/api_proto/features_objects.proto\"<\n\x19ListHotlistsByUserRequest\x12\x1f\n\x04user\x18\x02 \x01(\x0b\x32\x11.monorail.UserRef\"A\n\x1aListHotlistsByUserResponse\x12#\n\x08hotlists\x18\x01 \x03(\x0b\x32\x11.monorail.Hotlist\"?\n\x1aListHotlistsByIssueRequest\x12!\n\x05issue\x18\x02 \x01(\x0b\x32\x12.monorail.IssueRef\"B\n\x1bListHotlistsByIssueResponse\x12#\n\x08hotlists\x18\x01 \x03(\x0b\x32\x11.monorail.Hotlist\"$\n\"ListRecentlyVisitedHotlistsRequest\"J\n#ListRecentlyVisitedHotlistsResponse\x12#\n\x08hotlists\x18\x01 \x03(\x0b\x32\x11.monorail.Hotlist\"\x1c\n\x1aListStarredHotlistsRequest\"B\n\x1bListStarredHotlistsResponse\x12#\n\x08hotlists\x18\x01 \x03(\x0b\x32\x11.monorail.Hotlist\"G\n\x1aGetHotlistStarCountRequest\x12)\n\x0bhotlist_ref\x18\x02 \x01(\x0b\x32\x14.monorail.HotlistRef\"1\n\x1bGetHotlistStarCountResponse\x12\x12\n\nstar_count\x18\x01 \x01(\r\"P\n\x12StarHotlistRequest\x12)\n\x0bhotlist_ref\x18\x02 \x01(\x0b\x32\x14.monorail.HotlistRef\x12\x0f\n\x07starred\x18\x03 \x01(\x08\")\n\x13StarHotlistResponse\x12\x12\n\nstar_count\x18\x01 \x01(\r\">\n\x11GetHotlistRequest\x12)\n\x0bhotlist_ref\x18\x01 \x01(\x0b\x32\x14.monorail.HotlistRef\"8\n\x12GetHotlistResponse\x12\"\n\x07hotlist\x18\x01 \x01(\x0b\x32\x11.monorail.Hotlist\"\xa5\x01\n\x17ListHotlistItemsRequest\x12)\n\x0bhotlist_ref\x18\x02 \x01(\x0b\x32\x14.monorail.HotlistRef\x12(\n\npagination\x18\x03 \x01(\x0b\x32\x14.monorail.Pagination\x12\x0b\n\x03\x63\x61n\x18\x04 \x01(\r\x12\x11\n\tsort_spec\x18\x05 \x01(\t\x12\x15\n\rgroup_by_spec\x18\x06 \x01(\t\"@\n\x18ListHotlistItemsResponse\x12$\n\x05items\x18\x01 \x03(\x0b\x32\x15.monorail.HotlistItem\"\xae\x01\n\x14\x43reateHotlistRequest\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0f\n\x07summary\x18\x03 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12&\n\x0b\x65\x64itor_refs\x18\x05 \x03(\x0b\x32\x11.monorail.UserRef\x12&\n\nissue_refs\x18\x06 \x03(\x0b\x32\x12.monorail.IssueRef\x12\x12\n\nis_private\x18\x07 \x01(\x08\"\x17\n\x15\x43reateHotlistResponse\"\'\n\x17\x43heckHotlistNameRequest\x12\x0c\n\x04name\x18\x02 \x01(\t\")\n\x18\x43heckHotlistNameResponse\x12\r\n\x05\x65rror\x18\x01 \x01(\t\"u\n\x1fRemoveIssuesFromHotlistsRequest\x12*\n\x0chotlist_refs\x18\x02 \x03(\x0b\x32\x14.monorail.HotlistRef\x12&\n\nissue_refs\x18\x03 \x03(\x0b\x32\x12.monorail.IssueRef\"\"\n RemoveIssuesFromHotlistsResponse\"~\n\x1a\x41\x64\x64IssuesToHotlistsRequest\x12*\n\x0chotlist_refs\x18\x02 \x03(\x0b\x32\x14.monorail.HotlistRef\x12&\n\nissue_refs\x18\x03 \x03(\x0b\x32\x12.monorail.IssueRef\x12\x0c\n\x04note\x18\x04 \x01(\t\"\x1d\n\x1b\x41\x64\x64IssuesToHotlistsResponse\"\xac\x01\n\x1aRerankHotlistIssuesRequest\x12)\n\x0bhotlist_ref\x18\x01 \x01(\x0b\x32\x14.monorail.HotlistRef\x12&\n\nmoved_refs\x18\x02 \x03(\x0b\x32\x12.monorail.IssueRef\x12&\n\ntarget_ref\x18\x03 \x01(\x0b\x32\x12.monorail.IssueRef\x12\x13\n\x0bsplit_above\x18\x04 \x01(\x08\"\x1d\n\x1bRerankHotlistIssuesResponse\"\x7f\n\x1dUpdateHotlistIssueNoteRequest\x12)\n\x0bhotlist_ref\x18\x02 \x01(\x0b\x32\x14.monorail.HotlistRef\x12%\n\tissue_ref\x18\x03 \x01(\x0b\x32\x12.monorail.IssueRef\x12\x0c\n\x04note\x18\x04 \x01(\t\" \n\x1eUpdateHotlistIssueNoteResponse\"A\n\x14\x44\x65leteHotlistRequest\x12)\n\x0bhotlist_ref\x18\x01 \x01(\x0b\x32\x14.monorail.HotlistRef\"\x17\n\x15\x44\x65leteHotlistResponse\"=\n\x17PredictComponentRequest\x12\x0c\n\x04text\x18\x02 \x01(\t\x12\x14\n\x0cproject_name\x18\x03 \x01(\t\"I\n\x18PredictComponentResponse\x12-\n\rcomponent_ref\x18\x01 \x01(\x0b\x32\x16.monorail.ComponentRef2\xa5\x0c\n\x08\x46\x65\x61tures\x12\x61\n\x12ListHotlistsByUser\x12#.monorail.ListHotlistsByUserRequest\x1a$.monorail.ListHotlistsByUserResponse\"\x00\x12\x64\n\x13ListHotlistsByIssue\x12$.monorail.ListHotlistsByIssueRequest\x1a%.monorail.ListHotlistsByIssueResponse\"\x00\x12|\n\x1bListRecentlyVisitedHotlists\x12,.monorail.ListRecentlyVisitedHotlistsRequest\x1a-.monorail.ListRecentlyVisitedHotlistsResponse\"\x00\x12\x64\n\x13ListStarredHotlists\x12$.monorail.ListStarredHotlistsRequest\x1a%.monorail.ListStarredHotlistsResponse\"\x00\x12\x64\n\x13GetHotlistStarCount\x12$.monorail.GetHotlistStarCountRequest\x1a%.monorail.GetHotlistStarCountResponse\"\x00\x12L\n\x0bStarHotlist\x12\x1c.monorail.StarHotlistRequest\x1a\x1d.monorail.StarHotlistResponse\"\x00\x12I\n\nGetHotlist\x12\x1b.monorail.GetHotlistRequest\x1a\x1c.monorail.GetHotlistResponse\"\x00\x12[\n\x10ListHotlistItems\x12!.monorail.ListHotlistItemsRequest\x1a\".monorail.ListHotlistItemsResponse\"\x00\x12R\n\rCreateHotlist\x12\x1e.monorail.CreateHotlistRequest\x1a\x1f.monorail.CreateHotlistResponse\"\x00\x12[\n\x10\x43heckHotlistName\x12!.monorail.CheckHotlistNameRequest\x1a\".monorail.CheckHotlistNameResponse\"\x00\x12s\n\x18RemoveIssuesFromHotlists\x12).monorail.RemoveIssuesFromHotlistsRequest\x1a*.monorail.RemoveIssuesFromHotlistsResponse\"\x00\x12\x64\n\x13\x41\x64\x64IssuesToHotlists\x12$.monorail.AddIssuesToHotlistsRequest\x1a%.monorail.AddIssuesToHotlistsResponse\"\x00\x12\x64\n\x13RerankHotlistIssues\x12$.monorail.RerankHotlistIssuesRequest\x1a%.monorail.RerankHotlistIssuesResponse\"\x00\x12m\n\x16UpdateHotlistIssueNote\x12\'.monorail.UpdateHotlistIssueNoteRequest\x1a(.monorail.UpdateHotlistIssueNoteResponse\"\x00\x12R\n\rDeleteHotlist\x12\x1e.monorail.DeleteHotlistRequest\x1a\x1f.monorail.DeleteHotlistResponse\"\x00\x12[\n\x10PredictComponent\x12!.monorail.PredictComponentRequest\x1a\".monorail.PredictComponentResponse\"\x00\x62\x06proto3')
+ ,
+ dependencies=[api_dot_api__proto_dot_common__pb2.DESCRIPTOR,api_dot_api__proto_dot_features__objects__pb2.DESCRIPTOR,])
+
+
+
+
+_LISTHOTLISTSBYUSERREQUEST = _descriptor.Descriptor(
+ name='ListHotlistsByUserRequest',
+ full_name='monorail.ListHotlistsByUserRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='user', full_name='monorail.ListHotlistsByUserRequest.user', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=108,
+ serialized_end=168,
+)
+
+
+_LISTHOTLISTSBYUSERRESPONSE = _descriptor.Descriptor(
+ name='ListHotlistsByUserResponse',
+ full_name='monorail.ListHotlistsByUserResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='hotlists', full_name='monorail.ListHotlistsByUserResponse.hotlists', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=170,
+ serialized_end=235,
+)
+
+
+_LISTHOTLISTSBYISSUEREQUEST = _descriptor.Descriptor(
+ name='ListHotlistsByIssueRequest',
+ full_name='monorail.ListHotlistsByIssueRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue', full_name='monorail.ListHotlistsByIssueRequest.issue', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=237,
+ serialized_end=300,
+)
+
+
+_LISTHOTLISTSBYISSUERESPONSE = _descriptor.Descriptor(
+ name='ListHotlistsByIssueResponse',
+ full_name='monorail.ListHotlistsByIssueResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='hotlists', full_name='monorail.ListHotlistsByIssueResponse.hotlists', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=302,
+ serialized_end=368,
+)
+
+
+_LISTRECENTLYVISITEDHOTLISTSREQUEST = _descriptor.Descriptor(
+ name='ListRecentlyVisitedHotlistsRequest',
+ full_name='monorail.ListRecentlyVisitedHotlistsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=370,
+ serialized_end=406,
+)
+
+
+_LISTRECENTLYVISITEDHOTLISTSRESPONSE = _descriptor.Descriptor(
+ name='ListRecentlyVisitedHotlistsResponse',
+ full_name='monorail.ListRecentlyVisitedHotlistsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='hotlists', full_name='monorail.ListRecentlyVisitedHotlistsResponse.hotlists', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=408,
+ serialized_end=482,
+)
+
+
+_LISTSTARREDHOTLISTSREQUEST = _descriptor.Descriptor(
+ name='ListStarredHotlistsRequest',
+ full_name='monorail.ListStarredHotlistsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=484,
+ serialized_end=512,
+)
+
+
+_LISTSTARREDHOTLISTSRESPONSE = _descriptor.Descriptor(
+ name='ListStarredHotlistsResponse',
+ full_name='monorail.ListStarredHotlistsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='hotlists', full_name='monorail.ListStarredHotlistsResponse.hotlists', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=514,
+ serialized_end=580,
+)
+
+
+_GETHOTLISTSTARCOUNTREQUEST = _descriptor.Descriptor(
+ name='GetHotlistStarCountRequest',
+ full_name='monorail.GetHotlistStarCountRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='hotlist_ref', full_name='monorail.GetHotlistStarCountRequest.hotlist_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=582,
+ serialized_end=653,
+)
+
+
+_GETHOTLISTSTARCOUNTRESPONSE = _descriptor.Descriptor(
+ name='GetHotlistStarCountResponse',
+ full_name='monorail.GetHotlistStarCountResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='star_count', full_name='monorail.GetHotlistStarCountResponse.star_count', index=0,
+ number=1, type=13, cpp_type=3, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=655,
+ serialized_end=704,
+)
+
+
+_STARHOTLISTREQUEST = _descriptor.Descriptor(
+ name='StarHotlistRequest',
+ full_name='monorail.StarHotlistRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='hotlist_ref', full_name='monorail.StarHotlistRequest.hotlist_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='starred', full_name='monorail.StarHotlistRequest.starred', index=1,
+ number=3, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=706,
+ serialized_end=786,
+)
+
+
+_STARHOTLISTRESPONSE = _descriptor.Descriptor(
+ name='StarHotlistResponse',
+ full_name='monorail.StarHotlistResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='star_count', full_name='monorail.StarHotlistResponse.star_count', index=0,
+ number=1, type=13, cpp_type=3, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=788,
+ serialized_end=829,
+)
+
+
+_GETHOTLISTREQUEST = _descriptor.Descriptor(
+ name='GetHotlistRequest',
+ full_name='monorail.GetHotlistRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='hotlist_ref', full_name='monorail.GetHotlistRequest.hotlist_ref', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=831,
+ serialized_end=893,
+)
+
+
+_GETHOTLISTRESPONSE = _descriptor.Descriptor(
+ name='GetHotlistResponse',
+ full_name='monorail.GetHotlistResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='hotlist', full_name='monorail.GetHotlistResponse.hotlist', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=895,
+ serialized_end=951,
+)
+
+
+_LISTHOTLISTITEMSREQUEST = _descriptor.Descriptor(
+ name='ListHotlistItemsRequest',
+ full_name='monorail.ListHotlistItemsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='hotlist_ref', full_name='monorail.ListHotlistItemsRequest.hotlist_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='pagination', full_name='monorail.ListHotlistItemsRequest.pagination', index=1,
+ number=3, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='can', full_name='monorail.ListHotlistItemsRequest.can', index=2,
+ number=4, type=13, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='sort_spec', full_name='monorail.ListHotlistItemsRequest.sort_spec', index=3,
+ number=5, 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),
+ _descriptor.FieldDescriptor(
+ name='group_by_spec', full_name='monorail.ListHotlistItemsRequest.group_by_spec', index=4,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=954,
+ serialized_end=1119,
+)
+
+
+_LISTHOTLISTITEMSRESPONSE = _descriptor.Descriptor(
+ name='ListHotlistItemsResponse',
+ full_name='monorail.ListHotlistItemsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='items', full_name='monorail.ListHotlistItemsResponse.items', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1121,
+ serialized_end=1185,
+)
+
+
+_CREATEHOTLISTREQUEST = _descriptor.Descriptor(
+ name='CreateHotlistRequest',
+ full_name='monorail.CreateHotlistRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.CreateHotlistRequest.name', index=0,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='summary', full_name='monorail.CreateHotlistRequest.summary', index=1,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='description', full_name='monorail.CreateHotlistRequest.description', index=2,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='editor_refs', full_name='monorail.CreateHotlistRequest.editor_refs', index=3,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='issue_refs', full_name='monorail.CreateHotlistRequest.issue_refs', index=4,
+ number=6, 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),
+ _descriptor.FieldDescriptor(
+ name='is_private', full_name='monorail.CreateHotlistRequest.is_private', index=5,
+ number=7, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1188,
+ serialized_end=1362,
+)
+
+
+_CREATEHOTLISTRESPONSE = _descriptor.Descriptor(
+ name='CreateHotlistResponse',
+ full_name='monorail.CreateHotlistResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1364,
+ serialized_end=1387,
+)
+
+
+_CHECKHOTLISTNAMEREQUEST = _descriptor.Descriptor(
+ name='CheckHotlistNameRequest',
+ full_name='monorail.CheckHotlistNameRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.CheckHotlistNameRequest.name', index=0,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1389,
+ serialized_end=1428,
+)
+
+
+_CHECKHOTLISTNAMERESPONSE = _descriptor.Descriptor(
+ name='CheckHotlistNameResponse',
+ full_name='monorail.CheckHotlistNameResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='error', full_name='monorail.CheckHotlistNameResponse.error', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1430,
+ serialized_end=1471,
+)
+
+
+_REMOVEISSUESFROMHOTLISTSREQUEST = _descriptor.Descriptor(
+ name='RemoveIssuesFromHotlistsRequest',
+ full_name='monorail.RemoveIssuesFromHotlistsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='hotlist_refs', full_name='monorail.RemoveIssuesFromHotlistsRequest.hotlist_refs', index=0,
+ number=2, 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),
+ _descriptor.FieldDescriptor(
+ name='issue_refs', full_name='monorail.RemoveIssuesFromHotlistsRequest.issue_refs', index=1,
+ number=3, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1473,
+ serialized_end=1590,
+)
+
+
+_REMOVEISSUESFROMHOTLISTSRESPONSE = _descriptor.Descriptor(
+ name='RemoveIssuesFromHotlistsResponse',
+ full_name='monorail.RemoveIssuesFromHotlistsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1592,
+ serialized_end=1626,
+)
+
+
+_ADDISSUESTOHOTLISTSREQUEST = _descriptor.Descriptor(
+ name='AddIssuesToHotlistsRequest',
+ full_name='monorail.AddIssuesToHotlistsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='hotlist_refs', full_name='monorail.AddIssuesToHotlistsRequest.hotlist_refs', index=0,
+ number=2, 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),
+ _descriptor.FieldDescriptor(
+ name='issue_refs', full_name='monorail.AddIssuesToHotlistsRequest.issue_refs', index=1,
+ number=3, 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),
+ _descriptor.FieldDescriptor(
+ name='note', full_name='monorail.AddIssuesToHotlistsRequest.note', index=2,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1628,
+ serialized_end=1754,
+)
+
+
+_ADDISSUESTOHOTLISTSRESPONSE = _descriptor.Descriptor(
+ name='AddIssuesToHotlistsResponse',
+ full_name='monorail.AddIssuesToHotlistsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1756,
+ serialized_end=1785,
+)
+
+
+_RERANKHOTLISTISSUESREQUEST = _descriptor.Descriptor(
+ name='RerankHotlistIssuesRequest',
+ full_name='monorail.RerankHotlistIssuesRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='hotlist_ref', full_name='monorail.RerankHotlistIssuesRequest.hotlist_ref', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='moved_refs', full_name='monorail.RerankHotlistIssuesRequest.moved_refs', index=1,
+ number=2, 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),
+ _descriptor.FieldDescriptor(
+ name='target_ref', full_name='monorail.RerankHotlistIssuesRequest.target_ref', index=2,
+ number=3, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='split_above', full_name='monorail.RerankHotlistIssuesRequest.split_above', index=3,
+ number=4, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1788,
+ serialized_end=1960,
+)
+
+
+_RERANKHOTLISTISSUESRESPONSE = _descriptor.Descriptor(
+ name='RerankHotlistIssuesResponse',
+ full_name='monorail.RerankHotlistIssuesResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1962,
+ serialized_end=1991,
+)
+
+
+_UPDATEHOTLISTISSUENOTEREQUEST = _descriptor.Descriptor(
+ name='UpdateHotlistIssueNoteRequest',
+ full_name='monorail.UpdateHotlistIssueNoteRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='hotlist_ref', full_name='monorail.UpdateHotlistIssueNoteRequest.hotlist_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='issue_ref', full_name='monorail.UpdateHotlistIssueNoteRequest.issue_ref', index=1,
+ number=3, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='note', full_name='monorail.UpdateHotlistIssueNoteRequest.note', index=2,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1993,
+ serialized_end=2120,
+)
+
+
+_UPDATEHOTLISTISSUENOTERESPONSE = _descriptor.Descriptor(
+ name='UpdateHotlistIssueNoteResponse',
+ full_name='monorail.UpdateHotlistIssueNoteResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2122,
+ serialized_end=2154,
+)
+
+
+_DELETEHOTLISTREQUEST = _descriptor.Descriptor(
+ name='DeleteHotlistRequest',
+ full_name='monorail.DeleteHotlistRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='hotlist_ref', full_name='monorail.DeleteHotlistRequest.hotlist_ref', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2156,
+ serialized_end=2221,
+)
+
+
+_DELETEHOTLISTRESPONSE = _descriptor.Descriptor(
+ name='DeleteHotlistResponse',
+ full_name='monorail.DeleteHotlistResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2223,
+ serialized_end=2246,
+)
+
+
+_PREDICTCOMPONENTREQUEST = _descriptor.Descriptor(
+ name='PredictComponentRequest',
+ full_name='monorail.PredictComponentRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='text', full_name='monorail.PredictComponentRequest.text', index=0,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.PredictComponentRequest.project_name', index=1,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2248,
+ serialized_end=2309,
+)
+
+
+_PREDICTCOMPONENTRESPONSE = _descriptor.Descriptor(
+ name='PredictComponentResponse',
+ full_name='monorail.PredictComponentResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='component_ref', full_name='monorail.PredictComponentResponse.component_ref', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2311,
+ serialized_end=2384,
+)
+
+_LISTHOTLISTSBYUSERREQUEST.fields_by_name['user'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_LISTHOTLISTSBYUSERRESPONSE.fields_by_name['hotlists'].message_type = api_dot_api__proto_dot_features__objects__pb2._HOTLIST
+_LISTHOTLISTSBYISSUEREQUEST.fields_by_name['issue'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_LISTHOTLISTSBYISSUERESPONSE.fields_by_name['hotlists'].message_type = api_dot_api__proto_dot_features__objects__pb2._HOTLIST
+_LISTRECENTLYVISITEDHOTLISTSRESPONSE.fields_by_name['hotlists'].message_type = api_dot_api__proto_dot_features__objects__pb2._HOTLIST
+_LISTSTARREDHOTLISTSRESPONSE.fields_by_name['hotlists'].message_type = api_dot_api__proto_dot_features__objects__pb2._HOTLIST
+_GETHOTLISTSTARCOUNTREQUEST.fields_by_name['hotlist_ref'].message_type = api_dot_api__proto_dot_common__pb2._HOTLISTREF
+_STARHOTLISTREQUEST.fields_by_name['hotlist_ref'].message_type = api_dot_api__proto_dot_common__pb2._HOTLISTREF
+_GETHOTLISTREQUEST.fields_by_name['hotlist_ref'].message_type = api_dot_api__proto_dot_common__pb2._HOTLISTREF
+_GETHOTLISTRESPONSE.fields_by_name['hotlist'].message_type = api_dot_api__proto_dot_features__objects__pb2._HOTLIST
+_LISTHOTLISTITEMSREQUEST.fields_by_name['hotlist_ref'].message_type = api_dot_api__proto_dot_common__pb2._HOTLISTREF
+_LISTHOTLISTITEMSREQUEST.fields_by_name['pagination'].message_type = api_dot_api__proto_dot_common__pb2._PAGINATION
+_LISTHOTLISTITEMSRESPONSE.fields_by_name['items'].message_type = api_dot_api__proto_dot_features__objects__pb2._HOTLISTITEM
+_CREATEHOTLISTREQUEST.fields_by_name['editor_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_CREATEHOTLISTREQUEST.fields_by_name['issue_refs'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_REMOVEISSUESFROMHOTLISTSREQUEST.fields_by_name['hotlist_refs'].message_type = api_dot_api__proto_dot_common__pb2._HOTLISTREF
+_REMOVEISSUESFROMHOTLISTSREQUEST.fields_by_name['issue_refs'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_ADDISSUESTOHOTLISTSREQUEST.fields_by_name['hotlist_refs'].message_type = api_dot_api__proto_dot_common__pb2._HOTLISTREF
+_ADDISSUESTOHOTLISTSREQUEST.fields_by_name['issue_refs'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_RERANKHOTLISTISSUESREQUEST.fields_by_name['hotlist_ref'].message_type = api_dot_api__proto_dot_common__pb2._HOTLISTREF
+_RERANKHOTLISTISSUESREQUEST.fields_by_name['moved_refs'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_RERANKHOTLISTISSUESREQUEST.fields_by_name['target_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_UPDATEHOTLISTISSUENOTEREQUEST.fields_by_name['hotlist_ref'].message_type = api_dot_api__proto_dot_common__pb2._HOTLISTREF
+_UPDATEHOTLISTISSUENOTEREQUEST.fields_by_name['issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_DELETEHOTLISTREQUEST.fields_by_name['hotlist_ref'].message_type = api_dot_api__proto_dot_common__pb2._HOTLISTREF
+_PREDICTCOMPONENTRESPONSE.fields_by_name['component_ref'].message_type = api_dot_api__proto_dot_common__pb2._COMPONENTREF
+DESCRIPTOR.message_types_by_name['ListHotlistsByUserRequest'] = _LISTHOTLISTSBYUSERREQUEST
+DESCRIPTOR.message_types_by_name['ListHotlistsByUserResponse'] = _LISTHOTLISTSBYUSERRESPONSE
+DESCRIPTOR.message_types_by_name['ListHotlistsByIssueRequest'] = _LISTHOTLISTSBYISSUEREQUEST
+DESCRIPTOR.message_types_by_name['ListHotlistsByIssueResponse'] = _LISTHOTLISTSBYISSUERESPONSE
+DESCRIPTOR.message_types_by_name['ListRecentlyVisitedHotlistsRequest'] = _LISTRECENTLYVISITEDHOTLISTSREQUEST
+DESCRIPTOR.message_types_by_name['ListRecentlyVisitedHotlistsResponse'] = _LISTRECENTLYVISITEDHOTLISTSRESPONSE
+DESCRIPTOR.message_types_by_name['ListStarredHotlistsRequest'] = _LISTSTARREDHOTLISTSREQUEST
+DESCRIPTOR.message_types_by_name['ListStarredHotlistsResponse'] = _LISTSTARREDHOTLISTSRESPONSE
+DESCRIPTOR.message_types_by_name['GetHotlistStarCountRequest'] = _GETHOTLISTSTARCOUNTREQUEST
+DESCRIPTOR.message_types_by_name['GetHotlistStarCountResponse'] = _GETHOTLISTSTARCOUNTRESPONSE
+DESCRIPTOR.message_types_by_name['StarHotlistRequest'] = _STARHOTLISTREQUEST
+DESCRIPTOR.message_types_by_name['StarHotlistResponse'] = _STARHOTLISTRESPONSE
+DESCRIPTOR.message_types_by_name['GetHotlistRequest'] = _GETHOTLISTREQUEST
+DESCRIPTOR.message_types_by_name['GetHotlistResponse'] = _GETHOTLISTRESPONSE
+DESCRIPTOR.message_types_by_name['ListHotlistItemsRequest'] = _LISTHOTLISTITEMSREQUEST
+DESCRIPTOR.message_types_by_name['ListHotlistItemsResponse'] = _LISTHOTLISTITEMSRESPONSE
+DESCRIPTOR.message_types_by_name['CreateHotlistRequest'] = _CREATEHOTLISTREQUEST
+DESCRIPTOR.message_types_by_name['CreateHotlistResponse'] = _CREATEHOTLISTRESPONSE
+DESCRIPTOR.message_types_by_name['CheckHotlistNameRequest'] = _CHECKHOTLISTNAMEREQUEST
+DESCRIPTOR.message_types_by_name['CheckHotlistNameResponse'] = _CHECKHOTLISTNAMERESPONSE
+DESCRIPTOR.message_types_by_name['RemoveIssuesFromHotlistsRequest'] = _REMOVEISSUESFROMHOTLISTSREQUEST
+DESCRIPTOR.message_types_by_name['RemoveIssuesFromHotlistsResponse'] = _REMOVEISSUESFROMHOTLISTSRESPONSE
+DESCRIPTOR.message_types_by_name['AddIssuesToHotlistsRequest'] = _ADDISSUESTOHOTLISTSREQUEST
+DESCRIPTOR.message_types_by_name['AddIssuesToHotlistsResponse'] = _ADDISSUESTOHOTLISTSRESPONSE
+DESCRIPTOR.message_types_by_name['RerankHotlistIssuesRequest'] = _RERANKHOTLISTISSUESREQUEST
+DESCRIPTOR.message_types_by_name['RerankHotlistIssuesResponse'] = _RERANKHOTLISTISSUESRESPONSE
+DESCRIPTOR.message_types_by_name['UpdateHotlistIssueNoteRequest'] = _UPDATEHOTLISTISSUENOTEREQUEST
+DESCRIPTOR.message_types_by_name['UpdateHotlistIssueNoteResponse'] = _UPDATEHOTLISTISSUENOTERESPONSE
+DESCRIPTOR.message_types_by_name['DeleteHotlistRequest'] = _DELETEHOTLISTREQUEST
+DESCRIPTOR.message_types_by_name['DeleteHotlistResponse'] = _DELETEHOTLISTRESPONSE
+DESCRIPTOR.message_types_by_name['PredictComponentRequest'] = _PREDICTCOMPONENTREQUEST
+DESCRIPTOR.message_types_by_name['PredictComponentResponse'] = _PREDICTCOMPONENTRESPONSE
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+ListHotlistsByUserRequest = _reflection.GeneratedProtocolMessageType('ListHotlistsByUserRequest', (_message.Message,), dict(
+ DESCRIPTOR = _LISTHOTLISTSBYUSERREQUEST,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListHotlistsByUserRequest)
+ ))
+_sym_db.RegisterMessage(ListHotlistsByUserRequest)
+
+ListHotlistsByUserResponse = _reflection.GeneratedProtocolMessageType('ListHotlistsByUserResponse', (_message.Message,), dict(
+ DESCRIPTOR = _LISTHOTLISTSBYUSERRESPONSE,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListHotlistsByUserResponse)
+ ))
+_sym_db.RegisterMessage(ListHotlistsByUserResponse)
+
+ListHotlistsByIssueRequest = _reflection.GeneratedProtocolMessageType('ListHotlistsByIssueRequest', (_message.Message,), dict(
+ DESCRIPTOR = _LISTHOTLISTSBYISSUEREQUEST,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListHotlistsByIssueRequest)
+ ))
+_sym_db.RegisterMessage(ListHotlistsByIssueRequest)
+
+ListHotlistsByIssueResponse = _reflection.GeneratedProtocolMessageType('ListHotlistsByIssueResponse', (_message.Message,), dict(
+ DESCRIPTOR = _LISTHOTLISTSBYISSUERESPONSE,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListHotlistsByIssueResponse)
+ ))
+_sym_db.RegisterMessage(ListHotlistsByIssueResponse)
+
+ListRecentlyVisitedHotlistsRequest = _reflection.GeneratedProtocolMessageType('ListRecentlyVisitedHotlistsRequest', (_message.Message,), dict(
+ DESCRIPTOR = _LISTRECENTLYVISITEDHOTLISTSREQUEST,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListRecentlyVisitedHotlistsRequest)
+ ))
+_sym_db.RegisterMessage(ListRecentlyVisitedHotlistsRequest)
+
+ListRecentlyVisitedHotlistsResponse = _reflection.GeneratedProtocolMessageType('ListRecentlyVisitedHotlistsResponse', (_message.Message,), dict(
+ DESCRIPTOR = _LISTRECENTLYVISITEDHOTLISTSRESPONSE,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListRecentlyVisitedHotlistsResponse)
+ ))
+_sym_db.RegisterMessage(ListRecentlyVisitedHotlistsResponse)
+
+ListStarredHotlistsRequest = _reflection.GeneratedProtocolMessageType('ListStarredHotlistsRequest', (_message.Message,), dict(
+ DESCRIPTOR = _LISTSTARREDHOTLISTSREQUEST,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListStarredHotlistsRequest)
+ ))
+_sym_db.RegisterMessage(ListStarredHotlistsRequest)
+
+ListStarredHotlistsResponse = _reflection.GeneratedProtocolMessageType('ListStarredHotlistsResponse', (_message.Message,), dict(
+ DESCRIPTOR = _LISTSTARREDHOTLISTSRESPONSE,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListStarredHotlistsResponse)
+ ))
+_sym_db.RegisterMessage(ListStarredHotlistsResponse)
+
+GetHotlistStarCountRequest = _reflection.GeneratedProtocolMessageType('GetHotlistStarCountRequest', (_message.Message,), dict(
+ DESCRIPTOR = _GETHOTLISTSTARCOUNTREQUEST,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetHotlistStarCountRequest)
+ ))
+_sym_db.RegisterMessage(GetHotlistStarCountRequest)
+
+GetHotlistStarCountResponse = _reflection.GeneratedProtocolMessageType('GetHotlistStarCountResponse', (_message.Message,), dict(
+ DESCRIPTOR = _GETHOTLISTSTARCOUNTRESPONSE,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetHotlistStarCountResponse)
+ ))
+_sym_db.RegisterMessage(GetHotlistStarCountResponse)
+
+StarHotlistRequest = _reflection.GeneratedProtocolMessageType('StarHotlistRequest', (_message.Message,), dict(
+ DESCRIPTOR = _STARHOTLISTREQUEST,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.StarHotlistRequest)
+ ))
+_sym_db.RegisterMessage(StarHotlistRequest)
+
+StarHotlistResponse = _reflection.GeneratedProtocolMessageType('StarHotlistResponse', (_message.Message,), dict(
+ DESCRIPTOR = _STARHOTLISTRESPONSE,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.StarHotlistResponse)
+ ))
+_sym_db.RegisterMessage(StarHotlistResponse)
+
+GetHotlistRequest = _reflection.GeneratedProtocolMessageType('GetHotlistRequest', (_message.Message,), dict(
+ DESCRIPTOR = _GETHOTLISTREQUEST,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetHotlistRequest)
+ ))
+_sym_db.RegisterMessage(GetHotlistRequest)
+
+GetHotlistResponse = _reflection.GeneratedProtocolMessageType('GetHotlistResponse', (_message.Message,), dict(
+ DESCRIPTOR = _GETHOTLISTRESPONSE,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetHotlistResponse)
+ ))
+_sym_db.RegisterMessage(GetHotlistResponse)
+
+ListHotlistItemsRequest = _reflection.GeneratedProtocolMessageType('ListHotlistItemsRequest', (_message.Message,), dict(
+ DESCRIPTOR = _LISTHOTLISTITEMSREQUEST,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListHotlistItemsRequest)
+ ))
+_sym_db.RegisterMessage(ListHotlistItemsRequest)
+
+ListHotlistItemsResponse = _reflection.GeneratedProtocolMessageType('ListHotlistItemsResponse', (_message.Message,), dict(
+ DESCRIPTOR = _LISTHOTLISTITEMSRESPONSE,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListHotlistItemsResponse)
+ ))
+_sym_db.RegisterMessage(ListHotlistItemsResponse)
+
+CreateHotlistRequest = _reflection.GeneratedProtocolMessageType('CreateHotlistRequest', (_message.Message,), dict(
+ DESCRIPTOR = _CREATEHOTLISTREQUEST,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.CreateHotlistRequest)
+ ))
+_sym_db.RegisterMessage(CreateHotlistRequest)
+
+CreateHotlistResponse = _reflection.GeneratedProtocolMessageType('CreateHotlistResponse', (_message.Message,), dict(
+ DESCRIPTOR = _CREATEHOTLISTRESPONSE,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.CreateHotlistResponse)
+ ))
+_sym_db.RegisterMessage(CreateHotlistResponse)
+
+CheckHotlistNameRequest = _reflection.GeneratedProtocolMessageType('CheckHotlistNameRequest', (_message.Message,), dict(
+ DESCRIPTOR = _CHECKHOTLISTNAMEREQUEST,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.CheckHotlistNameRequest)
+ ))
+_sym_db.RegisterMessage(CheckHotlistNameRequest)
+
+CheckHotlistNameResponse = _reflection.GeneratedProtocolMessageType('CheckHotlistNameResponse', (_message.Message,), dict(
+ DESCRIPTOR = _CHECKHOTLISTNAMERESPONSE,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.CheckHotlistNameResponse)
+ ))
+_sym_db.RegisterMessage(CheckHotlistNameResponse)
+
+RemoveIssuesFromHotlistsRequest = _reflection.GeneratedProtocolMessageType('RemoveIssuesFromHotlistsRequest', (_message.Message,), dict(
+ DESCRIPTOR = _REMOVEISSUESFROMHOTLISTSREQUEST,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.RemoveIssuesFromHotlistsRequest)
+ ))
+_sym_db.RegisterMessage(RemoveIssuesFromHotlistsRequest)
+
+RemoveIssuesFromHotlistsResponse = _reflection.GeneratedProtocolMessageType('RemoveIssuesFromHotlistsResponse', (_message.Message,), dict(
+ DESCRIPTOR = _REMOVEISSUESFROMHOTLISTSRESPONSE,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.RemoveIssuesFromHotlistsResponse)
+ ))
+_sym_db.RegisterMessage(RemoveIssuesFromHotlistsResponse)
+
+AddIssuesToHotlistsRequest = _reflection.GeneratedProtocolMessageType('AddIssuesToHotlistsRequest', (_message.Message,), dict(
+ DESCRIPTOR = _ADDISSUESTOHOTLISTSREQUEST,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.AddIssuesToHotlistsRequest)
+ ))
+_sym_db.RegisterMessage(AddIssuesToHotlistsRequest)
+
+AddIssuesToHotlistsResponse = _reflection.GeneratedProtocolMessageType('AddIssuesToHotlistsResponse', (_message.Message,), dict(
+ DESCRIPTOR = _ADDISSUESTOHOTLISTSRESPONSE,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.AddIssuesToHotlistsResponse)
+ ))
+_sym_db.RegisterMessage(AddIssuesToHotlistsResponse)
+
+RerankHotlistIssuesRequest = _reflection.GeneratedProtocolMessageType('RerankHotlistIssuesRequest', (_message.Message,), dict(
+ DESCRIPTOR = _RERANKHOTLISTISSUESREQUEST,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.RerankHotlistIssuesRequest)
+ ))
+_sym_db.RegisterMessage(RerankHotlistIssuesRequest)
+
+RerankHotlistIssuesResponse = _reflection.GeneratedProtocolMessageType('RerankHotlistIssuesResponse', (_message.Message,), dict(
+ DESCRIPTOR = _RERANKHOTLISTISSUESRESPONSE,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.RerankHotlistIssuesResponse)
+ ))
+_sym_db.RegisterMessage(RerankHotlistIssuesResponse)
+
+UpdateHotlistIssueNoteRequest = _reflection.GeneratedProtocolMessageType('UpdateHotlistIssueNoteRequest', (_message.Message,), dict(
+ DESCRIPTOR = _UPDATEHOTLISTISSUENOTEREQUEST,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.UpdateHotlistIssueNoteRequest)
+ ))
+_sym_db.RegisterMessage(UpdateHotlistIssueNoteRequest)
+
+UpdateHotlistIssueNoteResponse = _reflection.GeneratedProtocolMessageType('UpdateHotlistIssueNoteResponse', (_message.Message,), dict(
+ DESCRIPTOR = _UPDATEHOTLISTISSUENOTERESPONSE,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.UpdateHotlistIssueNoteResponse)
+ ))
+_sym_db.RegisterMessage(UpdateHotlistIssueNoteResponse)
+
+DeleteHotlistRequest = _reflection.GeneratedProtocolMessageType('DeleteHotlistRequest', (_message.Message,), dict(
+ DESCRIPTOR = _DELETEHOTLISTREQUEST,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.DeleteHotlistRequest)
+ ))
+_sym_db.RegisterMessage(DeleteHotlistRequest)
+
+DeleteHotlistResponse = _reflection.GeneratedProtocolMessageType('DeleteHotlistResponse', (_message.Message,), dict(
+ DESCRIPTOR = _DELETEHOTLISTRESPONSE,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.DeleteHotlistResponse)
+ ))
+_sym_db.RegisterMessage(DeleteHotlistResponse)
+
+PredictComponentRequest = _reflection.GeneratedProtocolMessageType('PredictComponentRequest', (_message.Message,), dict(
+ DESCRIPTOR = _PREDICTCOMPONENTREQUEST,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.PredictComponentRequest)
+ ))
+_sym_db.RegisterMessage(PredictComponentRequest)
+
+PredictComponentResponse = _reflection.GeneratedProtocolMessageType('PredictComponentResponse', (_message.Message,), dict(
+ DESCRIPTOR = _PREDICTCOMPONENTRESPONSE,
+ __module__ = 'api.api_proto.features_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.PredictComponentResponse)
+ ))
+_sym_db.RegisterMessage(PredictComponentResponse)
+
+
+
+_FEATURES = _descriptor.ServiceDescriptor(
+ name='Features',
+ full_name='monorail.Features',
+ file=DESCRIPTOR,
+ index=0,
+ serialized_options=None,
+ serialized_start=2387,
+ serialized_end=3960,
+ methods=[
+ _descriptor.MethodDescriptor(
+ name='ListHotlistsByUser',
+ full_name='monorail.Features.ListHotlistsByUser',
+ index=0,
+ containing_service=None,
+ input_type=_LISTHOTLISTSBYUSERREQUEST,
+ output_type=_LISTHOTLISTSBYUSERRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListHotlistsByIssue',
+ full_name='monorail.Features.ListHotlistsByIssue',
+ index=1,
+ containing_service=None,
+ input_type=_LISTHOTLISTSBYISSUEREQUEST,
+ output_type=_LISTHOTLISTSBYISSUERESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListRecentlyVisitedHotlists',
+ full_name='monorail.Features.ListRecentlyVisitedHotlists',
+ index=2,
+ containing_service=None,
+ input_type=_LISTRECENTLYVISITEDHOTLISTSREQUEST,
+ output_type=_LISTRECENTLYVISITEDHOTLISTSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListStarredHotlists',
+ full_name='monorail.Features.ListStarredHotlists',
+ index=3,
+ containing_service=None,
+ input_type=_LISTSTARREDHOTLISTSREQUEST,
+ output_type=_LISTSTARREDHOTLISTSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='GetHotlistStarCount',
+ full_name='monorail.Features.GetHotlistStarCount',
+ index=4,
+ containing_service=None,
+ input_type=_GETHOTLISTSTARCOUNTREQUEST,
+ output_type=_GETHOTLISTSTARCOUNTRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='StarHotlist',
+ full_name='monorail.Features.StarHotlist',
+ index=5,
+ containing_service=None,
+ input_type=_STARHOTLISTREQUEST,
+ output_type=_STARHOTLISTRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='GetHotlist',
+ full_name='monorail.Features.GetHotlist',
+ index=6,
+ containing_service=None,
+ input_type=_GETHOTLISTREQUEST,
+ output_type=_GETHOTLISTRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListHotlistItems',
+ full_name='monorail.Features.ListHotlistItems',
+ index=7,
+ containing_service=None,
+ input_type=_LISTHOTLISTITEMSREQUEST,
+ output_type=_LISTHOTLISTITEMSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='CreateHotlist',
+ full_name='monorail.Features.CreateHotlist',
+ index=8,
+ containing_service=None,
+ input_type=_CREATEHOTLISTREQUEST,
+ output_type=_CREATEHOTLISTRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='CheckHotlistName',
+ full_name='monorail.Features.CheckHotlistName',
+ index=9,
+ containing_service=None,
+ input_type=_CHECKHOTLISTNAMEREQUEST,
+ output_type=_CHECKHOTLISTNAMERESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='RemoveIssuesFromHotlists',
+ full_name='monorail.Features.RemoveIssuesFromHotlists',
+ index=10,
+ containing_service=None,
+ input_type=_REMOVEISSUESFROMHOTLISTSREQUEST,
+ output_type=_REMOVEISSUESFROMHOTLISTSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='AddIssuesToHotlists',
+ full_name='monorail.Features.AddIssuesToHotlists',
+ index=11,
+ containing_service=None,
+ input_type=_ADDISSUESTOHOTLISTSREQUEST,
+ output_type=_ADDISSUESTOHOTLISTSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='RerankHotlistIssues',
+ full_name='monorail.Features.RerankHotlistIssues',
+ index=12,
+ containing_service=None,
+ input_type=_RERANKHOTLISTISSUESREQUEST,
+ output_type=_RERANKHOTLISTISSUESRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='UpdateHotlistIssueNote',
+ full_name='monorail.Features.UpdateHotlistIssueNote',
+ index=13,
+ containing_service=None,
+ input_type=_UPDATEHOTLISTISSUENOTEREQUEST,
+ output_type=_UPDATEHOTLISTISSUENOTERESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='DeleteHotlist',
+ full_name='monorail.Features.DeleteHotlist',
+ index=14,
+ containing_service=None,
+ input_type=_DELETEHOTLISTREQUEST,
+ output_type=_DELETEHOTLISTRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='PredictComponent',
+ full_name='monorail.Features.PredictComponent',
+ index=15,
+ containing_service=None,
+ input_type=_PREDICTCOMPONENTREQUEST,
+ output_type=_PREDICTCOMPONENTRESPONSE,
+ serialized_options=None,
+ ),
+])
+_sym_db.RegisterServiceDescriptor(_FEATURES)
+
+DESCRIPTOR.services_by_name['Features'] = _FEATURES
+
+# @@protoc_insertion_point(module_scope)
diff --git a/api/api_proto/features_prpc_pb2.py b/api/api_proto/features_prpc_pb2.py
new file mode 100644
index 0000000..17257e5
--- /dev/null
+++ b/api/api_proto/features_prpc_pb2.py
@@ -0,0 +1,274 @@
+# Generated by the pRPC protocol buffer compiler plugin. DO NOT EDIT!
+# source: api/api_proto/features.proto
+
+import base64
+import zlib
+
+from google.protobuf import descriptor_pb2
+
+# Includes description of the api/api_proto/features.proto and all of its transitive
+# dependencies. Includes source code info.
+FILE_DESCRIPTOR_SET = descriptor_pb2.FileDescriptorSet()
+FILE_DESCRIPTOR_SET.ParseFromString(zlib.decompress(base64.b64decode(
+ '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')))
+_INDEX = {
+ f.name: {
+ 'descriptor': f,
+ 'services': {s.name: s for s in f.service},
+ }
+ for f in FILE_DESCRIPTOR_SET.file
+}
+
+
+FeaturesServiceDescription = {
+ 'file_descriptor_set': FILE_DESCRIPTOR_SET,
+ 'file_descriptor': _INDEX[u'api/api_proto/features.proto']['descriptor'],
+ 'service_descriptor': _INDEX[u'api/api_proto/features.proto']['services'][u'Features'],
+}
diff --git a/api/api_proto/issue_objects.proto b/api/api_proto/issue_objects.proto
new file mode 100644
index 0000000..9343c98
--- /dev/null
+++ b/api/api_proto/issue_objects.proto
@@ -0,0 +1,207 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+// This file defines protobufs for issues and related business
+// objects, e.g., field values, comments, and attachments.
+
+syntax = "proto3";
+
+package monorail;
+
+import "google/protobuf/wrappers.proto";
+import "api/api_proto/common.proto";
+
+
+// Next available tag: 8
+message Approval {
+ FieldRef field_ref = 1;
+ repeated UserRef approver_refs = 2;
+ ApprovalStatus status = 3;
+ fixed32 set_on = 4;
+ UserRef setter_ref = 5;
+ PhaseRef phase_ref = 7;
+}
+
+
+// Next available tag: 8
+enum ApprovalStatus {
+ NOT_SET = 0;
+ NEEDS_REVIEW = 1;
+ NA = 2;
+ REVIEW_REQUESTED = 3;
+ REVIEW_STARTED = 4;
+ NEED_INFO = 5;
+ APPROVED = 6;
+ NOT_APPROVED = 7;
+}
+
+
+// This message is only suitable for displaying the amendment to users.
+// We don't currently offer structured amendments that client code can
+// reason about, field names can be ambiguous, and we don't have
+// old_value for most changes.
+// Next available tag: 4
+message Amendment {
+ // This may be the name of a built-in or custom field, or relative to
+ // an approval field name.
+ string field_name = 1;
+ // This may be a new value that overwrote the old value, e.g., "Assigned",
+ // or it may be a space-separated list of changes, e.g., "Size-L -Size-S".
+ string new_or_delta_value = 2;
+ // old_value is only used when the user changes the summary.
+ string old_value = 3;
+}
+
+
+// Next available tag: 9
+message Attachment {
+ uint64 attachment_id = 1;
+ string filename = 2;
+ uint64 size = 3; // Size in bytes.
+ string content_type = 4;
+ bool is_deleted = 5;
+ string thumbnail_url = 6;
+ string view_url = 7;
+ string download_url = 8;
+}
+
+
+// Next available tag: 16
+message Comment {
+ string project_name = 1;
+ uint32 local_id = 2;
+ uint32 sequence_num = 3;
+ bool is_deleted = 4;
+ UserRef commenter = 5;
+ fixed32 timestamp = 6;
+ string content = 7;
+ string inbound_message = 8;
+ repeated Amendment amendments = 9;
+ repeated Attachment attachments = 10;
+ FieldRef approval_ref = 11;
+ // If set, this comment is an issue description.
+ uint32 description_num = 12;
+ bool is_spam = 13;
+ bool can_delete = 14;
+ bool can_flag = 15;
+}
+
+
+// Next available tag: 5
+message FieldValue {
+ FieldRef field_ref = 1;
+ string value = 2;
+ bool is_derived = 3;
+ PhaseRef phase_ref = 4;
+}
+
+
+// Next available tag: 28
+message Issue {
+ string project_name = 1;
+ uint32 local_id = 2;
+ string summary = 3;
+ StatusRef status_ref = 4;
+ UserRef owner_ref = 5;
+ repeated UserRef cc_refs = 6;
+ repeated LabelRef label_refs = 7;
+ repeated ComponentRef component_refs = 8;
+ repeated IssueRef blocked_on_issue_refs = 9;
+ repeated IssueRef blocking_issue_refs = 10;
+ repeated IssueRef dangling_blocked_on_refs = 23;
+ repeated IssueRef dangling_blocking_refs = 24;
+ IssueRef merged_into_issue_ref = 11;
+ repeated FieldValue field_values = 12;
+ bool is_deleted = 13;
+ UserRef reporter_ref = 14;
+ fixed32 opened_timestamp = 15;
+ fixed32 closed_timestamp = 16;
+ fixed32 modified_timestamp = 17;
+ fixed32 component_modified_timestamp = 25;
+ fixed32 status_modified_timestamp = 26;
+ fixed32 owner_modified_timestamp = 27;
+ uint32 star_count = 18;
+ bool is_spam = 19;
+ uint32 attachment_count = 20;
+ repeated Approval approval_values = 21;
+ repeated PhaseDef phases = 22;
+}
+
+
+// Next available tag: 18
+message IssueDelta {
+ // Note: We use StringValue instead of string so that we can
+ // check if delta.HasField('status'). Proto3 only allows that
+ // for nested messages and provides "boxed" values for this purpose.
+ // In JSON, a StringValue is represented as a simple string.
+ google.protobuf.StringValue status = 1;
+ UserRef owner_ref = 2;
+ repeated UserRef cc_refs_add = 3;
+ repeated UserRef cc_refs_remove = 4;
+ repeated ComponentRef comp_refs_add = 5;
+ repeated ComponentRef comp_refs_remove = 6;
+ repeated LabelRef label_refs_add = 7;
+ repeated LabelRef label_refs_remove = 8;
+ repeated FieldValue field_vals_add = 9;
+ repeated FieldValue field_vals_remove = 10;
+ repeated FieldRef fields_clear = 11;
+ repeated IssueRef blocked_on_refs_add = 12;
+ repeated IssueRef blocked_on_refs_remove = 13;
+ repeated IssueRef blocking_refs_add = 14;
+ repeated IssueRef blocking_refs_remove = 15;
+ IssueRef merged_into_ref = 16;
+ google.protobuf.StringValue summary = 17;
+}
+
+
+// Next available tag: 7
+message ApprovalDelta {
+ ApprovalStatus status = 1;
+ repeated UserRef approver_refs_add = 2;
+ repeated UserRef approver_refs_remove = 3;
+ repeated FieldValue field_vals_add = 4;
+ repeated FieldValue field_vals_remove = 5;
+ repeated FieldRef fields_clear = 6;
+}
+
+
+// Next available tag: 3
+message AttachmentUpload {
+ string filename = 1;
+ bytes content = 2;
+}
+
+
+// Next available tag: 4
+message IssueSummary {
+ string project_name = 1;
+ uint32 local_id = 2;
+ string summary = 3;
+}
+
+
+// Next available tag: 3
+message PhaseDef {
+ PhaseRef phase_ref = 1;
+ uint32 rank = 2;
+}
+
+
+// Next available tag: 2
+message PhaseRef {
+ string phase_name = 1;
+}
+
+
+// Next available tag: 7
+enum SearchScope {
+ ALL = 0;
+ NEW = 1;
+ OPEN = 2;
+ OWNED = 3;
+ REPORTED = 4;
+ STARRED = 5;
+ TO_VERIFY = 6;
+}
diff --git a/api/api_proto/issue_objects_pb2.py b/api/api_proto/issue_objects_pb2.py
new file mode 100644
index 0000000..fa347e1
--- /dev/null
+++ b/api/api_proto/issue_objects_pb2.py
@@ -0,0 +1,1232 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/api_proto/issue_objects.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf.internal import enum_type_wrapper
+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()
+
+
+from google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2
+from api.api_proto import common_pb2 as api_dot_api__proto_dot_common__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='api/api_proto/issue_objects.proto',
+ package='monorail',
+ syntax='proto3',
+ serialized_options=None,
+ serialized_pb=_b('\n!api/api_proto/issue_objects.proto\x12\x08monorail\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x1a\x61pi/api_proto/common.proto\"\xe3\x01\n\x08\x41pproval\x12%\n\tfield_ref\x18\x01 \x01(\x0b\x32\x12.monorail.FieldRef\x12(\n\rapprover_refs\x18\x02 \x03(\x0b\x32\x11.monorail.UserRef\x12(\n\x06status\x18\x03 \x01(\x0e\x32\x18.monorail.ApprovalStatus\x12\x0e\n\x06set_on\x18\x04 \x01(\x07\x12%\n\nsetter_ref\x18\x05 \x01(\x0b\x32\x11.monorail.UserRef\x12%\n\tphase_ref\x18\x07 \x01(\x0b\x32\x12.monorail.PhaseRef\"N\n\tAmendment\x12\x12\n\nfield_name\x18\x01 \x01(\t\x12\x1a\n\x12new_or_delta_value\x18\x02 \x01(\t\x12\x11\n\told_value\x18\x03 \x01(\t\"\xac\x01\n\nAttachment\x12\x15\n\rattachment_id\x18\x01 \x01(\x04\x12\x10\n\x08\x66ilename\x18\x02 \x01(\t\x12\x0c\n\x04size\x18\x03 \x01(\x04\x12\x14\n\x0c\x63ontent_type\x18\x04 \x01(\t\x12\x12\n\nis_deleted\x18\x05 \x01(\x08\x12\x15\n\rthumbnail_url\x18\x06 \x01(\t\x12\x10\n\x08view_url\x18\x07 \x01(\t\x12\x14\n\x0c\x64ownload_url\x18\x08 \x01(\t\"\x8c\x03\n\x07\x43omment\x12\x14\n\x0cproject_name\x18\x01 \x01(\t\x12\x10\n\x08local_id\x18\x02 \x01(\r\x12\x14\n\x0csequence_num\x18\x03 \x01(\r\x12\x12\n\nis_deleted\x18\x04 \x01(\x08\x12$\n\tcommenter\x18\x05 \x01(\x0b\x32\x11.monorail.UserRef\x12\x11\n\ttimestamp\x18\x06 \x01(\x07\x12\x0f\n\x07\x63ontent\x18\x07 \x01(\t\x12\x17\n\x0finbound_message\x18\x08 \x01(\t\x12\'\n\namendments\x18\t \x03(\x0b\x32\x13.monorail.Amendment\x12)\n\x0b\x61ttachments\x18\n \x03(\x0b\x32\x14.monorail.Attachment\x12(\n\x0c\x61pproval_ref\x18\x0b \x01(\x0b\x32\x12.monorail.FieldRef\x12\x17\n\x0f\x64\x65scription_num\x18\x0c \x01(\r\x12\x0f\n\x07is_spam\x18\r \x01(\x08\x12\x12\n\ncan_delete\x18\x0e \x01(\x08\x12\x10\n\x08\x63\x61n_flag\x18\x0f \x01(\x08\"}\n\nFieldValue\x12%\n\tfield_ref\x18\x01 \x01(\x0b\x32\x12.monorail.FieldRef\x12\r\n\x05value\x18\x02 \x01(\t\x12\x12\n\nis_derived\x18\x03 \x01(\x08\x12%\n\tphase_ref\x18\x04 \x01(\x0b\x32\x12.monorail.PhaseRef\"\xc0\x07\n\x05Issue\x12\x14\n\x0cproject_name\x18\x01 \x01(\t\x12\x10\n\x08local_id\x18\x02 \x01(\r\x12\x0f\n\x07summary\x18\x03 \x01(\t\x12\'\n\nstatus_ref\x18\x04 \x01(\x0b\x32\x13.monorail.StatusRef\x12$\n\towner_ref\x18\x05 \x01(\x0b\x32\x11.monorail.UserRef\x12\"\n\x07\x63\x63_refs\x18\x06 \x03(\x0b\x32\x11.monorail.UserRef\x12&\n\nlabel_refs\x18\x07 \x03(\x0b\x32\x12.monorail.LabelRef\x12.\n\x0e\x63omponent_refs\x18\x08 \x03(\x0b\x32\x16.monorail.ComponentRef\x12\x31\n\x15\x62locked_on_issue_refs\x18\t \x03(\x0b\x32\x12.monorail.IssueRef\x12/\n\x13\x62locking_issue_refs\x18\n \x03(\x0b\x32\x12.monorail.IssueRef\x12\x34\n\x18\x64\x61ngling_blocked_on_refs\x18\x17 \x03(\x0b\x32\x12.monorail.IssueRef\x12\x32\n\x16\x64\x61ngling_blocking_refs\x18\x18 \x03(\x0b\x32\x12.monorail.IssueRef\x12\x31\n\x15merged_into_issue_ref\x18\x0b \x01(\x0b\x32\x12.monorail.IssueRef\x12*\n\x0c\x66ield_values\x18\x0c \x03(\x0b\x32\x14.monorail.FieldValue\x12\x12\n\nis_deleted\x18\r \x01(\x08\x12\'\n\x0creporter_ref\x18\x0e \x01(\x0b\x32\x11.monorail.UserRef\x12\x18\n\x10opened_timestamp\x18\x0f \x01(\x07\x12\x18\n\x10\x63losed_timestamp\x18\x10 \x01(\x07\x12\x1a\n\x12modified_timestamp\x18\x11 \x01(\x07\x12$\n\x1c\x63omponent_modified_timestamp\x18\x19 \x01(\x07\x12!\n\x19status_modified_timestamp\x18\x1a \x01(\x07\x12 \n\x18owner_modified_timestamp\x18\x1b \x01(\x07\x12\x12\n\nstar_count\x18\x12 \x01(\r\x12\x0f\n\x07is_spam\x18\x13 \x01(\x08\x12\x18\n\x10\x61ttachment_count\x18\x14 \x01(\r\x12+\n\x0f\x61pproval_values\x18\x15 \x03(\x0b\x32\x12.monorail.Approval\x12\"\n\x06phases\x18\x16 \x03(\x0b\x32\x12.monorail.PhaseDef\"\x9a\x06\n\nIssueDelta\x12,\n\x06status\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12$\n\towner_ref\x18\x02 \x01(\x0b\x32\x11.monorail.UserRef\x12&\n\x0b\x63\x63_refs_add\x18\x03 \x03(\x0b\x32\x11.monorail.UserRef\x12)\n\x0e\x63\x63_refs_remove\x18\x04 \x03(\x0b\x32\x11.monorail.UserRef\x12-\n\rcomp_refs_add\x18\x05 \x03(\x0b\x32\x16.monorail.ComponentRef\x12\x30\n\x10\x63omp_refs_remove\x18\x06 \x03(\x0b\x32\x16.monorail.ComponentRef\x12*\n\x0elabel_refs_add\x18\x07 \x03(\x0b\x32\x12.monorail.LabelRef\x12-\n\x11label_refs_remove\x18\x08 \x03(\x0b\x32\x12.monorail.LabelRef\x12,\n\x0e\x66ield_vals_add\x18\t \x03(\x0b\x32\x14.monorail.FieldValue\x12/\n\x11\x66ield_vals_remove\x18\n \x03(\x0b\x32\x14.monorail.FieldValue\x12(\n\x0c\x66ields_clear\x18\x0b \x03(\x0b\x32\x12.monorail.FieldRef\x12/\n\x13\x62locked_on_refs_add\x18\x0c \x03(\x0b\x32\x12.monorail.IssueRef\x12\x32\n\x16\x62locked_on_refs_remove\x18\r \x03(\x0b\x32\x12.monorail.IssueRef\x12-\n\x11\x62locking_refs_add\x18\x0e \x03(\x0b\x32\x12.monorail.IssueRef\x12\x30\n\x14\x62locking_refs_remove\x18\x0f \x03(\x0b\x32\x12.monorail.IssueRef\x12+\n\x0fmerged_into_ref\x18\x10 \x01(\x0b\x32\x12.monorail.IssueRef\x12-\n\x07summary\x18\x11 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa1\x02\n\rApprovalDelta\x12(\n\x06status\x18\x01 \x01(\x0e\x32\x18.monorail.ApprovalStatus\x12,\n\x11\x61pprover_refs_add\x18\x02 \x03(\x0b\x32\x11.monorail.UserRef\x12/\n\x14\x61pprover_refs_remove\x18\x03 \x03(\x0b\x32\x11.monorail.UserRef\x12,\n\x0e\x66ield_vals_add\x18\x04 \x03(\x0b\x32\x14.monorail.FieldValue\x12/\n\x11\x66ield_vals_remove\x18\x05 \x03(\x0b\x32\x14.monorail.FieldValue\x12(\n\x0c\x66ields_clear\x18\x06 \x03(\x0b\x32\x12.monorail.FieldRef\"5\n\x10\x41ttachmentUpload\x12\x10\n\x08\x66ilename\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\x0c\"G\n\x0cIssueSummary\x12\x14\n\x0cproject_name\x18\x01 \x01(\t\x12\x10\n\x08local_id\x18\x02 \x01(\r\x12\x0f\n\x07summary\x18\x03 \x01(\t\"?\n\x08PhaseDef\x12%\n\tphase_ref\x18\x01 \x01(\x0b\x32\x12.monorail.PhaseRef\x12\x0c\n\x04rank\x18\x02 \x01(\r\"\x1e\n\x08PhaseRef\x12\x12\n\nphase_name\x18\x01 \x01(\t*\x90\x01\n\x0e\x41pprovalStatus\x12\x0b\n\x07NOT_SET\x10\x00\x12\x10\n\x0cNEEDS_REVIEW\x10\x01\x12\x06\n\x02NA\x10\x02\x12\x14\n\x10REVIEW_REQUESTED\x10\x03\x12\x12\n\x0eREVIEW_STARTED\x10\x04\x12\r\n\tNEED_INFO\x10\x05\x12\x0c\n\x08\x41PPROVED\x10\x06\x12\x10\n\x0cNOT_APPROVED\x10\x07*^\n\x0bSearchScope\x12\x07\n\x03\x41LL\x10\x00\x12\x07\n\x03NEW\x10\x01\x12\x08\n\x04OPEN\x10\x02\x12\t\n\x05OWNED\x10\x03\x12\x0c\n\x08REPORTED\x10\x04\x12\x0b\n\x07STARRED\x10\x05\x12\r\n\tTO_VERIFY\x10\x06\x62\x06proto3')
+ ,
+ dependencies=[google_dot_protobuf_dot_wrappers__pb2.DESCRIPTOR,api_dot_api__proto_dot_common__pb2.DESCRIPTOR,])
+
+_APPROVALSTATUS = _descriptor.EnumDescriptor(
+ name='ApprovalStatus',
+ full_name='monorail.ApprovalStatus',
+ filename=None,
+ file=DESCRIPTOR,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='NOT_SET', index=0, number=0,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='NEEDS_REVIEW', index=1, number=1,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='NA', index=2, number=2,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='REVIEW_REQUESTED', index=3, number=3,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='REVIEW_STARTED', index=4, number=4,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='NEED_INFO', index=5, number=5,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='APPROVED', index=6, number=6,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='NOT_APPROVED', index=7, number=7,
+ serialized_options=None,
+ type=None),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=3396,
+ serialized_end=3540,
+)
+_sym_db.RegisterEnumDescriptor(_APPROVALSTATUS)
+
+ApprovalStatus = enum_type_wrapper.EnumTypeWrapper(_APPROVALSTATUS)
+_SEARCHSCOPE = _descriptor.EnumDescriptor(
+ name='SearchScope',
+ full_name='monorail.SearchScope',
+ filename=None,
+ file=DESCRIPTOR,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='ALL', index=0, number=0,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='NEW', index=1, number=1,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='OPEN', index=2, number=2,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='OWNED', index=3, number=3,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='REPORTED', index=4, number=4,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='STARRED', index=5, number=5,
+ serialized_options=None,
+ type=None),
+ _descriptor.EnumValueDescriptor(
+ name='TO_VERIFY', index=6, number=6,
+ serialized_options=None,
+ type=None),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=3542,
+ serialized_end=3636,
+)
+_sym_db.RegisterEnumDescriptor(_SEARCHSCOPE)
+
+SearchScope = enum_type_wrapper.EnumTypeWrapper(_SEARCHSCOPE)
+NOT_SET = 0
+NEEDS_REVIEW = 1
+NA = 2
+REVIEW_REQUESTED = 3
+REVIEW_STARTED = 4
+NEED_INFO = 5
+APPROVED = 6
+NOT_APPROVED = 7
+ALL = 0
+NEW = 1
+OPEN = 2
+OWNED = 3
+REPORTED = 4
+STARRED = 5
+TO_VERIFY = 6
+
+
+
+_APPROVAL = _descriptor.Descriptor(
+ name='Approval',
+ full_name='monorail.Approval',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='field_ref', full_name='monorail.Approval.field_ref', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='approver_refs', full_name='monorail.Approval.approver_refs', index=1,
+ number=2, 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),
+ _descriptor.FieldDescriptor(
+ name='status', full_name='monorail.Approval.status', index=2,
+ number=3, type=14, cpp_type=8, 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),
+ _descriptor.FieldDescriptor(
+ name='set_on', full_name='monorail.Approval.set_on', index=3,
+ number=4, type=7, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='setter_ref', full_name='monorail.Approval.setter_ref', index=4,
+ number=5, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='phase_ref', full_name='monorail.Approval.phase_ref', index=5,
+ number=7, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=108,
+ serialized_end=335,
+)
+
+
+_AMENDMENT = _descriptor.Descriptor(
+ name='Amendment',
+ full_name='monorail.Amendment',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='field_name', full_name='monorail.Amendment.field_name', 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),
+ _descriptor.FieldDescriptor(
+ name='new_or_delta_value', full_name='monorail.Amendment.new_or_delta_value', 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),
+ _descriptor.FieldDescriptor(
+ name='old_value', full_name='monorail.Amendment.old_value', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=337,
+ serialized_end=415,
+)
+
+
+_ATTACHMENT = _descriptor.Descriptor(
+ name='Attachment',
+ full_name='monorail.Attachment',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='attachment_id', full_name='monorail.Attachment.attachment_id', index=0,
+ number=1, type=4, cpp_type=4, 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),
+ _descriptor.FieldDescriptor(
+ name='filename', full_name='monorail.Attachment.filename', 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),
+ _descriptor.FieldDescriptor(
+ name='size', full_name='monorail.Attachment.size', index=2,
+ number=3, type=4, cpp_type=4, 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),
+ _descriptor.FieldDescriptor(
+ name='content_type', full_name='monorail.Attachment.content_type', 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),
+ _descriptor.FieldDescriptor(
+ name='is_deleted', full_name='monorail.Attachment.is_deleted', index=4,
+ number=5, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='thumbnail_url', full_name='monorail.Attachment.thumbnail_url', 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),
+ _descriptor.FieldDescriptor(
+ name='view_url', full_name='monorail.Attachment.view_url', index=6,
+ number=7, 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),
+ _descriptor.FieldDescriptor(
+ name='download_url', full_name='monorail.Attachment.download_url', index=7,
+ number=8, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=418,
+ serialized_end=590,
+)
+
+
+_COMMENT = _descriptor.Descriptor(
+ name='Comment',
+ full_name='monorail.Comment',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.Comment.project_name', 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),
+ _descriptor.FieldDescriptor(
+ name='local_id', full_name='monorail.Comment.local_id', index=1,
+ number=2, type=13, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='sequence_num', full_name='monorail.Comment.sequence_num', index=2,
+ number=3, type=13, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='is_deleted', full_name='monorail.Comment.is_deleted', index=3,
+ number=4, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='commenter', full_name='monorail.Comment.commenter', index=4,
+ number=5, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='timestamp', full_name='monorail.Comment.timestamp', index=5,
+ number=6, type=7, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='content', full_name='monorail.Comment.content', index=6,
+ number=7, 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),
+ _descriptor.FieldDescriptor(
+ name='inbound_message', full_name='monorail.Comment.inbound_message', index=7,
+ number=8, 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),
+ _descriptor.FieldDescriptor(
+ name='amendments', full_name='monorail.Comment.amendments', index=8,
+ number=9, 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),
+ _descriptor.FieldDescriptor(
+ name='attachments', full_name='monorail.Comment.attachments', index=9,
+ number=10, 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),
+ _descriptor.FieldDescriptor(
+ name='approval_ref', full_name='monorail.Comment.approval_ref', index=10,
+ number=11, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='description_num', full_name='monorail.Comment.description_num', index=11,
+ number=12, type=13, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='is_spam', full_name='monorail.Comment.is_spam', index=12,
+ number=13, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='can_delete', full_name='monorail.Comment.can_delete', index=13,
+ number=14, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='can_flag', full_name='monorail.Comment.can_flag', index=14,
+ number=15, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=593,
+ serialized_end=989,
+)
+
+
+_FIELDVALUE = _descriptor.Descriptor(
+ name='FieldValue',
+ full_name='monorail.FieldValue',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='field_ref', full_name='monorail.FieldValue.field_ref', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='value', full_name='monorail.FieldValue.value', 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),
+ _descriptor.FieldDescriptor(
+ name='is_derived', full_name='monorail.FieldValue.is_derived', index=2,
+ number=3, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='phase_ref', full_name='monorail.FieldValue.phase_ref', index=3,
+ number=4, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=991,
+ serialized_end=1116,
+)
+
+
+_ISSUE = _descriptor.Descriptor(
+ name='Issue',
+ full_name='monorail.Issue',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.Issue.project_name', 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),
+ _descriptor.FieldDescriptor(
+ name='local_id', full_name='monorail.Issue.local_id', index=1,
+ number=2, type=13, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='summary', full_name='monorail.Issue.summary', 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),
+ _descriptor.FieldDescriptor(
+ name='status_ref', full_name='monorail.Issue.status_ref', index=3,
+ number=4, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='owner_ref', full_name='monorail.Issue.owner_ref', index=4,
+ number=5, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='cc_refs', full_name='monorail.Issue.cc_refs', index=5,
+ number=6, 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),
+ _descriptor.FieldDescriptor(
+ name='label_refs', full_name='monorail.Issue.label_refs', index=6,
+ number=7, 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),
+ _descriptor.FieldDescriptor(
+ name='component_refs', full_name='monorail.Issue.component_refs', index=7,
+ number=8, 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),
+ _descriptor.FieldDescriptor(
+ name='blocked_on_issue_refs', full_name='monorail.Issue.blocked_on_issue_refs', index=8,
+ number=9, 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),
+ _descriptor.FieldDescriptor(
+ name='blocking_issue_refs', full_name='monorail.Issue.blocking_issue_refs', index=9,
+ number=10, 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),
+ _descriptor.FieldDescriptor(
+ name='dangling_blocked_on_refs', full_name='monorail.Issue.dangling_blocked_on_refs', index=10,
+ number=23, 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),
+ _descriptor.FieldDescriptor(
+ name='dangling_blocking_refs', full_name='monorail.Issue.dangling_blocking_refs', index=11,
+ number=24, 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),
+ _descriptor.FieldDescriptor(
+ name='merged_into_issue_ref', full_name='monorail.Issue.merged_into_issue_ref', index=12,
+ number=11, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='field_values', full_name='monorail.Issue.field_values', index=13,
+ number=12, 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),
+ _descriptor.FieldDescriptor(
+ name='is_deleted', full_name='monorail.Issue.is_deleted', index=14,
+ number=13, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='reporter_ref', full_name='monorail.Issue.reporter_ref', index=15,
+ number=14, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='opened_timestamp', full_name='monorail.Issue.opened_timestamp', index=16,
+ number=15, type=7, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='closed_timestamp', full_name='monorail.Issue.closed_timestamp', index=17,
+ number=16, type=7, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='modified_timestamp', full_name='monorail.Issue.modified_timestamp', index=18,
+ number=17, type=7, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='component_modified_timestamp', full_name='monorail.Issue.component_modified_timestamp', index=19,
+ number=25, type=7, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='status_modified_timestamp', full_name='monorail.Issue.status_modified_timestamp', index=20,
+ number=26, type=7, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='owner_modified_timestamp', full_name='monorail.Issue.owner_modified_timestamp', index=21,
+ number=27, type=7, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='star_count', full_name='monorail.Issue.star_count', index=22,
+ number=18, type=13, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='is_spam', full_name='monorail.Issue.is_spam', index=23,
+ number=19, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='attachment_count', full_name='monorail.Issue.attachment_count', index=24,
+ number=20, type=13, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='approval_values', full_name='monorail.Issue.approval_values', index=25,
+ number=21, 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),
+ _descriptor.FieldDescriptor(
+ name='phases', full_name='monorail.Issue.phases', index=26,
+ number=22, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1119,
+ serialized_end=2079,
+)
+
+
+_ISSUEDELTA = _descriptor.Descriptor(
+ name='IssueDelta',
+ full_name='monorail.IssueDelta',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='status', full_name='monorail.IssueDelta.status', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='owner_ref', full_name='monorail.IssueDelta.owner_ref', index=1,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='cc_refs_add', full_name='monorail.IssueDelta.cc_refs_add', index=2,
+ number=3, 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),
+ _descriptor.FieldDescriptor(
+ name='cc_refs_remove', full_name='monorail.IssueDelta.cc_refs_remove', index=3,
+ number=4, 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),
+ _descriptor.FieldDescriptor(
+ name='comp_refs_add', full_name='monorail.IssueDelta.comp_refs_add', index=4,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='comp_refs_remove', full_name='monorail.IssueDelta.comp_refs_remove', index=5,
+ number=6, 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),
+ _descriptor.FieldDescriptor(
+ name='label_refs_add', full_name='monorail.IssueDelta.label_refs_add', index=6,
+ number=7, 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),
+ _descriptor.FieldDescriptor(
+ name='label_refs_remove', full_name='monorail.IssueDelta.label_refs_remove', index=7,
+ number=8, 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),
+ _descriptor.FieldDescriptor(
+ name='field_vals_add', full_name='monorail.IssueDelta.field_vals_add', index=8,
+ number=9, 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),
+ _descriptor.FieldDescriptor(
+ name='field_vals_remove', full_name='monorail.IssueDelta.field_vals_remove', index=9,
+ number=10, 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),
+ _descriptor.FieldDescriptor(
+ name='fields_clear', full_name='monorail.IssueDelta.fields_clear', index=10,
+ number=11, 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),
+ _descriptor.FieldDescriptor(
+ name='blocked_on_refs_add', full_name='monorail.IssueDelta.blocked_on_refs_add', index=11,
+ number=12, 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),
+ _descriptor.FieldDescriptor(
+ name='blocked_on_refs_remove', full_name='monorail.IssueDelta.blocked_on_refs_remove', index=12,
+ number=13, 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),
+ _descriptor.FieldDescriptor(
+ name='blocking_refs_add', full_name='monorail.IssueDelta.blocking_refs_add', index=13,
+ number=14, 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),
+ _descriptor.FieldDescriptor(
+ name='blocking_refs_remove', full_name='monorail.IssueDelta.blocking_refs_remove', index=14,
+ number=15, 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),
+ _descriptor.FieldDescriptor(
+ name='merged_into_ref', full_name='monorail.IssueDelta.merged_into_ref', index=15,
+ number=16, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='summary', full_name='monorail.IssueDelta.summary', index=16,
+ number=17, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2082,
+ serialized_end=2876,
+)
+
+
+_APPROVALDELTA = _descriptor.Descriptor(
+ name='ApprovalDelta',
+ full_name='monorail.ApprovalDelta',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='status', full_name='monorail.ApprovalDelta.status', index=0,
+ number=1, type=14, cpp_type=8, 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),
+ _descriptor.FieldDescriptor(
+ name='approver_refs_add', full_name='monorail.ApprovalDelta.approver_refs_add', index=1,
+ number=2, 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),
+ _descriptor.FieldDescriptor(
+ name='approver_refs_remove', full_name='monorail.ApprovalDelta.approver_refs_remove', index=2,
+ number=3, 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),
+ _descriptor.FieldDescriptor(
+ name='field_vals_add', full_name='monorail.ApprovalDelta.field_vals_add', index=3,
+ number=4, 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),
+ _descriptor.FieldDescriptor(
+ name='field_vals_remove', full_name='monorail.ApprovalDelta.field_vals_remove', index=4,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='fields_clear', full_name='monorail.ApprovalDelta.fields_clear', index=5,
+ number=6, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2879,
+ serialized_end=3168,
+)
+
+
+_ATTACHMENTUPLOAD = _descriptor.Descriptor(
+ name='AttachmentUpload',
+ full_name='monorail.AttachmentUpload',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='filename', full_name='monorail.AttachmentUpload.filename', 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),
+ _descriptor.FieldDescriptor(
+ name='content', full_name='monorail.AttachmentUpload.content', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=3170,
+ serialized_end=3223,
+)
+
+
+_ISSUESUMMARY = _descriptor.Descriptor(
+ name='IssueSummary',
+ full_name='monorail.IssueSummary',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.IssueSummary.project_name', 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),
+ _descriptor.FieldDescriptor(
+ name='local_id', full_name='monorail.IssueSummary.local_id', index=1,
+ number=2, type=13, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='summary', full_name='monorail.IssueSummary.summary', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=3225,
+ serialized_end=3296,
+)
+
+
+_PHASEDEF = _descriptor.Descriptor(
+ name='PhaseDef',
+ full_name='monorail.PhaseDef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='phase_ref', full_name='monorail.PhaseDef.phase_ref', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='rank', full_name='monorail.PhaseDef.rank', index=1,
+ number=2, type=13, cpp_type=3, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=3298,
+ serialized_end=3361,
+)
+
+
+_PHASEREF = _descriptor.Descriptor(
+ name='PhaseRef',
+ full_name='monorail.PhaseRef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='phase_name', full_name='monorail.PhaseRef.phase_name', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=3363,
+ serialized_end=3393,
+)
+
+_APPROVAL.fields_by_name['field_ref'].message_type = api_dot_api__proto_dot_common__pb2._FIELDREF
+_APPROVAL.fields_by_name['approver_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_APPROVAL.fields_by_name['status'].enum_type = _APPROVALSTATUS
+_APPROVAL.fields_by_name['setter_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_APPROVAL.fields_by_name['phase_ref'].message_type = _PHASEREF
+_COMMENT.fields_by_name['commenter'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_COMMENT.fields_by_name['amendments'].message_type = _AMENDMENT
+_COMMENT.fields_by_name['attachments'].message_type = _ATTACHMENT
+_COMMENT.fields_by_name['approval_ref'].message_type = api_dot_api__proto_dot_common__pb2._FIELDREF
+_FIELDVALUE.fields_by_name['field_ref'].message_type = api_dot_api__proto_dot_common__pb2._FIELDREF
+_FIELDVALUE.fields_by_name['phase_ref'].message_type = _PHASEREF
+_ISSUE.fields_by_name['status_ref'].message_type = api_dot_api__proto_dot_common__pb2._STATUSREF
+_ISSUE.fields_by_name['owner_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_ISSUE.fields_by_name['cc_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_ISSUE.fields_by_name['label_refs'].message_type = api_dot_api__proto_dot_common__pb2._LABELREF
+_ISSUE.fields_by_name['component_refs'].message_type = api_dot_api__proto_dot_common__pb2._COMPONENTREF
+_ISSUE.fields_by_name['blocked_on_issue_refs'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_ISSUE.fields_by_name['blocking_issue_refs'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_ISSUE.fields_by_name['dangling_blocked_on_refs'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_ISSUE.fields_by_name['dangling_blocking_refs'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_ISSUE.fields_by_name['merged_into_issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_ISSUE.fields_by_name['field_values'].message_type = _FIELDVALUE
+_ISSUE.fields_by_name['reporter_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_ISSUE.fields_by_name['approval_values'].message_type = _APPROVAL
+_ISSUE.fields_by_name['phases'].message_type = _PHASEDEF
+_ISSUEDELTA.fields_by_name['status'].message_type = google_dot_protobuf_dot_wrappers__pb2._STRINGVALUE
+_ISSUEDELTA.fields_by_name['owner_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_ISSUEDELTA.fields_by_name['cc_refs_add'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_ISSUEDELTA.fields_by_name['cc_refs_remove'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_ISSUEDELTA.fields_by_name['comp_refs_add'].message_type = api_dot_api__proto_dot_common__pb2._COMPONENTREF
+_ISSUEDELTA.fields_by_name['comp_refs_remove'].message_type = api_dot_api__proto_dot_common__pb2._COMPONENTREF
+_ISSUEDELTA.fields_by_name['label_refs_add'].message_type = api_dot_api__proto_dot_common__pb2._LABELREF
+_ISSUEDELTA.fields_by_name['label_refs_remove'].message_type = api_dot_api__proto_dot_common__pb2._LABELREF
+_ISSUEDELTA.fields_by_name['field_vals_add'].message_type = _FIELDVALUE
+_ISSUEDELTA.fields_by_name['field_vals_remove'].message_type = _FIELDVALUE
+_ISSUEDELTA.fields_by_name['fields_clear'].message_type = api_dot_api__proto_dot_common__pb2._FIELDREF
+_ISSUEDELTA.fields_by_name['blocked_on_refs_add'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_ISSUEDELTA.fields_by_name['blocked_on_refs_remove'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_ISSUEDELTA.fields_by_name['blocking_refs_add'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_ISSUEDELTA.fields_by_name['blocking_refs_remove'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_ISSUEDELTA.fields_by_name['merged_into_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_ISSUEDELTA.fields_by_name['summary'].message_type = google_dot_protobuf_dot_wrappers__pb2._STRINGVALUE
+_APPROVALDELTA.fields_by_name['status'].enum_type = _APPROVALSTATUS
+_APPROVALDELTA.fields_by_name['approver_refs_add'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_APPROVALDELTA.fields_by_name['approver_refs_remove'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_APPROVALDELTA.fields_by_name['field_vals_add'].message_type = _FIELDVALUE
+_APPROVALDELTA.fields_by_name['field_vals_remove'].message_type = _FIELDVALUE
+_APPROVALDELTA.fields_by_name['fields_clear'].message_type = api_dot_api__proto_dot_common__pb2._FIELDREF
+_PHASEDEF.fields_by_name['phase_ref'].message_type = _PHASEREF
+DESCRIPTOR.message_types_by_name['Approval'] = _APPROVAL
+DESCRIPTOR.message_types_by_name['Amendment'] = _AMENDMENT
+DESCRIPTOR.message_types_by_name['Attachment'] = _ATTACHMENT
+DESCRIPTOR.message_types_by_name['Comment'] = _COMMENT
+DESCRIPTOR.message_types_by_name['FieldValue'] = _FIELDVALUE
+DESCRIPTOR.message_types_by_name['Issue'] = _ISSUE
+DESCRIPTOR.message_types_by_name['IssueDelta'] = _ISSUEDELTA
+DESCRIPTOR.message_types_by_name['ApprovalDelta'] = _APPROVALDELTA
+DESCRIPTOR.message_types_by_name['AttachmentUpload'] = _ATTACHMENTUPLOAD
+DESCRIPTOR.message_types_by_name['IssueSummary'] = _ISSUESUMMARY
+DESCRIPTOR.message_types_by_name['PhaseDef'] = _PHASEDEF
+DESCRIPTOR.message_types_by_name['PhaseRef'] = _PHASEREF
+DESCRIPTOR.enum_types_by_name['ApprovalStatus'] = _APPROVALSTATUS
+DESCRIPTOR.enum_types_by_name['SearchScope'] = _SEARCHSCOPE
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+Approval = _reflection.GeneratedProtocolMessageType('Approval', (_message.Message,), dict(
+ DESCRIPTOR = _APPROVAL,
+ __module__ = 'api.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.Approval)
+ ))
+_sym_db.RegisterMessage(Approval)
+
+Amendment = _reflection.GeneratedProtocolMessageType('Amendment', (_message.Message,), dict(
+ DESCRIPTOR = _AMENDMENT,
+ __module__ = 'api.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.Amendment)
+ ))
+_sym_db.RegisterMessage(Amendment)
+
+Attachment = _reflection.GeneratedProtocolMessageType('Attachment', (_message.Message,), dict(
+ DESCRIPTOR = _ATTACHMENT,
+ __module__ = 'api.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.Attachment)
+ ))
+_sym_db.RegisterMessage(Attachment)
+
+Comment = _reflection.GeneratedProtocolMessageType('Comment', (_message.Message,), dict(
+ DESCRIPTOR = _COMMENT,
+ __module__ = 'api.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.Comment)
+ ))
+_sym_db.RegisterMessage(Comment)
+
+FieldValue = _reflection.GeneratedProtocolMessageType('FieldValue', (_message.Message,), dict(
+ DESCRIPTOR = _FIELDVALUE,
+ __module__ = 'api.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.FieldValue)
+ ))
+_sym_db.RegisterMessage(FieldValue)
+
+Issue = _reflection.GeneratedProtocolMessageType('Issue', (_message.Message,), dict(
+ DESCRIPTOR = _ISSUE,
+ __module__ = 'api.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.Issue)
+ ))
+_sym_db.RegisterMessage(Issue)
+
+IssueDelta = _reflection.GeneratedProtocolMessageType('IssueDelta', (_message.Message,), dict(
+ DESCRIPTOR = _ISSUEDELTA,
+ __module__ = 'api.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.IssueDelta)
+ ))
+_sym_db.RegisterMessage(IssueDelta)
+
+ApprovalDelta = _reflection.GeneratedProtocolMessageType('ApprovalDelta', (_message.Message,), dict(
+ DESCRIPTOR = _APPROVALDELTA,
+ __module__ = 'api.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ApprovalDelta)
+ ))
+_sym_db.RegisterMessage(ApprovalDelta)
+
+AttachmentUpload = _reflection.GeneratedProtocolMessageType('AttachmentUpload', (_message.Message,), dict(
+ DESCRIPTOR = _ATTACHMENTUPLOAD,
+ __module__ = 'api.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.AttachmentUpload)
+ ))
+_sym_db.RegisterMessage(AttachmentUpload)
+
+IssueSummary = _reflection.GeneratedProtocolMessageType('IssueSummary', (_message.Message,), dict(
+ DESCRIPTOR = _ISSUESUMMARY,
+ __module__ = 'api.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.IssueSummary)
+ ))
+_sym_db.RegisterMessage(IssueSummary)
+
+PhaseDef = _reflection.GeneratedProtocolMessageType('PhaseDef', (_message.Message,), dict(
+ DESCRIPTOR = _PHASEDEF,
+ __module__ = 'api.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.PhaseDef)
+ ))
+_sym_db.RegisterMessage(PhaseDef)
+
+PhaseRef = _reflection.GeneratedProtocolMessageType('PhaseRef', (_message.Message,), dict(
+ DESCRIPTOR = _PHASEREF,
+ __module__ = 'api.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.PhaseRef)
+ ))
+_sym_db.RegisterMessage(PhaseRef)
+
+
+# @@protoc_insertion_point(module_scope)
diff --git a/api/api_proto/issue_objects_prpc_pb2.py b/api/api_proto/issue_objects_prpc_pb2.py
new file mode 100644
index 0000000..fe96e84
--- /dev/null
+++ b/api/api_proto/issue_objects_prpc_pb2.py
@@ -0,0 +1,4 @@
+# Generated by the pRPC protocol buffer compiler plugin. DO NOT EDIT!
+# source: api/api_proto/issue_objects.proto
+
+from google.protobuf import descriptor_pb2
diff --git a/api/api_proto/issues.proto b/api/api_proto/issues.proto
new file mode 100644
index 0000000..7c39884
--- /dev/null
+++ b/api/api_proto/issues.proto
@@ -0,0 +1,417 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+syntax = "proto3";
+
+package monorail;
+
+import "google/protobuf/empty.proto";
+import "api/api_proto/common.proto";
+import "api/api_proto/issue_objects.proto";
+import "api/api_proto/project_objects.proto";
+
+
+service Issues {
+ rpc CreateIssue (CreateIssueRequest) returns (IssueResponse) {}
+ rpc GetIssue (GetIssueRequest) returns (IssueResponse) {}
+ rpc ListIssues (ListIssuesRequest) returns (ListIssuesResponse) {}
+ rpc ListReferencedIssues(ListReferencedIssuesRequest) returns (ListReferencedIssuesResponse) {}
+ rpc ListApplicableFieldDefs(ListApplicableFieldDefsRequest) returns (ListApplicableFieldDefsResponse) {}
+ rpc UpdateIssue (UpdateIssueRequest) returns (IssueResponse) {}
+ rpc StarIssue (StarIssueRequest) returns (StarIssueResponse) {}
+ rpc IsIssueStarred (IsIssueStarredRequest) returns (IsIssueStarredResponse) {}
+ rpc ListStarredIssues (ListStarredIssuesRequest) returns (ListStarredIssuesResponse) {}
+ // There is no CreateComment method because comments are created by updates,
+ // which may have just comment content and no delta.
+ // There is no GetComment method, clients should use ListComments.
+ rpc ListComments (ListCommentsRequest) returns (ListCommentsResponse) {}
+ rpc ListActivities (ListActivitiesRequest) returns (ListActivitiesResponse) {}
+ rpc DeleteComment (DeleteCommentRequest) returns (google.protobuf.Empty) {}
+ rpc BulkUpdateApprovals (BulkUpdateApprovalsRequest) returns (BulkUpdateApprovalsResponse) {}
+ rpc UpdateApproval (UpdateApprovalRequest) returns (UpdateApprovalResponse) {}
+ rpc ConvertIssueApprovalsTemplate (ConvertIssueApprovalsTemplateRequest) returns (ConvertIssueApprovalsTemplateResponse) {}
+ rpc IssueSnapshot (IssueSnapshotRequest) returns (IssueSnapshotResponse) {}
+ rpc PresubmitIssue (PresubmitIssueRequest) returns (PresubmitIssueResponse) {}
+ rpc RerankBlockedOnIssues (RerankBlockedOnIssuesRequest) returns (RerankBlockedOnIssuesResponse) {}
+ rpc DeleteIssue (DeleteIssueRequest) returns (DeleteIssueResponse) {}
+ rpc DeleteIssueComment (DeleteIssueCommentRequest) returns (DeleteIssueCommentResponse) {}
+ rpc DeleteAttachment (DeleteAttachmentRequest) returns (DeleteAttachmentResponse) {}
+ rpc FlagIssues (FlagIssuesRequest) returns (FlagIssuesResponse) {}
+ rpc FlagComment (FlagCommentRequest) returns (FlagCommentResponse) {}
+ rpc ListIssuePermissions (ListIssuePermissionsRequest) returns (ListIssuePermissionsResponse) {}
+ rpc MoveIssue (MoveIssueRequest) returns (MoveIssueResponse) {}
+ rpc CopyIssue (CopyIssueRequest) returns (CopyIssueResponse) {}
+}
+
+
+// Next available tag: 4
+message CreateIssueRequest {
+ string project_name = 2;
+ Issue issue = 3;
+}
+
+
+// Next available tag: 3
+message GetIssueRequest {
+ IssueRef issue_ref = 2;
+}
+
+
+// Next available tag: 3
+message IssueResponse {
+ Issue issue = 1;
+ IssueRef moved_to_ref = 2;
+}
+
+
+// Next available tag: 8
+message ListIssuesRequest {
+ string query = 2;
+ uint32 canned_query = 3;
+ repeated string project_names = 4;
+ Pagination pagination = 5;
+ string group_by_spec = 6;
+ string sort_spec = 7;
+}
+
+
+// Next available tag: 3
+message ListIssuesResponse {
+ repeated Issue issues = 1;
+ uint32 total_results = 2;
+}
+
+
+// Next available tag: 3
+message ListReferencedIssuesRequest {
+ repeated IssueRef issue_refs = 2;
+}
+
+
+// Next available tag: 2
+message ListReferencedIssuesResponse {
+ // TODO(ehmaldonado): monorail:4033 Rename these fields to issues rather than
+ // refs.
+ repeated Issue open_refs = 1;
+ repeated Issue closed_refs = 2;
+}
+
+
+// Next available tag: 3
+message ListApplicableFieldDefsRequest {
+ repeated IssueRef issue_refs = 2;
+}
+
+
+// Next available tag: 2
+message ListApplicableFieldDefsResponse {
+ repeated FieldDef field_defs = 1;
+}
+
+
+// Next available tag: 9
+message UpdateIssueRequest {
+ IssueRef issue_ref = 2;
+ bool send_email = 3;
+ IssueDelta delta = 4;
+ string comment_content = 5;
+ bool is_description = 6;
+ repeated AttachmentUpload uploads = 7;
+ repeated int64 kept_attachments = 8;
+}
+
+
+// Next available tag: 4
+message StarIssueRequest {
+ IssueRef issue_ref = 2;
+ bool starred = 3; // True to add a star, False to remove one.
+}
+
+
+// Next available tag: 2
+message StarIssueResponse {
+ uint32 star_count = 1;
+}
+
+
+// Next available tag: 3
+message IsIssueStarredRequest {
+ IssueRef issue_ref = 2;
+}
+
+
+// Next available tag: 2
+message IsIssueStarredResponse {
+ bool is_starred = 1;
+}
+
+
+// Next available tag: 1
+message ListStarredIssuesRequest {
+}
+
+
+// Next available tag: 2
+message ListStarredIssuesResponse {
+ repeated IssueRef starred_issue_refs = 1;
+}
+
+
+// Next available tag: 3
+message ListCommentsRequest {
+ IssueRef issue_ref = 2;
+}
+
+
+// Next available tag: 2
+message ListCommentsResponse {
+ // Comments are in chronological order. The list of comments may
+ // include deleted, spam, and description comments. Spam and
+ // deleted comments will only have content supplied if the user is
+ // allowed to see it.
+ repeated Comment comments = 1;
+}
+
+
+// Next available tag: 5
+message ListActivitiesRequest {
+ // TODO(tyreej) description
+ UserRef user_ref = 2;
+ fixed32 before = 3;
+ fixed32 after = 4;
+}
+
+
+// Next available tag: 3
+message ListActivitiesResponse {
+ // TODO(tyreej) description
+ repeated Comment comments = 1;
+ repeated IssueSummary issue_summaries = 2;
+}
+
+
+// Next available tag: 5
+message DeleteCommentRequest {
+ IssueRef issue_ref = 2;
+ int64 sequence_num = 3;
+ bool delete = 4; // True to delete, False to undelete.
+}
+
+
+// TODO: Consider eventually replacing calls to UpdateApprovalRequest
+// with BulkUpdateApprovalsRequest.
+// TODO: For now, block bulk attaching uploads and survey editing.
+// Next available tag: 7
+message BulkUpdateApprovalsRequest {
+ repeated IssueRef issue_refs = 2;
+ FieldRef field_ref = 3;
+ ApprovalDelta approval_delta = 4;
+ string comment_content = 5;
+ bool send_email = 6;
+}
+
+
+// Next available tag: 2
+message BulkUpdateApprovalsResponse {
+ repeated IssueRef issue_refs = 1;
+}
+
+
+// Next available tag: 10
+message UpdateApprovalRequest {
+ IssueRef issue_ref = 2;
+ FieldRef field_ref = 3;
+ ApprovalDelta approval_delta = 4;
+ string comment_content = 5;
+ bool send_email = 6;
+ bool is_description = 7;
+ repeated AttachmentUpload uploads = 8;
+ repeated int64 kept_attachments = 9;
+}
+
+
+// Next available tag: 2
+message UpdateApprovalResponse {
+ Approval approval = 1;
+ // TODO(jojwang): monorail:3895, add new_comment field.
+}
+
+
+// Next available tag: 6
+message ConvertIssueApprovalsTemplateRequest {
+ IssueRef issue_ref = 2;
+ string template_name = 3;
+ string comment_content = 4;
+ bool send_email = 5;
+}
+
+
+// Next available tag: 2
+message ConvertIssueApprovalsTemplateResponse {
+ Issue issue = 1;
+}
+
+
+// Next available tag: 9
+message IssueSnapshotRequest {
+ int32 timestamp = 2;
+ string query = 3;
+ int32 canned_query = 4;
+ string group_by = 5;
+ string label_prefix = 6;
+ string project_name = 7;
+ int32 hotlist_id = 8;
+}
+
+
+// Next available tag: 3
+message IssueSnapshotCount {
+ string dimension = 1;
+ int32 count = 2;
+}
+
+
+// Next available tag: 3
+message IssueSnapshotResponse {
+ repeated IssueSnapshotCount snapshot_count = 1;
+ repeated string unsupported_field = 2;
+ bool search_limit_reached = 3;
+}
+
+
+// Next available tag: 4
+message PresubmitIssueRequest {
+ IssueRef issue_ref = 2;
+ IssueDelta issue_delta = 3;
+}
+
+
+// Next available tag: 8
+message PresubmitIssueResponse {
+ string owner_availability = 1;
+ string owner_availability_state = 2;
+ repeated ValueAndWhy derived_labels = 3;
+ repeated ValueAndWhy derived_owners = 4;
+ repeated ValueAndWhy derived_ccs = 5;
+ repeated ValueAndWhy warnings = 6;
+ repeated ValueAndWhy errors = 7;
+}
+
+
+// Next available tag: 6
+message RerankBlockedOnIssuesRequest {
+ IssueRef issue_ref = 2;
+ IssueRef moved_ref = 3;
+ IssueRef target_ref = 4;
+ bool split_above = 5;
+}
+
+
+// Next available tag: 2
+message RerankBlockedOnIssuesResponse {
+ repeated IssueRef blocked_on_issue_refs = 1;
+}
+
+
+// Next available tag: 4
+message DeleteIssueRequest {
+ IssueRef issue_ref = 2;
+ bool delete = 3;
+}
+
+
+// Next available tag: 1
+message DeleteIssueResponse {
+}
+
+
+// Next available tag: 5
+message DeleteIssueCommentRequest {
+ IssueRef issue_ref = 2;
+ uint32 sequence_num = 3;
+ bool delete = 4;
+}
+
+
+// Next available tag: 1
+message DeleteIssueCommentResponse {
+}
+
+
+// Next available tag: 6
+message DeleteAttachmentRequest {
+ IssueRef issue_ref = 2;
+ uint32 sequence_num = 3;
+ uint32 attachment_id = 4;
+ bool delete = 5;
+}
+
+
+// Next available tag: 1
+message DeleteAttachmentResponse {
+}
+
+
+// Next available tag: 4
+message FlagIssuesRequest {
+ repeated IssueRef issue_refs = 2;
+ bool flag = 3;
+}
+
+
+// Next available tag: 1
+message FlagIssuesResponse {
+}
+
+
+// Next available tag: 5
+message FlagCommentRequest {
+ IssueRef issue_ref = 2;
+ uint32 sequence_num = 3;
+ bool flag = 4;
+}
+
+
+// Next available tag: 1
+message FlagCommentResponse {
+}
+
+
+// Next available tag: 3
+message ListIssuePermissionsRequest {
+ IssueRef issue_ref = 2;
+}
+
+
+// Next available tag: 2
+message ListIssuePermissionsResponse {
+ repeated string permissions = 1;
+}
+
+
+// Next available tag: 4
+message MoveIssueRequest {
+ IssueRef issue_ref = 2;
+ string target_project_name = 3;
+}
+
+
+// Next available tag: 2
+message MoveIssueResponse {
+ IssueRef new_issue_ref = 1;
+}
+
+
+// Next available tag: 4
+message CopyIssueRequest {
+ IssueRef issue_ref = 2;
+ string target_project_name = 3;
+}
+
+
+// Next available tag: 2
+message CopyIssueResponse {
+ IssueRef new_issue_ref = 1;
+}
diff --git a/api/api_proto/issues_pb2.py b/api/api_proto/issues_pb2.py
new file mode 100644
index 0000000..470fa21
--- /dev/null
+++ b/api/api_proto/issues_pb2.py
@@ -0,0 +1,2703 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/api_proto/issues.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+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()
+
+
+from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
+from api.api_proto import common_pb2 as api_dot_api__proto_dot_common__pb2
+from api.api_proto import issue_objects_pb2 as api_dot_api__proto_dot_issue__objects__pb2
+from api.api_proto import project_objects_pb2 as api_dot_api__proto_dot_project__objects__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='api/api_proto/issues.proto',
+ package='monorail',
+ syntax='proto3',
+ serialized_options=None,
+ serialized_pb=_b('\n\x1a\x61pi/api_proto/issues.proto\x12\x08monorail\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1a\x61pi/api_proto/common.proto\x1a!api/api_proto/issue_objects.proto\x1a#api/api_proto/project_objects.proto\"J\n\x12\x43reateIssueRequest\x12\x14\n\x0cproject_name\x18\x02 \x01(\t\x12\x1e\n\x05issue\x18\x03 \x01(\x0b\x32\x0f.monorail.Issue\"8\n\x0fGetIssueRequest\x12%\n\tissue_ref\x18\x02 \x01(\x0b\x32\x12.monorail.IssueRef\"Y\n\rIssueResponse\x12\x1e\n\x05issue\x18\x01 \x01(\x0b\x32\x0f.monorail.Issue\x12(\n\x0cmoved_to_ref\x18\x02 \x01(\x0b\x32\x12.monorail.IssueRef\"\xa3\x01\n\x11ListIssuesRequest\x12\r\n\x05query\x18\x02 \x01(\t\x12\x14\n\x0c\x63\x61nned_query\x18\x03 \x01(\r\x12\x15\n\rproject_names\x18\x04 \x03(\t\x12(\n\npagination\x18\x05 \x01(\x0b\x32\x14.monorail.Pagination\x12\x15\n\rgroup_by_spec\x18\x06 \x01(\t\x12\x11\n\tsort_spec\x18\x07 \x01(\t\"L\n\x12ListIssuesResponse\x12\x1f\n\x06issues\x18\x01 \x03(\x0b\x32\x0f.monorail.Issue\x12\x15\n\rtotal_results\x18\x02 \x01(\r\"E\n\x1bListReferencedIssuesRequest\x12&\n\nissue_refs\x18\x02 \x03(\x0b\x32\x12.monorail.IssueRef\"h\n\x1cListReferencedIssuesResponse\x12\"\n\topen_refs\x18\x01 \x03(\x0b\x32\x0f.monorail.Issue\x12$\n\x0b\x63losed_refs\x18\x02 \x03(\x0b\x32\x0f.monorail.Issue\"H\n\x1eListApplicableFieldDefsRequest\x12&\n\nissue_refs\x18\x02 \x03(\x0b\x32\x12.monorail.IssueRef\"I\n\x1fListApplicableFieldDefsResponse\x12&\n\nfield_defs\x18\x01 \x03(\x0b\x32\x12.monorail.FieldDef\"\xec\x01\n\x12UpdateIssueRequest\x12%\n\tissue_ref\x18\x02 \x01(\x0b\x32\x12.monorail.IssueRef\x12\x12\n\nsend_email\x18\x03 \x01(\x08\x12#\n\x05\x64\x65lta\x18\x04 \x01(\x0b\x32\x14.monorail.IssueDelta\x12\x17\n\x0f\x63omment_content\x18\x05 \x01(\t\x12\x16\n\x0eis_description\x18\x06 \x01(\x08\x12+\n\x07uploads\x18\x07 \x03(\x0b\x32\x1a.monorail.AttachmentUpload\x12\x18\n\x10kept_attachments\x18\x08 \x03(\x03\"J\n\x10StarIssueRequest\x12%\n\tissue_ref\x18\x02 \x01(\x0b\x32\x12.monorail.IssueRef\x12\x0f\n\x07starred\x18\x03 \x01(\x08\"\'\n\x11StarIssueResponse\x12\x12\n\nstar_count\x18\x01 \x01(\r\">\n\x15IsIssueStarredRequest\x12%\n\tissue_ref\x18\x02 \x01(\x0b\x32\x12.monorail.IssueRef\",\n\x16IsIssueStarredResponse\x12\x12\n\nis_starred\x18\x01 \x01(\x08\"\x1a\n\x18ListStarredIssuesRequest\"K\n\x19ListStarredIssuesResponse\x12.\n\x12starred_issue_refs\x18\x01 \x03(\x0b\x32\x12.monorail.IssueRef\"<\n\x13ListCommentsRequest\x12%\n\tissue_ref\x18\x02 \x01(\x0b\x32\x12.monorail.IssueRef\";\n\x14ListCommentsResponse\x12#\n\x08\x63omments\x18\x01 \x03(\x0b\x32\x11.monorail.Comment\"[\n\x15ListActivitiesRequest\x12#\n\x08user_ref\x18\x02 \x01(\x0b\x32\x11.monorail.UserRef\x12\x0e\n\x06\x62\x65\x66ore\x18\x03 \x01(\x07\x12\r\n\x05\x61\x66ter\x18\x04 \x01(\x07\"n\n\x16ListActivitiesResponse\x12#\n\x08\x63omments\x18\x01 \x03(\x0b\x32\x11.monorail.Comment\x12/\n\x0fissue_summaries\x18\x02 \x03(\x0b\x32\x16.monorail.IssueSummary\"c\n\x14\x44\x65leteCommentRequest\x12%\n\tissue_ref\x18\x02 \x01(\x0b\x32\x12.monorail.IssueRef\x12\x14\n\x0csequence_num\x18\x03 \x01(\x03\x12\x0e\n\x06\x64\x65lete\x18\x04 \x01(\x08\"\xc9\x01\n\x1a\x42ulkUpdateApprovalsRequest\x12&\n\nissue_refs\x18\x02 \x03(\x0b\x32\x12.monorail.IssueRef\x12%\n\tfield_ref\x18\x03 \x01(\x0b\x32\x12.monorail.FieldRef\x12/\n\x0e\x61pproval_delta\x18\x04 \x01(\x0b\x32\x17.monorail.ApprovalDelta\x12\x17\n\x0f\x63omment_content\x18\x05 \x01(\t\x12\x12\n\nsend_email\x18\x06 \x01(\x08\"E\n\x1b\x42ulkUpdateApprovalsResponse\x12&\n\nissue_refs\x18\x01 \x03(\x0b\x32\x12.monorail.IssueRef\"\xa2\x02\n\x15UpdateApprovalRequest\x12%\n\tissue_ref\x18\x02 \x01(\x0b\x32\x12.monorail.IssueRef\x12%\n\tfield_ref\x18\x03 \x01(\x0b\x32\x12.monorail.FieldRef\x12/\n\x0e\x61pproval_delta\x18\x04 \x01(\x0b\x32\x17.monorail.ApprovalDelta\x12\x17\n\x0f\x63omment_content\x18\x05 \x01(\t\x12\x12\n\nsend_email\x18\x06 \x01(\x08\x12\x16\n\x0eis_description\x18\x07 \x01(\x08\x12+\n\x07uploads\x18\x08 \x03(\x0b\x32\x1a.monorail.AttachmentUpload\x12\x18\n\x10kept_attachments\x18\t \x03(\x03\">\n\x16UpdateApprovalResponse\x12$\n\x08\x61pproval\x18\x01 \x01(\x0b\x32\x12.monorail.Approval\"\x91\x01\n$ConvertIssueApprovalsTemplateRequest\x12%\n\tissue_ref\x18\x02 \x01(\x0b\x32\x12.monorail.IssueRef\x12\x15\n\rtemplate_name\x18\x03 \x01(\t\x12\x17\n\x0f\x63omment_content\x18\x04 \x01(\t\x12\x12\n\nsend_email\x18\x05 \x01(\x08\"G\n%ConvertIssueApprovalsTemplateResponse\x12\x1e\n\x05issue\x18\x01 \x01(\x0b\x32\x0f.monorail.Issue\"\xa0\x01\n\x14IssueSnapshotRequest\x12\x11\n\ttimestamp\x18\x02 \x01(\x05\x12\r\n\x05query\x18\x03 \x01(\t\x12\x14\n\x0c\x63\x61nned_query\x18\x04 \x01(\x05\x12\x10\n\x08group_by\x18\x05 \x01(\t\x12\x14\n\x0clabel_prefix\x18\x06 \x01(\t\x12\x14\n\x0cproject_name\x18\x07 \x01(\t\x12\x12\n\nhotlist_id\x18\x08 \x01(\x05\"6\n\x12IssueSnapshotCount\x12\x11\n\tdimension\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\x05\"\x86\x01\n\x15IssueSnapshotResponse\x12\x34\n\x0esnapshot_count\x18\x01 \x03(\x0b\x32\x1c.monorail.IssueSnapshotCount\x12\x19\n\x11unsupported_field\x18\x02 \x03(\t\x12\x1c\n\x14search_limit_reached\x18\x03 \x01(\x08\"i\n\x15PresubmitIssueRequest\x12%\n\tissue_ref\x18\x02 \x01(\x0b\x32\x12.monorail.IssueRef\x12)\n\x0bissue_delta\x18\x03 \x01(\x0b\x32\x14.monorail.IssueDelta\"\xb0\x02\n\x16PresubmitIssueResponse\x12\x1a\n\x12owner_availability\x18\x01 \x01(\t\x12 \n\x18owner_availability_state\x18\x02 \x01(\t\x12-\n\x0e\x64\x65rived_labels\x18\x03 \x03(\x0b\x32\x15.monorail.ValueAndWhy\x12-\n\x0e\x64\x65rived_owners\x18\x04 \x03(\x0b\x32\x15.monorail.ValueAndWhy\x12*\n\x0b\x64\x65rived_ccs\x18\x05 \x03(\x0b\x32\x15.monorail.ValueAndWhy\x12\'\n\x08warnings\x18\x06 \x03(\x0b\x32\x15.monorail.ValueAndWhy\x12%\n\x06\x65rrors\x18\x07 \x03(\x0b\x32\x15.monorail.ValueAndWhy\"\xa9\x01\n\x1cRerankBlockedOnIssuesRequest\x12%\n\tissue_ref\x18\x02 \x01(\x0b\x32\x12.monorail.IssueRef\x12%\n\tmoved_ref\x18\x03 \x01(\x0b\x32\x12.monorail.IssueRef\x12&\n\ntarget_ref\x18\x04 \x01(\x0b\x32\x12.monorail.IssueRef\x12\x13\n\x0bsplit_above\x18\x05 \x01(\x08\"R\n\x1dRerankBlockedOnIssuesResponse\x12\x31\n\x15\x62locked_on_issue_refs\x18\x01 \x03(\x0b\x32\x12.monorail.IssueRef\"K\n\x12\x44\x65leteIssueRequest\x12%\n\tissue_ref\x18\x02 \x01(\x0b\x32\x12.monorail.IssueRef\x12\x0e\n\x06\x64\x65lete\x18\x03 \x01(\x08\"\x15\n\x13\x44\x65leteIssueResponse\"h\n\x19\x44\x65leteIssueCommentRequest\x12%\n\tissue_ref\x18\x02 \x01(\x0b\x32\x12.monorail.IssueRef\x12\x14\n\x0csequence_num\x18\x03 \x01(\r\x12\x0e\n\x06\x64\x65lete\x18\x04 \x01(\x08\"\x1c\n\x1a\x44\x65leteIssueCommentResponse\"}\n\x17\x44\x65leteAttachmentRequest\x12%\n\tissue_ref\x18\x02 \x01(\x0b\x32\x12.monorail.IssueRef\x12\x14\n\x0csequence_num\x18\x03 \x01(\r\x12\x15\n\rattachment_id\x18\x04 \x01(\r\x12\x0e\n\x06\x64\x65lete\x18\x05 \x01(\x08\"\x1a\n\x18\x44\x65leteAttachmentResponse\"I\n\x11\x46lagIssuesRequest\x12&\n\nissue_refs\x18\x02 \x03(\x0b\x32\x12.monorail.IssueRef\x12\x0c\n\x04\x66lag\x18\x03 \x01(\x08\"\x14\n\x12\x46lagIssuesResponse\"_\n\x12\x46lagCommentRequest\x12%\n\tissue_ref\x18\x02 \x01(\x0b\x32\x12.monorail.IssueRef\x12\x14\n\x0csequence_num\x18\x03 \x01(\r\x12\x0c\n\x04\x66lag\x18\x04 \x01(\x08\"\x15\n\x13\x46lagCommentResponse\"D\n\x1bListIssuePermissionsRequest\x12%\n\tissue_ref\x18\x02 \x01(\x0b\x32\x12.monorail.IssueRef\"3\n\x1cListIssuePermissionsResponse\x12\x13\n\x0bpermissions\x18\x01 \x03(\t\"V\n\x10MoveIssueRequest\x12%\n\tissue_ref\x18\x02 \x01(\x0b\x32\x12.monorail.IssueRef\x12\x1b\n\x13target_project_name\x18\x03 \x01(\t\">\n\x11MoveIssueResponse\x12)\n\rnew_issue_ref\x18\x01 \x01(\x0b\x32\x12.monorail.IssueRef\"V\n\x10\x43opyIssueRequest\x12%\n\tissue_ref\x18\x02 \x01(\x0b\x32\x12.monorail.IssueRef\x12\x1b\n\x13target_project_name\x18\x03 \x01(\t\">\n\x11\x43opyIssueResponse\x12)\n\rnew_issue_ref\x18\x01 \x01(\x0b\x32\x12.monorail.IssueRef2\xeb\x11\n\x06Issues\x12\x46\n\x0b\x43reateIssue\x12\x1c.monorail.CreateIssueRequest\x1a\x17.monorail.IssueResponse\"\x00\x12@\n\x08GetIssue\x12\x19.monorail.GetIssueRequest\x1a\x17.monorail.IssueResponse\"\x00\x12I\n\nListIssues\x12\x1b.monorail.ListIssuesRequest\x1a\x1c.monorail.ListIssuesResponse\"\x00\x12g\n\x14ListReferencedIssues\x12%.monorail.ListReferencedIssuesRequest\x1a&.monorail.ListReferencedIssuesResponse\"\x00\x12p\n\x17ListApplicableFieldDefs\x12(.monorail.ListApplicableFieldDefsRequest\x1a).monorail.ListApplicableFieldDefsResponse\"\x00\x12\x46\n\x0bUpdateIssue\x12\x1c.monorail.UpdateIssueRequest\x1a\x17.monorail.IssueResponse\"\x00\x12\x46\n\tStarIssue\x12\x1a.monorail.StarIssueRequest\x1a\x1b.monorail.StarIssueResponse\"\x00\x12U\n\x0eIsIssueStarred\x12\x1f.monorail.IsIssueStarredRequest\x1a .monorail.IsIssueStarredResponse\"\x00\x12^\n\x11ListStarredIssues\x12\".monorail.ListStarredIssuesRequest\x1a#.monorail.ListStarredIssuesResponse\"\x00\x12O\n\x0cListComments\x12\x1d.monorail.ListCommentsRequest\x1a\x1e.monorail.ListCommentsResponse\"\x00\x12U\n\x0eListActivities\x12\x1f.monorail.ListActivitiesRequest\x1a .monorail.ListActivitiesResponse\"\x00\x12I\n\rDeleteComment\x12\x1e.monorail.DeleteCommentRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x64\n\x13\x42ulkUpdateApprovals\x12$.monorail.BulkUpdateApprovalsRequest\x1a%.monorail.BulkUpdateApprovalsResponse\"\x00\x12U\n\x0eUpdateApproval\x12\x1f.monorail.UpdateApprovalRequest\x1a .monorail.UpdateApprovalResponse\"\x00\x12\x82\x01\n\x1d\x43onvertIssueApprovalsTemplate\x12..monorail.ConvertIssueApprovalsTemplateRequest\x1a/.monorail.ConvertIssueApprovalsTemplateResponse\"\x00\x12R\n\rIssueSnapshot\x12\x1e.monorail.IssueSnapshotRequest\x1a\x1f.monorail.IssueSnapshotResponse\"\x00\x12U\n\x0ePresubmitIssue\x12\x1f.monorail.PresubmitIssueRequest\x1a .monorail.PresubmitIssueResponse\"\x00\x12j\n\x15RerankBlockedOnIssues\x12&.monorail.RerankBlockedOnIssuesRequest\x1a\'.monorail.RerankBlockedOnIssuesResponse\"\x00\x12L\n\x0b\x44\x65leteIssue\x12\x1c.monorail.DeleteIssueRequest\x1a\x1d.monorail.DeleteIssueResponse\"\x00\x12\x61\n\x12\x44\x65leteIssueComment\x12#.monorail.DeleteIssueCommentRequest\x1a$.monorail.DeleteIssueCommentResponse\"\x00\x12[\n\x10\x44\x65leteAttachment\x12!.monorail.DeleteAttachmentRequest\x1a\".monorail.DeleteAttachmentResponse\"\x00\x12I\n\nFlagIssues\x12\x1b.monorail.FlagIssuesRequest\x1a\x1c.monorail.FlagIssuesResponse\"\x00\x12L\n\x0b\x46lagComment\x12\x1c.monorail.FlagCommentRequest\x1a\x1d.monorail.FlagCommentResponse\"\x00\x12g\n\x14ListIssuePermissions\x12%.monorail.ListIssuePermissionsRequest\x1a&.monorail.ListIssuePermissionsResponse\"\x00\x12\x46\n\tMoveIssue\x12\x1a.monorail.MoveIssueRequest\x1a\x1b.monorail.MoveIssueResponse\"\x00\x12\x46\n\tCopyIssue\x12\x1a.monorail.CopyIssueRequest\x1a\x1b.monorail.CopyIssueResponse\"\x00\x62\x06proto3')
+ ,
+ dependencies=[google_dot_protobuf_dot_empty__pb2.DESCRIPTOR,api_dot_api__proto_dot_common__pb2.DESCRIPTOR,api_dot_api__proto_dot_issue__objects__pb2.DESCRIPTOR,api_dot_api__proto_dot_project__objects__pb2.DESCRIPTOR,])
+
+
+
+
+_CREATEISSUEREQUEST = _descriptor.Descriptor(
+ name='CreateIssueRequest',
+ full_name='monorail.CreateIssueRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.CreateIssueRequest.project_name', index=0,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='issue', full_name='monorail.CreateIssueRequest.issue', index=1,
+ number=3, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=169,
+ serialized_end=243,
+)
+
+
+_GETISSUEREQUEST = _descriptor.Descriptor(
+ name='GetIssueRequest',
+ full_name='monorail.GetIssueRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_ref', full_name='monorail.GetIssueRequest.issue_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=245,
+ serialized_end=301,
+)
+
+
+_ISSUERESPONSE = _descriptor.Descriptor(
+ name='IssueResponse',
+ full_name='monorail.IssueResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue', full_name='monorail.IssueResponse.issue', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='moved_to_ref', full_name='monorail.IssueResponse.moved_to_ref', index=1,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=303,
+ serialized_end=392,
+)
+
+
+_LISTISSUESREQUEST = _descriptor.Descriptor(
+ name='ListIssuesRequest',
+ full_name='monorail.ListIssuesRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='query', full_name='monorail.ListIssuesRequest.query', index=0,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='canned_query', full_name='monorail.ListIssuesRequest.canned_query', index=1,
+ number=3, type=13, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='project_names', full_name='monorail.ListIssuesRequest.project_names', index=2,
+ number=4, 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),
+ _descriptor.FieldDescriptor(
+ name='pagination', full_name='monorail.ListIssuesRequest.pagination', index=3,
+ number=5, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='group_by_spec', full_name='monorail.ListIssuesRequest.group_by_spec', index=4,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='sort_spec', full_name='monorail.ListIssuesRequest.sort_spec', index=5,
+ number=7, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=395,
+ serialized_end=558,
+)
+
+
+_LISTISSUESRESPONSE = _descriptor.Descriptor(
+ name='ListIssuesResponse',
+ full_name='monorail.ListIssuesResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issues', full_name='monorail.ListIssuesResponse.issues', 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),
+ _descriptor.FieldDescriptor(
+ name='total_results', full_name='monorail.ListIssuesResponse.total_results', index=1,
+ number=2, type=13, cpp_type=3, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=560,
+ serialized_end=636,
+)
+
+
+_LISTREFERENCEDISSUESREQUEST = _descriptor.Descriptor(
+ name='ListReferencedIssuesRequest',
+ full_name='monorail.ListReferencedIssuesRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_refs', full_name='monorail.ListReferencedIssuesRequest.issue_refs', index=0,
+ number=2, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=638,
+ serialized_end=707,
+)
+
+
+_LISTREFERENCEDISSUESRESPONSE = _descriptor.Descriptor(
+ name='ListReferencedIssuesResponse',
+ full_name='monorail.ListReferencedIssuesResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='open_refs', full_name='monorail.ListReferencedIssuesResponse.open_refs', 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),
+ _descriptor.FieldDescriptor(
+ name='closed_refs', full_name='monorail.ListReferencedIssuesResponse.closed_refs', index=1,
+ number=2, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=709,
+ serialized_end=813,
+)
+
+
+_LISTAPPLICABLEFIELDDEFSREQUEST = _descriptor.Descriptor(
+ name='ListApplicableFieldDefsRequest',
+ full_name='monorail.ListApplicableFieldDefsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_refs', full_name='monorail.ListApplicableFieldDefsRequest.issue_refs', index=0,
+ number=2, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=815,
+ serialized_end=887,
+)
+
+
+_LISTAPPLICABLEFIELDDEFSRESPONSE = _descriptor.Descriptor(
+ name='ListApplicableFieldDefsResponse',
+ full_name='monorail.ListApplicableFieldDefsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='field_defs', full_name='monorail.ListApplicableFieldDefsResponse.field_defs', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=889,
+ serialized_end=962,
+)
+
+
+_UPDATEISSUEREQUEST = _descriptor.Descriptor(
+ name='UpdateIssueRequest',
+ full_name='monorail.UpdateIssueRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_ref', full_name='monorail.UpdateIssueRequest.issue_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='send_email', full_name='monorail.UpdateIssueRequest.send_email', index=1,
+ number=3, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='delta', full_name='monorail.UpdateIssueRequest.delta', index=2,
+ number=4, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='comment_content', full_name='monorail.UpdateIssueRequest.comment_content', index=3,
+ number=5, 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),
+ _descriptor.FieldDescriptor(
+ name='is_description', full_name='monorail.UpdateIssueRequest.is_description', index=4,
+ number=6, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='uploads', full_name='monorail.UpdateIssueRequest.uploads', index=5,
+ number=7, 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),
+ _descriptor.FieldDescriptor(
+ name='kept_attachments', full_name='monorail.UpdateIssueRequest.kept_attachments', index=6,
+ number=8, type=3, cpp_type=2, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=965,
+ serialized_end=1201,
+)
+
+
+_STARISSUEREQUEST = _descriptor.Descriptor(
+ name='StarIssueRequest',
+ full_name='monorail.StarIssueRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_ref', full_name='monorail.StarIssueRequest.issue_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='starred', full_name='monorail.StarIssueRequest.starred', index=1,
+ number=3, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1203,
+ serialized_end=1277,
+)
+
+
+_STARISSUERESPONSE = _descriptor.Descriptor(
+ name='StarIssueResponse',
+ full_name='monorail.StarIssueResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='star_count', full_name='monorail.StarIssueResponse.star_count', index=0,
+ number=1, type=13, cpp_type=3, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1279,
+ serialized_end=1318,
+)
+
+
+_ISISSUESTARREDREQUEST = _descriptor.Descriptor(
+ name='IsIssueStarredRequest',
+ full_name='monorail.IsIssueStarredRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_ref', full_name='monorail.IsIssueStarredRequest.issue_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1320,
+ serialized_end=1382,
+)
+
+
+_ISISSUESTARREDRESPONSE = _descriptor.Descriptor(
+ name='IsIssueStarredResponse',
+ full_name='monorail.IsIssueStarredResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='is_starred', full_name='monorail.IsIssueStarredResponse.is_starred', index=0,
+ number=1, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1384,
+ serialized_end=1428,
+)
+
+
+_LISTSTARREDISSUESREQUEST = _descriptor.Descriptor(
+ name='ListStarredIssuesRequest',
+ full_name='monorail.ListStarredIssuesRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1430,
+ serialized_end=1456,
+)
+
+
+_LISTSTARREDISSUESRESPONSE = _descriptor.Descriptor(
+ name='ListStarredIssuesResponse',
+ full_name='monorail.ListStarredIssuesResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='starred_issue_refs', full_name='monorail.ListStarredIssuesResponse.starred_issue_refs', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1458,
+ serialized_end=1533,
+)
+
+
+_LISTCOMMENTSREQUEST = _descriptor.Descriptor(
+ name='ListCommentsRequest',
+ full_name='monorail.ListCommentsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_ref', full_name='monorail.ListCommentsRequest.issue_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1535,
+ serialized_end=1595,
+)
+
+
+_LISTCOMMENTSRESPONSE = _descriptor.Descriptor(
+ name='ListCommentsResponse',
+ full_name='monorail.ListCommentsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='comments', full_name='monorail.ListCommentsResponse.comments', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1597,
+ serialized_end=1656,
+)
+
+
+_LISTACTIVITIESREQUEST = _descriptor.Descriptor(
+ name='ListActivitiesRequest',
+ full_name='monorail.ListActivitiesRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='user_ref', full_name='monorail.ListActivitiesRequest.user_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='before', full_name='monorail.ListActivitiesRequest.before', index=1,
+ number=3, type=7, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='after', full_name='monorail.ListActivitiesRequest.after', index=2,
+ number=4, type=7, cpp_type=3, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1658,
+ serialized_end=1749,
+)
+
+
+_LISTACTIVITIESRESPONSE = _descriptor.Descriptor(
+ name='ListActivitiesResponse',
+ full_name='monorail.ListActivitiesResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='comments', full_name='monorail.ListActivitiesResponse.comments', 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),
+ _descriptor.FieldDescriptor(
+ name='issue_summaries', full_name='monorail.ListActivitiesResponse.issue_summaries', index=1,
+ number=2, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1751,
+ serialized_end=1861,
+)
+
+
+_DELETECOMMENTREQUEST = _descriptor.Descriptor(
+ name='DeleteCommentRequest',
+ full_name='monorail.DeleteCommentRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_ref', full_name='monorail.DeleteCommentRequest.issue_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='sequence_num', full_name='monorail.DeleteCommentRequest.sequence_num', index=1,
+ number=3, type=3, cpp_type=2, 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),
+ _descriptor.FieldDescriptor(
+ name='delete', full_name='monorail.DeleteCommentRequest.delete', index=2,
+ number=4, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1863,
+ serialized_end=1962,
+)
+
+
+_BULKUPDATEAPPROVALSREQUEST = _descriptor.Descriptor(
+ name='BulkUpdateApprovalsRequest',
+ full_name='monorail.BulkUpdateApprovalsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_refs', full_name='monorail.BulkUpdateApprovalsRequest.issue_refs', index=0,
+ number=2, 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),
+ _descriptor.FieldDescriptor(
+ name='field_ref', full_name='monorail.BulkUpdateApprovalsRequest.field_ref', index=1,
+ number=3, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='approval_delta', full_name='monorail.BulkUpdateApprovalsRequest.approval_delta', index=2,
+ number=4, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='comment_content', full_name='monorail.BulkUpdateApprovalsRequest.comment_content', index=3,
+ number=5, 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),
+ _descriptor.FieldDescriptor(
+ name='send_email', full_name='monorail.BulkUpdateApprovalsRequest.send_email', index=4,
+ number=6, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1965,
+ serialized_end=2166,
+)
+
+
+_BULKUPDATEAPPROVALSRESPONSE = _descriptor.Descriptor(
+ name='BulkUpdateApprovalsResponse',
+ full_name='monorail.BulkUpdateApprovalsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_refs', full_name='monorail.BulkUpdateApprovalsResponse.issue_refs', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2168,
+ serialized_end=2237,
+)
+
+
+_UPDATEAPPROVALREQUEST = _descriptor.Descriptor(
+ name='UpdateApprovalRequest',
+ full_name='monorail.UpdateApprovalRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_ref', full_name='monorail.UpdateApprovalRequest.issue_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='field_ref', full_name='monorail.UpdateApprovalRequest.field_ref', index=1,
+ number=3, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='approval_delta', full_name='monorail.UpdateApprovalRequest.approval_delta', index=2,
+ number=4, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='comment_content', full_name='monorail.UpdateApprovalRequest.comment_content', index=3,
+ number=5, 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),
+ _descriptor.FieldDescriptor(
+ name='send_email', full_name='monorail.UpdateApprovalRequest.send_email', index=4,
+ number=6, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='is_description', full_name='monorail.UpdateApprovalRequest.is_description', index=5,
+ number=7, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='uploads', full_name='monorail.UpdateApprovalRequest.uploads', index=6,
+ number=8, 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),
+ _descriptor.FieldDescriptor(
+ name='kept_attachments', full_name='monorail.UpdateApprovalRequest.kept_attachments', index=7,
+ number=9, type=3, cpp_type=2, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2240,
+ serialized_end=2530,
+)
+
+
+_UPDATEAPPROVALRESPONSE = _descriptor.Descriptor(
+ name='UpdateApprovalResponse',
+ full_name='monorail.UpdateApprovalResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='approval', full_name='monorail.UpdateApprovalResponse.approval', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2532,
+ serialized_end=2594,
+)
+
+
+_CONVERTISSUEAPPROVALSTEMPLATEREQUEST = _descriptor.Descriptor(
+ name='ConvertIssueApprovalsTemplateRequest',
+ full_name='monorail.ConvertIssueApprovalsTemplateRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_ref', full_name='monorail.ConvertIssueApprovalsTemplateRequest.issue_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='template_name', full_name='monorail.ConvertIssueApprovalsTemplateRequest.template_name', index=1,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='comment_content', full_name='monorail.ConvertIssueApprovalsTemplateRequest.comment_content', index=2,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='send_email', full_name='monorail.ConvertIssueApprovalsTemplateRequest.send_email', index=3,
+ number=5, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2597,
+ serialized_end=2742,
+)
+
+
+_CONVERTISSUEAPPROVALSTEMPLATERESPONSE = _descriptor.Descriptor(
+ name='ConvertIssueApprovalsTemplateResponse',
+ full_name='monorail.ConvertIssueApprovalsTemplateResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue', full_name='monorail.ConvertIssueApprovalsTemplateResponse.issue', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2744,
+ serialized_end=2815,
+)
+
+
+_ISSUESNAPSHOTREQUEST = _descriptor.Descriptor(
+ name='IssueSnapshotRequest',
+ full_name='monorail.IssueSnapshotRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='timestamp', full_name='monorail.IssueSnapshotRequest.timestamp', index=0,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='query', full_name='monorail.IssueSnapshotRequest.query', index=1,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='canned_query', full_name='monorail.IssueSnapshotRequest.canned_query', index=2,
+ number=4, 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),
+ _descriptor.FieldDescriptor(
+ name='group_by', full_name='monorail.IssueSnapshotRequest.group_by', index=3,
+ number=5, 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),
+ _descriptor.FieldDescriptor(
+ name='label_prefix', full_name='monorail.IssueSnapshotRequest.label_prefix', index=4,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.IssueSnapshotRequest.project_name', index=5,
+ number=7, 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),
+ _descriptor.FieldDescriptor(
+ name='hotlist_id', full_name='monorail.IssueSnapshotRequest.hotlist_id', index=6,
+ number=8, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2818,
+ serialized_end=2978,
+)
+
+
+_ISSUESNAPSHOTCOUNT = _descriptor.Descriptor(
+ name='IssueSnapshotCount',
+ full_name='monorail.IssueSnapshotCount',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='dimension', full_name='monorail.IssueSnapshotCount.dimension', 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),
+ _descriptor.FieldDescriptor(
+ name='count', full_name='monorail.IssueSnapshotCount.count', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2980,
+ serialized_end=3034,
+)
+
+
+_ISSUESNAPSHOTRESPONSE = _descriptor.Descriptor(
+ name='IssueSnapshotResponse',
+ full_name='monorail.IssueSnapshotResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='snapshot_count', full_name='monorail.IssueSnapshotResponse.snapshot_count', 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),
+ _descriptor.FieldDescriptor(
+ name='unsupported_field', full_name='monorail.IssueSnapshotResponse.unsupported_field', index=1,
+ number=2, 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),
+ _descriptor.FieldDescriptor(
+ name='search_limit_reached', full_name='monorail.IssueSnapshotResponse.search_limit_reached', index=2,
+ number=3, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=3037,
+ serialized_end=3171,
+)
+
+
+_PRESUBMITISSUEREQUEST = _descriptor.Descriptor(
+ name='PresubmitIssueRequest',
+ full_name='monorail.PresubmitIssueRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_ref', full_name='monorail.PresubmitIssueRequest.issue_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='issue_delta', full_name='monorail.PresubmitIssueRequest.issue_delta', index=1,
+ number=3, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=3173,
+ serialized_end=3278,
+)
+
+
+_PRESUBMITISSUERESPONSE = _descriptor.Descriptor(
+ name='PresubmitIssueResponse',
+ full_name='monorail.PresubmitIssueResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='owner_availability', full_name='monorail.PresubmitIssueResponse.owner_availability', 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),
+ _descriptor.FieldDescriptor(
+ name='owner_availability_state', full_name='monorail.PresubmitIssueResponse.owner_availability_state', 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),
+ _descriptor.FieldDescriptor(
+ name='derived_labels', full_name='monorail.PresubmitIssueResponse.derived_labels', index=2,
+ number=3, 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),
+ _descriptor.FieldDescriptor(
+ name='derived_owners', full_name='monorail.PresubmitIssueResponse.derived_owners', index=3,
+ number=4, 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),
+ _descriptor.FieldDescriptor(
+ name='derived_ccs', full_name='monorail.PresubmitIssueResponse.derived_ccs', index=4,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='warnings', full_name='monorail.PresubmitIssueResponse.warnings', index=5,
+ number=6, 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),
+ _descriptor.FieldDescriptor(
+ name='errors', full_name='monorail.PresubmitIssueResponse.errors', index=6,
+ number=7, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=3281,
+ serialized_end=3585,
+)
+
+
+_RERANKBLOCKEDONISSUESREQUEST = _descriptor.Descriptor(
+ name='RerankBlockedOnIssuesRequest',
+ full_name='monorail.RerankBlockedOnIssuesRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_ref', full_name='monorail.RerankBlockedOnIssuesRequest.issue_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='moved_ref', full_name='monorail.RerankBlockedOnIssuesRequest.moved_ref', index=1,
+ number=3, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='target_ref', full_name='monorail.RerankBlockedOnIssuesRequest.target_ref', index=2,
+ number=4, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='split_above', full_name='monorail.RerankBlockedOnIssuesRequest.split_above', index=3,
+ number=5, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=3588,
+ serialized_end=3757,
+)
+
+
+_RERANKBLOCKEDONISSUESRESPONSE = _descriptor.Descriptor(
+ name='RerankBlockedOnIssuesResponse',
+ full_name='monorail.RerankBlockedOnIssuesResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='blocked_on_issue_refs', full_name='monorail.RerankBlockedOnIssuesResponse.blocked_on_issue_refs', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=3759,
+ serialized_end=3841,
+)
+
+
+_DELETEISSUEREQUEST = _descriptor.Descriptor(
+ name='DeleteIssueRequest',
+ full_name='monorail.DeleteIssueRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_ref', full_name='monorail.DeleteIssueRequest.issue_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='delete', full_name='monorail.DeleteIssueRequest.delete', index=1,
+ number=3, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=3843,
+ serialized_end=3918,
+)
+
+
+_DELETEISSUERESPONSE = _descriptor.Descriptor(
+ name='DeleteIssueResponse',
+ full_name='monorail.DeleteIssueResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=3920,
+ serialized_end=3941,
+)
+
+
+_DELETEISSUECOMMENTREQUEST = _descriptor.Descriptor(
+ name='DeleteIssueCommentRequest',
+ full_name='monorail.DeleteIssueCommentRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_ref', full_name='monorail.DeleteIssueCommentRequest.issue_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='sequence_num', full_name='monorail.DeleteIssueCommentRequest.sequence_num', index=1,
+ number=3, type=13, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='delete', full_name='monorail.DeleteIssueCommentRequest.delete', index=2,
+ number=4, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=3943,
+ serialized_end=4047,
+)
+
+
+_DELETEISSUECOMMENTRESPONSE = _descriptor.Descriptor(
+ name='DeleteIssueCommentResponse',
+ full_name='monorail.DeleteIssueCommentResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=4049,
+ serialized_end=4077,
+)
+
+
+_DELETEATTACHMENTREQUEST = _descriptor.Descriptor(
+ name='DeleteAttachmentRequest',
+ full_name='monorail.DeleteAttachmentRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_ref', full_name='monorail.DeleteAttachmentRequest.issue_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='sequence_num', full_name='monorail.DeleteAttachmentRequest.sequence_num', index=1,
+ number=3, type=13, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='attachment_id', full_name='monorail.DeleteAttachmentRequest.attachment_id', index=2,
+ number=4, type=13, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='delete', full_name='monorail.DeleteAttachmentRequest.delete', index=3,
+ number=5, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=4079,
+ serialized_end=4204,
+)
+
+
+_DELETEATTACHMENTRESPONSE = _descriptor.Descriptor(
+ name='DeleteAttachmentResponse',
+ full_name='monorail.DeleteAttachmentResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=4206,
+ serialized_end=4232,
+)
+
+
+_FLAGISSUESREQUEST = _descriptor.Descriptor(
+ name='FlagIssuesRequest',
+ full_name='monorail.FlagIssuesRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_refs', full_name='monorail.FlagIssuesRequest.issue_refs', index=0,
+ number=2, 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),
+ _descriptor.FieldDescriptor(
+ name='flag', full_name='monorail.FlagIssuesRequest.flag', index=1,
+ number=3, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=4234,
+ serialized_end=4307,
+)
+
+
+_FLAGISSUESRESPONSE = _descriptor.Descriptor(
+ name='FlagIssuesResponse',
+ full_name='monorail.FlagIssuesResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=4309,
+ serialized_end=4329,
+)
+
+
+_FLAGCOMMENTREQUEST = _descriptor.Descriptor(
+ name='FlagCommentRequest',
+ full_name='monorail.FlagCommentRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_ref', full_name='monorail.FlagCommentRequest.issue_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='sequence_num', full_name='monorail.FlagCommentRequest.sequence_num', index=1,
+ number=3, type=13, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='flag', full_name='monorail.FlagCommentRequest.flag', index=2,
+ number=4, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=4331,
+ serialized_end=4426,
+)
+
+
+_FLAGCOMMENTRESPONSE = _descriptor.Descriptor(
+ name='FlagCommentResponse',
+ full_name='monorail.FlagCommentResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=4428,
+ serialized_end=4449,
+)
+
+
+_LISTISSUEPERMISSIONSREQUEST = _descriptor.Descriptor(
+ name='ListIssuePermissionsRequest',
+ full_name='monorail.ListIssuePermissionsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_ref', full_name='monorail.ListIssuePermissionsRequest.issue_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=4451,
+ serialized_end=4519,
+)
+
+
+_LISTISSUEPERMISSIONSRESPONSE = _descriptor.Descriptor(
+ name='ListIssuePermissionsResponse',
+ full_name='monorail.ListIssuePermissionsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='permissions', full_name='monorail.ListIssuePermissionsResponse.permissions', index=0,
+ number=1, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=4521,
+ serialized_end=4572,
+)
+
+
+_MOVEISSUEREQUEST = _descriptor.Descriptor(
+ name='MoveIssueRequest',
+ full_name='monorail.MoveIssueRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_ref', full_name='monorail.MoveIssueRequest.issue_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='target_project_name', full_name='monorail.MoveIssueRequest.target_project_name', index=1,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=4574,
+ serialized_end=4660,
+)
+
+
+_MOVEISSUERESPONSE = _descriptor.Descriptor(
+ name='MoveIssueResponse',
+ full_name='monorail.MoveIssueResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='new_issue_ref', full_name='monorail.MoveIssueResponse.new_issue_ref', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=4662,
+ serialized_end=4724,
+)
+
+
+_COPYISSUEREQUEST = _descriptor.Descriptor(
+ name='CopyIssueRequest',
+ full_name='monorail.CopyIssueRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue_ref', full_name='monorail.CopyIssueRequest.issue_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='target_project_name', full_name='monorail.CopyIssueRequest.target_project_name', index=1,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=4726,
+ serialized_end=4812,
+)
+
+
+_COPYISSUERESPONSE = _descriptor.Descriptor(
+ name='CopyIssueResponse',
+ full_name='monorail.CopyIssueResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='new_issue_ref', full_name='monorail.CopyIssueResponse.new_issue_ref', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=4814,
+ serialized_end=4876,
+)
+
+_CREATEISSUEREQUEST.fields_by_name['issue'].message_type = api_dot_api__proto_dot_issue__objects__pb2._ISSUE
+_GETISSUEREQUEST.fields_by_name['issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_ISSUERESPONSE.fields_by_name['issue'].message_type = api_dot_api__proto_dot_issue__objects__pb2._ISSUE
+_ISSUERESPONSE.fields_by_name['moved_to_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_LISTISSUESREQUEST.fields_by_name['pagination'].message_type = api_dot_api__proto_dot_common__pb2._PAGINATION
+_LISTISSUESRESPONSE.fields_by_name['issues'].message_type = api_dot_api__proto_dot_issue__objects__pb2._ISSUE
+_LISTREFERENCEDISSUESREQUEST.fields_by_name['issue_refs'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_LISTREFERENCEDISSUESRESPONSE.fields_by_name['open_refs'].message_type = api_dot_api__proto_dot_issue__objects__pb2._ISSUE
+_LISTREFERENCEDISSUESRESPONSE.fields_by_name['closed_refs'].message_type = api_dot_api__proto_dot_issue__objects__pb2._ISSUE
+_LISTAPPLICABLEFIELDDEFSREQUEST.fields_by_name['issue_refs'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_LISTAPPLICABLEFIELDDEFSRESPONSE.fields_by_name['field_defs'].message_type = api_dot_api__proto_dot_project__objects__pb2._FIELDDEF
+_UPDATEISSUEREQUEST.fields_by_name['issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_UPDATEISSUEREQUEST.fields_by_name['delta'].message_type = api_dot_api__proto_dot_issue__objects__pb2._ISSUEDELTA
+_UPDATEISSUEREQUEST.fields_by_name['uploads'].message_type = api_dot_api__proto_dot_issue__objects__pb2._ATTACHMENTUPLOAD
+_STARISSUEREQUEST.fields_by_name['issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_ISISSUESTARREDREQUEST.fields_by_name['issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_LISTSTARREDISSUESRESPONSE.fields_by_name['starred_issue_refs'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_LISTCOMMENTSREQUEST.fields_by_name['issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_LISTCOMMENTSRESPONSE.fields_by_name['comments'].message_type = api_dot_api__proto_dot_issue__objects__pb2._COMMENT
+_LISTACTIVITIESREQUEST.fields_by_name['user_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_LISTACTIVITIESRESPONSE.fields_by_name['comments'].message_type = api_dot_api__proto_dot_issue__objects__pb2._COMMENT
+_LISTACTIVITIESRESPONSE.fields_by_name['issue_summaries'].message_type = api_dot_api__proto_dot_issue__objects__pb2._ISSUESUMMARY
+_DELETECOMMENTREQUEST.fields_by_name['issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_BULKUPDATEAPPROVALSREQUEST.fields_by_name['issue_refs'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_BULKUPDATEAPPROVALSREQUEST.fields_by_name['field_ref'].message_type = api_dot_api__proto_dot_common__pb2._FIELDREF
+_BULKUPDATEAPPROVALSREQUEST.fields_by_name['approval_delta'].message_type = api_dot_api__proto_dot_issue__objects__pb2._APPROVALDELTA
+_BULKUPDATEAPPROVALSRESPONSE.fields_by_name['issue_refs'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_UPDATEAPPROVALREQUEST.fields_by_name['issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_UPDATEAPPROVALREQUEST.fields_by_name['field_ref'].message_type = api_dot_api__proto_dot_common__pb2._FIELDREF
+_UPDATEAPPROVALREQUEST.fields_by_name['approval_delta'].message_type = api_dot_api__proto_dot_issue__objects__pb2._APPROVALDELTA
+_UPDATEAPPROVALREQUEST.fields_by_name['uploads'].message_type = api_dot_api__proto_dot_issue__objects__pb2._ATTACHMENTUPLOAD
+_UPDATEAPPROVALRESPONSE.fields_by_name['approval'].message_type = api_dot_api__proto_dot_issue__objects__pb2._APPROVAL
+_CONVERTISSUEAPPROVALSTEMPLATEREQUEST.fields_by_name['issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_CONVERTISSUEAPPROVALSTEMPLATERESPONSE.fields_by_name['issue'].message_type = api_dot_api__proto_dot_issue__objects__pb2._ISSUE
+_ISSUESNAPSHOTRESPONSE.fields_by_name['snapshot_count'].message_type = _ISSUESNAPSHOTCOUNT
+_PRESUBMITISSUEREQUEST.fields_by_name['issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_PRESUBMITISSUEREQUEST.fields_by_name['issue_delta'].message_type = api_dot_api__proto_dot_issue__objects__pb2._ISSUEDELTA
+_PRESUBMITISSUERESPONSE.fields_by_name['derived_labels'].message_type = api_dot_api__proto_dot_common__pb2._VALUEANDWHY
+_PRESUBMITISSUERESPONSE.fields_by_name['derived_owners'].message_type = api_dot_api__proto_dot_common__pb2._VALUEANDWHY
+_PRESUBMITISSUERESPONSE.fields_by_name['derived_ccs'].message_type = api_dot_api__proto_dot_common__pb2._VALUEANDWHY
+_PRESUBMITISSUERESPONSE.fields_by_name['warnings'].message_type = api_dot_api__proto_dot_common__pb2._VALUEANDWHY
+_PRESUBMITISSUERESPONSE.fields_by_name['errors'].message_type = api_dot_api__proto_dot_common__pb2._VALUEANDWHY
+_RERANKBLOCKEDONISSUESREQUEST.fields_by_name['issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_RERANKBLOCKEDONISSUESREQUEST.fields_by_name['moved_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_RERANKBLOCKEDONISSUESREQUEST.fields_by_name['target_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_RERANKBLOCKEDONISSUESRESPONSE.fields_by_name['blocked_on_issue_refs'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_DELETEISSUEREQUEST.fields_by_name['issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_DELETEISSUECOMMENTREQUEST.fields_by_name['issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_DELETEATTACHMENTREQUEST.fields_by_name['issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_FLAGISSUESREQUEST.fields_by_name['issue_refs'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_FLAGCOMMENTREQUEST.fields_by_name['issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_LISTISSUEPERMISSIONSREQUEST.fields_by_name['issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_MOVEISSUEREQUEST.fields_by_name['issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_MOVEISSUERESPONSE.fields_by_name['new_issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_COPYISSUEREQUEST.fields_by_name['issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+_COPYISSUERESPONSE.fields_by_name['new_issue_ref'].message_type = api_dot_api__proto_dot_common__pb2._ISSUEREF
+DESCRIPTOR.message_types_by_name['CreateIssueRequest'] = _CREATEISSUEREQUEST
+DESCRIPTOR.message_types_by_name['GetIssueRequest'] = _GETISSUEREQUEST
+DESCRIPTOR.message_types_by_name['IssueResponse'] = _ISSUERESPONSE
+DESCRIPTOR.message_types_by_name['ListIssuesRequest'] = _LISTISSUESREQUEST
+DESCRIPTOR.message_types_by_name['ListIssuesResponse'] = _LISTISSUESRESPONSE
+DESCRIPTOR.message_types_by_name['ListReferencedIssuesRequest'] = _LISTREFERENCEDISSUESREQUEST
+DESCRIPTOR.message_types_by_name['ListReferencedIssuesResponse'] = _LISTREFERENCEDISSUESRESPONSE
+DESCRIPTOR.message_types_by_name['ListApplicableFieldDefsRequest'] = _LISTAPPLICABLEFIELDDEFSREQUEST
+DESCRIPTOR.message_types_by_name['ListApplicableFieldDefsResponse'] = _LISTAPPLICABLEFIELDDEFSRESPONSE
+DESCRIPTOR.message_types_by_name['UpdateIssueRequest'] = _UPDATEISSUEREQUEST
+DESCRIPTOR.message_types_by_name['StarIssueRequest'] = _STARISSUEREQUEST
+DESCRIPTOR.message_types_by_name['StarIssueResponse'] = _STARISSUERESPONSE
+DESCRIPTOR.message_types_by_name['IsIssueStarredRequest'] = _ISISSUESTARREDREQUEST
+DESCRIPTOR.message_types_by_name['IsIssueStarredResponse'] = _ISISSUESTARREDRESPONSE
+DESCRIPTOR.message_types_by_name['ListStarredIssuesRequest'] = _LISTSTARREDISSUESREQUEST
+DESCRIPTOR.message_types_by_name['ListStarredIssuesResponse'] = _LISTSTARREDISSUESRESPONSE
+DESCRIPTOR.message_types_by_name['ListCommentsRequest'] = _LISTCOMMENTSREQUEST
+DESCRIPTOR.message_types_by_name['ListCommentsResponse'] = _LISTCOMMENTSRESPONSE
+DESCRIPTOR.message_types_by_name['ListActivitiesRequest'] = _LISTACTIVITIESREQUEST
+DESCRIPTOR.message_types_by_name['ListActivitiesResponse'] = _LISTACTIVITIESRESPONSE
+DESCRIPTOR.message_types_by_name['DeleteCommentRequest'] = _DELETECOMMENTREQUEST
+DESCRIPTOR.message_types_by_name['BulkUpdateApprovalsRequest'] = _BULKUPDATEAPPROVALSREQUEST
+DESCRIPTOR.message_types_by_name['BulkUpdateApprovalsResponse'] = _BULKUPDATEAPPROVALSRESPONSE
+DESCRIPTOR.message_types_by_name['UpdateApprovalRequest'] = _UPDATEAPPROVALREQUEST
+DESCRIPTOR.message_types_by_name['UpdateApprovalResponse'] = _UPDATEAPPROVALRESPONSE
+DESCRIPTOR.message_types_by_name['ConvertIssueApprovalsTemplateRequest'] = _CONVERTISSUEAPPROVALSTEMPLATEREQUEST
+DESCRIPTOR.message_types_by_name['ConvertIssueApprovalsTemplateResponse'] = _CONVERTISSUEAPPROVALSTEMPLATERESPONSE
+DESCRIPTOR.message_types_by_name['IssueSnapshotRequest'] = _ISSUESNAPSHOTREQUEST
+DESCRIPTOR.message_types_by_name['IssueSnapshotCount'] = _ISSUESNAPSHOTCOUNT
+DESCRIPTOR.message_types_by_name['IssueSnapshotResponse'] = _ISSUESNAPSHOTRESPONSE
+DESCRIPTOR.message_types_by_name['PresubmitIssueRequest'] = _PRESUBMITISSUEREQUEST
+DESCRIPTOR.message_types_by_name['PresubmitIssueResponse'] = _PRESUBMITISSUERESPONSE
+DESCRIPTOR.message_types_by_name['RerankBlockedOnIssuesRequest'] = _RERANKBLOCKEDONISSUESREQUEST
+DESCRIPTOR.message_types_by_name['RerankBlockedOnIssuesResponse'] = _RERANKBLOCKEDONISSUESRESPONSE
+DESCRIPTOR.message_types_by_name['DeleteIssueRequest'] = _DELETEISSUEREQUEST
+DESCRIPTOR.message_types_by_name['DeleteIssueResponse'] = _DELETEISSUERESPONSE
+DESCRIPTOR.message_types_by_name['DeleteIssueCommentRequest'] = _DELETEISSUECOMMENTREQUEST
+DESCRIPTOR.message_types_by_name['DeleteIssueCommentResponse'] = _DELETEISSUECOMMENTRESPONSE
+DESCRIPTOR.message_types_by_name['DeleteAttachmentRequest'] = _DELETEATTACHMENTREQUEST
+DESCRIPTOR.message_types_by_name['DeleteAttachmentResponse'] = _DELETEATTACHMENTRESPONSE
+DESCRIPTOR.message_types_by_name['FlagIssuesRequest'] = _FLAGISSUESREQUEST
+DESCRIPTOR.message_types_by_name['FlagIssuesResponse'] = _FLAGISSUESRESPONSE
+DESCRIPTOR.message_types_by_name['FlagCommentRequest'] = _FLAGCOMMENTREQUEST
+DESCRIPTOR.message_types_by_name['FlagCommentResponse'] = _FLAGCOMMENTRESPONSE
+DESCRIPTOR.message_types_by_name['ListIssuePermissionsRequest'] = _LISTISSUEPERMISSIONSREQUEST
+DESCRIPTOR.message_types_by_name['ListIssuePermissionsResponse'] = _LISTISSUEPERMISSIONSRESPONSE
+DESCRIPTOR.message_types_by_name['MoveIssueRequest'] = _MOVEISSUEREQUEST
+DESCRIPTOR.message_types_by_name['MoveIssueResponse'] = _MOVEISSUERESPONSE
+DESCRIPTOR.message_types_by_name['CopyIssueRequest'] = _COPYISSUEREQUEST
+DESCRIPTOR.message_types_by_name['CopyIssueResponse'] = _COPYISSUERESPONSE
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+CreateIssueRequest = _reflection.GeneratedProtocolMessageType('CreateIssueRequest', (_message.Message,), dict(
+ DESCRIPTOR = _CREATEISSUEREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.CreateIssueRequest)
+ ))
+_sym_db.RegisterMessage(CreateIssueRequest)
+
+GetIssueRequest = _reflection.GeneratedProtocolMessageType('GetIssueRequest', (_message.Message,), dict(
+ DESCRIPTOR = _GETISSUEREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetIssueRequest)
+ ))
+_sym_db.RegisterMessage(GetIssueRequest)
+
+IssueResponse = _reflection.GeneratedProtocolMessageType('IssueResponse', (_message.Message,), dict(
+ DESCRIPTOR = _ISSUERESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.IssueResponse)
+ ))
+_sym_db.RegisterMessage(IssueResponse)
+
+ListIssuesRequest = _reflection.GeneratedProtocolMessageType('ListIssuesRequest', (_message.Message,), dict(
+ DESCRIPTOR = _LISTISSUESREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListIssuesRequest)
+ ))
+_sym_db.RegisterMessage(ListIssuesRequest)
+
+ListIssuesResponse = _reflection.GeneratedProtocolMessageType('ListIssuesResponse', (_message.Message,), dict(
+ DESCRIPTOR = _LISTISSUESRESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListIssuesResponse)
+ ))
+_sym_db.RegisterMessage(ListIssuesResponse)
+
+ListReferencedIssuesRequest = _reflection.GeneratedProtocolMessageType('ListReferencedIssuesRequest', (_message.Message,), dict(
+ DESCRIPTOR = _LISTREFERENCEDISSUESREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListReferencedIssuesRequest)
+ ))
+_sym_db.RegisterMessage(ListReferencedIssuesRequest)
+
+ListReferencedIssuesResponse = _reflection.GeneratedProtocolMessageType('ListReferencedIssuesResponse', (_message.Message,), dict(
+ DESCRIPTOR = _LISTREFERENCEDISSUESRESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListReferencedIssuesResponse)
+ ))
+_sym_db.RegisterMessage(ListReferencedIssuesResponse)
+
+ListApplicableFieldDefsRequest = _reflection.GeneratedProtocolMessageType('ListApplicableFieldDefsRequest', (_message.Message,), dict(
+ DESCRIPTOR = _LISTAPPLICABLEFIELDDEFSREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListApplicableFieldDefsRequest)
+ ))
+_sym_db.RegisterMessage(ListApplicableFieldDefsRequest)
+
+ListApplicableFieldDefsResponse = _reflection.GeneratedProtocolMessageType('ListApplicableFieldDefsResponse', (_message.Message,), dict(
+ DESCRIPTOR = _LISTAPPLICABLEFIELDDEFSRESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListApplicableFieldDefsResponse)
+ ))
+_sym_db.RegisterMessage(ListApplicableFieldDefsResponse)
+
+UpdateIssueRequest = _reflection.GeneratedProtocolMessageType('UpdateIssueRequest', (_message.Message,), dict(
+ DESCRIPTOR = _UPDATEISSUEREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.UpdateIssueRequest)
+ ))
+_sym_db.RegisterMessage(UpdateIssueRequest)
+
+StarIssueRequest = _reflection.GeneratedProtocolMessageType('StarIssueRequest', (_message.Message,), dict(
+ DESCRIPTOR = _STARISSUEREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.StarIssueRequest)
+ ))
+_sym_db.RegisterMessage(StarIssueRequest)
+
+StarIssueResponse = _reflection.GeneratedProtocolMessageType('StarIssueResponse', (_message.Message,), dict(
+ DESCRIPTOR = _STARISSUERESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.StarIssueResponse)
+ ))
+_sym_db.RegisterMessage(StarIssueResponse)
+
+IsIssueStarredRequest = _reflection.GeneratedProtocolMessageType('IsIssueStarredRequest', (_message.Message,), dict(
+ DESCRIPTOR = _ISISSUESTARREDREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.IsIssueStarredRequest)
+ ))
+_sym_db.RegisterMessage(IsIssueStarredRequest)
+
+IsIssueStarredResponse = _reflection.GeneratedProtocolMessageType('IsIssueStarredResponse', (_message.Message,), dict(
+ DESCRIPTOR = _ISISSUESTARREDRESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.IsIssueStarredResponse)
+ ))
+_sym_db.RegisterMessage(IsIssueStarredResponse)
+
+ListStarredIssuesRequest = _reflection.GeneratedProtocolMessageType('ListStarredIssuesRequest', (_message.Message,), dict(
+ DESCRIPTOR = _LISTSTARREDISSUESREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListStarredIssuesRequest)
+ ))
+_sym_db.RegisterMessage(ListStarredIssuesRequest)
+
+ListStarredIssuesResponse = _reflection.GeneratedProtocolMessageType('ListStarredIssuesResponse', (_message.Message,), dict(
+ DESCRIPTOR = _LISTSTARREDISSUESRESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListStarredIssuesResponse)
+ ))
+_sym_db.RegisterMessage(ListStarredIssuesResponse)
+
+ListCommentsRequest = _reflection.GeneratedProtocolMessageType('ListCommentsRequest', (_message.Message,), dict(
+ DESCRIPTOR = _LISTCOMMENTSREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListCommentsRequest)
+ ))
+_sym_db.RegisterMessage(ListCommentsRequest)
+
+ListCommentsResponse = _reflection.GeneratedProtocolMessageType('ListCommentsResponse', (_message.Message,), dict(
+ DESCRIPTOR = _LISTCOMMENTSRESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListCommentsResponse)
+ ))
+_sym_db.RegisterMessage(ListCommentsResponse)
+
+ListActivitiesRequest = _reflection.GeneratedProtocolMessageType('ListActivitiesRequest', (_message.Message,), dict(
+ DESCRIPTOR = _LISTACTIVITIESREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListActivitiesRequest)
+ ))
+_sym_db.RegisterMessage(ListActivitiesRequest)
+
+ListActivitiesResponse = _reflection.GeneratedProtocolMessageType('ListActivitiesResponse', (_message.Message,), dict(
+ DESCRIPTOR = _LISTACTIVITIESRESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListActivitiesResponse)
+ ))
+_sym_db.RegisterMessage(ListActivitiesResponse)
+
+DeleteCommentRequest = _reflection.GeneratedProtocolMessageType('DeleteCommentRequest', (_message.Message,), dict(
+ DESCRIPTOR = _DELETECOMMENTREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.DeleteCommentRequest)
+ ))
+_sym_db.RegisterMessage(DeleteCommentRequest)
+
+BulkUpdateApprovalsRequest = _reflection.GeneratedProtocolMessageType('BulkUpdateApprovalsRequest', (_message.Message,), dict(
+ DESCRIPTOR = _BULKUPDATEAPPROVALSREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.BulkUpdateApprovalsRequest)
+ ))
+_sym_db.RegisterMessage(BulkUpdateApprovalsRequest)
+
+BulkUpdateApprovalsResponse = _reflection.GeneratedProtocolMessageType('BulkUpdateApprovalsResponse', (_message.Message,), dict(
+ DESCRIPTOR = _BULKUPDATEAPPROVALSRESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.BulkUpdateApprovalsResponse)
+ ))
+_sym_db.RegisterMessage(BulkUpdateApprovalsResponse)
+
+UpdateApprovalRequest = _reflection.GeneratedProtocolMessageType('UpdateApprovalRequest', (_message.Message,), dict(
+ DESCRIPTOR = _UPDATEAPPROVALREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.UpdateApprovalRequest)
+ ))
+_sym_db.RegisterMessage(UpdateApprovalRequest)
+
+UpdateApprovalResponse = _reflection.GeneratedProtocolMessageType('UpdateApprovalResponse', (_message.Message,), dict(
+ DESCRIPTOR = _UPDATEAPPROVALRESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.UpdateApprovalResponse)
+ ))
+_sym_db.RegisterMessage(UpdateApprovalResponse)
+
+ConvertIssueApprovalsTemplateRequest = _reflection.GeneratedProtocolMessageType('ConvertIssueApprovalsTemplateRequest', (_message.Message,), dict(
+ DESCRIPTOR = _CONVERTISSUEAPPROVALSTEMPLATEREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ConvertIssueApprovalsTemplateRequest)
+ ))
+_sym_db.RegisterMessage(ConvertIssueApprovalsTemplateRequest)
+
+ConvertIssueApprovalsTemplateResponse = _reflection.GeneratedProtocolMessageType('ConvertIssueApprovalsTemplateResponse', (_message.Message,), dict(
+ DESCRIPTOR = _CONVERTISSUEAPPROVALSTEMPLATERESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ConvertIssueApprovalsTemplateResponse)
+ ))
+_sym_db.RegisterMessage(ConvertIssueApprovalsTemplateResponse)
+
+IssueSnapshotRequest = _reflection.GeneratedProtocolMessageType('IssueSnapshotRequest', (_message.Message,), dict(
+ DESCRIPTOR = _ISSUESNAPSHOTREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.IssueSnapshotRequest)
+ ))
+_sym_db.RegisterMessage(IssueSnapshotRequest)
+
+IssueSnapshotCount = _reflection.GeneratedProtocolMessageType('IssueSnapshotCount', (_message.Message,), dict(
+ DESCRIPTOR = _ISSUESNAPSHOTCOUNT,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.IssueSnapshotCount)
+ ))
+_sym_db.RegisterMessage(IssueSnapshotCount)
+
+IssueSnapshotResponse = _reflection.GeneratedProtocolMessageType('IssueSnapshotResponse', (_message.Message,), dict(
+ DESCRIPTOR = _ISSUESNAPSHOTRESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.IssueSnapshotResponse)
+ ))
+_sym_db.RegisterMessage(IssueSnapshotResponse)
+
+PresubmitIssueRequest = _reflection.GeneratedProtocolMessageType('PresubmitIssueRequest', (_message.Message,), dict(
+ DESCRIPTOR = _PRESUBMITISSUEREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.PresubmitIssueRequest)
+ ))
+_sym_db.RegisterMessage(PresubmitIssueRequest)
+
+PresubmitIssueResponse = _reflection.GeneratedProtocolMessageType('PresubmitIssueResponse', (_message.Message,), dict(
+ DESCRIPTOR = _PRESUBMITISSUERESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.PresubmitIssueResponse)
+ ))
+_sym_db.RegisterMessage(PresubmitIssueResponse)
+
+RerankBlockedOnIssuesRequest = _reflection.GeneratedProtocolMessageType('RerankBlockedOnIssuesRequest', (_message.Message,), dict(
+ DESCRIPTOR = _RERANKBLOCKEDONISSUESREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.RerankBlockedOnIssuesRequest)
+ ))
+_sym_db.RegisterMessage(RerankBlockedOnIssuesRequest)
+
+RerankBlockedOnIssuesResponse = _reflection.GeneratedProtocolMessageType('RerankBlockedOnIssuesResponse', (_message.Message,), dict(
+ DESCRIPTOR = _RERANKBLOCKEDONISSUESRESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.RerankBlockedOnIssuesResponse)
+ ))
+_sym_db.RegisterMessage(RerankBlockedOnIssuesResponse)
+
+DeleteIssueRequest = _reflection.GeneratedProtocolMessageType('DeleteIssueRequest', (_message.Message,), dict(
+ DESCRIPTOR = _DELETEISSUEREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.DeleteIssueRequest)
+ ))
+_sym_db.RegisterMessage(DeleteIssueRequest)
+
+DeleteIssueResponse = _reflection.GeneratedProtocolMessageType('DeleteIssueResponse', (_message.Message,), dict(
+ DESCRIPTOR = _DELETEISSUERESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.DeleteIssueResponse)
+ ))
+_sym_db.RegisterMessage(DeleteIssueResponse)
+
+DeleteIssueCommentRequest = _reflection.GeneratedProtocolMessageType('DeleteIssueCommentRequest', (_message.Message,), dict(
+ DESCRIPTOR = _DELETEISSUECOMMENTREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.DeleteIssueCommentRequest)
+ ))
+_sym_db.RegisterMessage(DeleteIssueCommentRequest)
+
+DeleteIssueCommentResponse = _reflection.GeneratedProtocolMessageType('DeleteIssueCommentResponse', (_message.Message,), dict(
+ DESCRIPTOR = _DELETEISSUECOMMENTRESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.DeleteIssueCommentResponse)
+ ))
+_sym_db.RegisterMessage(DeleteIssueCommentResponse)
+
+DeleteAttachmentRequest = _reflection.GeneratedProtocolMessageType('DeleteAttachmentRequest', (_message.Message,), dict(
+ DESCRIPTOR = _DELETEATTACHMENTREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.DeleteAttachmentRequest)
+ ))
+_sym_db.RegisterMessage(DeleteAttachmentRequest)
+
+DeleteAttachmentResponse = _reflection.GeneratedProtocolMessageType('DeleteAttachmentResponse', (_message.Message,), dict(
+ DESCRIPTOR = _DELETEATTACHMENTRESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.DeleteAttachmentResponse)
+ ))
+_sym_db.RegisterMessage(DeleteAttachmentResponse)
+
+FlagIssuesRequest = _reflection.GeneratedProtocolMessageType('FlagIssuesRequest', (_message.Message,), dict(
+ DESCRIPTOR = _FLAGISSUESREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.FlagIssuesRequest)
+ ))
+_sym_db.RegisterMessage(FlagIssuesRequest)
+
+FlagIssuesResponse = _reflection.GeneratedProtocolMessageType('FlagIssuesResponse', (_message.Message,), dict(
+ DESCRIPTOR = _FLAGISSUESRESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.FlagIssuesResponse)
+ ))
+_sym_db.RegisterMessage(FlagIssuesResponse)
+
+FlagCommentRequest = _reflection.GeneratedProtocolMessageType('FlagCommentRequest', (_message.Message,), dict(
+ DESCRIPTOR = _FLAGCOMMENTREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.FlagCommentRequest)
+ ))
+_sym_db.RegisterMessage(FlagCommentRequest)
+
+FlagCommentResponse = _reflection.GeneratedProtocolMessageType('FlagCommentResponse', (_message.Message,), dict(
+ DESCRIPTOR = _FLAGCOMMENTRESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.FlagCommentResponse)
+ ))
+_sym_db.RegisterMessage(FlagCommentResponse)
+
+ListIssuePermissionsRequest = _reflection.GeneratedProtocolMessageType('ListIssuePermissionsRequest', (_message.Message,), dict(
+ DESCRIPTOR = _LISTISSUEPERMISSIONSREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListIssuePermissionsRequest)
+ ))
+_sym_db.RegisterMessage(ListIssuePermissionsRequest)
+
+ListIssuePermissionsResponse = _reflection.GeneratedProtocolMessageType('ListIssuePermissionsResponse', (_message.Message,), dict(
+ DESCRIPTOR = _LISTISSUEPERMISSIONSRESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListIssuePermissionsResponse)
+ ))
+_sym_db.RegisterMessage(ListIssuePermissionsResponse)
+
+MoveIssueRequest = _reflection.GeneratedProtocolMessageType('MoveIssueRequest', (_message.Message,), dict(
+ DESCRIPTOR = _MOVEISSUEREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.MoveIssueRequest)
+ ))
+_sym_db.RegisterMessage(MoveIssueRequest)
+
+MoveIssueResponse = _reflection.GeneratedProtocolMessageType('MoveIssueResponse', (_message.Message,), dict(
+ DESCRIPTOR = _MOVEISSUERESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.MoveIssueResponse)
+ ))
+_sym_db.RegisterMessage(MoveIssueResponse)
+
+CopyIssueRequest = _reflection.GeneratedProtocolMessageType('CopyIssueRequest', (_message.Message,), dict(
+ DESCRIPTOR = _COPYISSUEREQUEST,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.CopyIssueRequest)
+ ))
+_sym_db.RegisterMessage(CopyIssueRequest)
+
+CopyIssueResponse = _reflection.GeneratedProtocolMessageType('CopyIssueResponse', (_message.Message,), dict(
+ DESCRIPTOR = _COPYISSUERESPONSE,
+ __module__ = 'api.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.CopyIssueResponse)
+ ))
+_sym_db.RegisterMessage(CopyIssueResponse)
+
+
+
+_ISSUES = _descriptor.ServiceDescriptor(
+ name='Issues',
+ full_name='monorail.Issues',
+ file=DESCRIPTOR,
+ index=0,
+ serialized_options=None,
+ serialized_start=4879,
+ serialized_end=7162,
+ methods=[
+ _descriptor.MethodDescriptor(
+ name='CreateIssue',
+ full_name='monorail.Issues.CreateIssue',
+ index=0,
+ containing_service=None,
+ input_type=_CREATEISSUEREQUEST,
+ output_type=_ISSUERESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='GetIssue',
+ full_name='monorail.Issues.GetIssue',
+ index=1,
+ containing_service=None,
+ input_type=_GETISSUEREQUEST,
+ output_type=_ISSUERESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListIssues',
+ full_name='monorail.Issues.ListIssues',
+ index=2,
+ containing_service=None,
+ input_type=_LISTISSUESREQUEST,
+ output_type=_LISTISSUESRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListReferencedIssues',
+ full_name='monorail.Issues.ListReferencedIssues',
+ index=3,
+ containing_service=None,
+ input_type=_LISTREFERENCEDISSUESREQUEST,
+ output_type=_LISTREFERENCEDISSUESRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListApplicableFieldDefs',
+ full_name='monorail.Issues.ListApplicableFieldDefs',
+ index=4,
+ containing_service=None,
+ input_type=_LISTAPPLICABLEFIELDDEFSREQUEST,
+ output_type=_LISTAPPLICABLEFIELDDEFSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='UpdateIssue',
+ full_name='monorail.Issues.UpdateIssue',
+ index=5,
+ containing_service=None,
+ input_type=_UPDATEISSUEREQUEST,
+ output_type=_ISSUERESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='StarIssue',
+ full_name='monorail.Issues.StarIssue',
+ index=6,
+ containing_service=None,
+ input_type=_STARISSUEREQUEST,
+ output_type=_STARISSUERESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='IsIssueStarred',
+ full_name='monorail.Issues.IsIssueStarred',
+ index=7,
+ containing_service=None,
+ input_type=_ISISSUESTARREDREQUEST,
+ output_type=_ISISSUESTARREDRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListStarredIssues',
+ full_name='monorail.Issues.ListStarredIssues',
+ index=8,
+ containing_service=None,
+ input_type=_LISTSTARREDISSUESREQUEST,
+ output_type=_LISTSTARREDISSUESRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListComments',
+ full_name='monorail.Issues.ListComments',
+ index=9,
+ containing_service=None,
+ input_type=_LISTCOMMENTSREQUEST,
+ output_type=_LISTCOMMENTSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListActivities',
+ full_name='monorail.Issues.ListActivities',
+ index=10,
+ containing_service=None,
+ input_type=_LISTACTIVITIESREQUEST,
+ output_type=_LISTACTIVITIESRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='DeleteComment',
+ full_name='monorail.Issues.DeleteComment',
+ index=11,
+ containing_service=None,
+ input_type=_DELETECOMMENTREQUEST,
+ output_type=google_dot_protobuf_dot_empty__pb2._EMPTY,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='BulkUpdateApprovals',
+ full_name='monorail.Issues.BulkUpdateApprovals',
+ index=12,
+ containing_service=None,
+ input_type=_BULKUPDATEAPPROVALSREQUEST,
+ output_type=_BULKUPDATEAPPROVALSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='UpdateApproval',
+ full_name='monorail.Issues.UpdateApproval',
+ index=13,
+ containing_service=None,
+ input_type=_UPDATEAPPROVALREQUEST,
+ output_type=_UPDATEAPPROVALRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ConvertIssueApprovalsTemplate',
+ full_name='monorail.Issues.ConvertIssueApprovalsTemplate',
+ index=14,
+ containing_service=None,
+ input_type=_CONVERTISSUEAPPROVALSTEMPLATEREQUEST,
+ output_type=_CONVERTISSUEAPPROVALSTEMPLATERESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='IssueSnapshot',
+ full_name='monorail.Issues.IssueSnapshot',
+ index=15,
+ containing_service=None,
+ input_type=_ISSUESNAPSHOTREQUEST,
+ output_type=_ISSUESNAPSHOTRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='PresubmitIssue',
+ full_name='monorail.Issues.PresubmitIssue',
+ index=16,
+ containing_service=None,
+ input_type=_PRESUBMITISSUEREQUEST,
+ output_type=_PRESUBMITISSUERESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='RerankBlockedOnIssues',
+ full_name='monorail.Issues.RerankBlockedOnIssues',
+ index=17,
+ containing_service=None,
+ input_type=_RERANKBLOCKEDONISSUESREQUEST,
+ output_type=_RERANKBLOCKEDONISSUESRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='DeleteIssue',
+ full_name='monorail.Issues.DeleteIssue',
+ index=18,
+ containing_service=None,
+ input_type=_DELETEISSUEREQUEST,
+ output_type=_DELETEISSUERESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='DeleteIssueComment',
+ full_name='monorail.Issues.DeleteIssueComment',
+ index=19,
+ containing_service=None,
+ input_type=_DELETEISSUECOMMENTREQUEST,
+ output_type=_DELETEISSUECOMMENTRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='DeleteAttachment',
+ full_name='monorail.Issues.DeleteAttachment',
+ index=20,
+ containing_service=None,
+ input_type=_DELETEATTACHMENTREQUEST,
+ output_type=_DELETEATTACHMENTRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='FlagIssues',
+ full_name='monorail.Issues.FlagIssues',
+ index=21,
+ containing_service=None,
+ input_type=_FLAGISSUESREQUEST,
+ output_type=_FLAGISSUESRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='FlagComment',
+ full_name='monorail.Issues.FlagComment',
+ index=22,
+ containing_service=None,
+ input_type=_FLAGCOMMENTREQUEST,
+ output_type=_FLAGCOMMENTRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListIssuePermissions',
+ full_name='monorail.Issues.ListIssuePermissions',
+ index=23,
+ containing_service=None,
+ input_type=_LISTISSUEPERMISSIONSREQUEST,
+ output_type=_LISTISSUEPERMISSIONSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='MoveIssue',
+ full_name='monorail.Issues.MoveIssue',
+ index=24,
+ containing_service=None,
+ input_type=_MOVEISSUEREQUEST,
+ output_type=_MOVEISSUERESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='CopyIssue',
+ full_name='monorail.Issues.CopyIssue',
+ index=25,
+ containing_service=None,
+ input_type=_COPYISSUEREQUEST,
+ output_type=_COPYISSUERESPONSE,
+ serialized_options=None,
+ ),
+])
+_sym_db.RegisterServiceDescriptor(_ISSUES)
+
+DESCRIPTOR.services_by_name['Issues'] = _ISSUES
+
+# @@protoc_insertion_point(module_scope)
diff --git a/api/api_proto/issues_prpc_pb2.py b/api/api_proto/issues_prpc_pb2.py
new file mode 100644
index 0000000..942730d
--- /dev/null
+++ b/api/api_proto/issues_prpc_pb2.py
@@ -0,0 +1,403 @@
+# Generated by the pRPC protocol buffer compiler plugin. DO NOT EDIT!
+# source: api/api_proto/issues.proto
+
+import base64
+import zlib
+
+from google.protobuf import descriptor_pb2
+
+# Includes description of the api/api_proto/issues.proto and all of its transitive
+# dependencies. Includes source code info.
+FILE_DESCRIPTOR_SET = descriptor_pb2.FileDescriptorSet()
+FILE_DESCRIPTOR_SET.ParseFromString(zlib.decompress(base64.b64decode(
+ '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')))
+_INDEX = {
+ f.name: {
+ 'descriptor': f,
+ 'services': {s.name: s for s in f.service},
+ }
+ for f in FILE_DESCRIPTOR_SET.file
+}
+
+
+IssuesServiceDescription = {
+ 'file_descriptor_set': FILE_DESCRIPTOR_SET,
+ 'file_descriptor': _INDEX[u'api/api_proto/issues.proto']['descriptor'],
+ 'service_descriptor': _INDEX[u'api/api_proto/issues.proto']['services'][u'Issues'],
+}
diff --git a/api/api_proto/project_objects.proto b/api/api_proto/project_objects.proto
new file mode 100644
index 0000000..a6943ec
--- /dev/null
+++ b/api/api_proto/project_objects.proto
@@ -0,0 +1,136 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+// This file defines protobufs for issues and related business
+// objects, e.g., field values, comments, and attachments.
+
+syntax = "proto3";
+
+package monorail;
+
+import "api/api_proto/common.proto";
+import "api/api_proto/issue_objects.proto";
+
+
+// Next available tag: 4
+message Project {
+ string name = 1;
+ string summary = 2;
+ string description = 3;
+}
+
+
+// Next available tag: 6
+message StatusDef {
+ string status = 1;
+ bool means_open = 2;
+ uint32 rank = 3;
+ string docstring = 4;
+ bool deprecated = 5;
+}
+
+
+// Next available tag: 5
+message LabelDef {
+ string label = 1;
+ string docstring = 3;
+ bool deprecated = 4;
+}
+
+
+// Next available tag: 11
+message ComponentDef {
+ string path = 1;
+ string docstring = 2;
+ repeated UserRef admin_refs = 3;
+ repeated UserRef cc_refs = 4;
+ bool deprecated = 5;
+ fixed32 created = 6;
+ UserRef creator_ref = 7;
+ fixed32 modified = 8;
+ UserRef modifier_ref = 9;
+ repeated LabelRef label_refs = 10;
+}
+
+
+// Next available tag: 9
+message FieldDef {
+ FieldRef field_ref = 1;
+ string applicable_type = 2;
+ // TODO(jrobbins): applicable_predicate
+ bool is_required = 3;
+ bool is_niche = 4;
+ bool is_multivalued = 5;
+ string docstring = 6;
+ repeated UserRef admin_refs = 7;
+ // TODO(jrobbins): validation, permission granting, and notification options.
+ bool is_phase_field = 8;
+ repeated UserRef user_choices = 9;
+ repeated LabelDef enum_choices = 10;
+}
+
+
+// Next available tag: 3
+message FieldOptions {
+ FieldRef field_ref = 1;
+ repeated UserRef user_refs = 2;
+}
+
+
+// Next available tag: 4
+message ApprovalDef {
+ FieldRef field_ref = 1;
+ repeated UserRef approver_refs = 2;
+ string survey = 3;
+}
+
+
+// Next available tag: 11
+message Config {
+ string project_name = 1;
+ repeated StatusDef status_defs = 2;
+ repeated StatusRef statuses_offer_merge = 3;
+ repeated LabelDef label_defs = 4;
+ repeated string exclusive_label_prefixes = 5;
+ repeated ComponentDef component_defs = 6;
+ repeated FieldDef field_defs = 7;
+ repeated ApprovalDef approval_defs = 8;
+ bool restrict_to_known = 9;
+}
+
+
+// Next available tag: 11
+message PresentationConfig {
+ string project_thumbnail_url = 1;
+ string project_summary = 2;
+ string custom_issue_entry_url = 3;
+ string default_query = 4;
+ repeated SavedQuery saved_queries = 5;
+ string revision_url_format = 6;
+ string default_col_spec = 7;
+ string default_sort_spec = 8;
+ string default_x_attr = 9;
+ string default_y_attr = 10;
+}
+
+
+// Next available tag: 16
+message TemplateDef {
+ string template_name = 1;
+ string content = 2;
+ string summary = 3;
+ bool summary_must_be_edited = 4;
+ UserRef owner_ref = 5;
+ StatusRef status_ref = 6;
+ repeated LabelRef label_refs = 7;
+ bool members_only = 8;
+ bool owner_defaults_to_member = 9;
+ repeated UserRef admin_refs = 10;
+ repeated FieldValue field_values = 11;
+ repeated ComponentRef component_refs = 12;
+ bool component_required = 13;
+ repeated Approval approval_values = 14;
+ repeated PhaseDef phases = 15;
+}
diff --git a/api/api_proto/project_objects_pb2.py b/api/api_proto/project_objects_pb2.py
new file mode 100644
index 0000000..580810a
--- /dev/null
+++ b/api/api_proto/project_objects_pb2.py
@@ -0,0 +1,871 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/api_proto/project_objects.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+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()
+
+
+from api.api_proto import common_pb2 as api_dot_api__proto_dot_common__pb2
+from api.api_proto import issue_objects_pb2 as api_dot_api__proto_dot_issue__objects__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='api/api_proto/project_objects.proto',
+ package='monorail',
+ syntax='proto3',
+ serialized_options=None,
+ serialized_pb=_b('\n#api/api_proto/project_objects.proto\x12\x08monorail\x1a\x1a\x61pi/api_proto/common.proto\x1a!api/api_proto/issue_objects.proto\"=\n\x07Project\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07summary\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\"d\n\tStatusDef\x12\x0e\n\x06status\x18\x01 \x01(\t\x12\x12\n\nmeans_open\x18\x02 \x01(\x08\x12\x0c\n\x04rank\x18\x03 \x01(\r\x12\x11\n\tdocstring\x18\x04 \x01(\t\x12\x12\n\ndeprecated\x18\x05 \x01(\x08\"@\n\x08LabelDef\x12\r\n\x05label\x18\x01 \x01(\t\x12\x11\n\tdocstring\x18\x03 \x01(\t\x12\x12\n\ndeprecated\x18\x04 \x01(\x08\"\xaa\x02\n\x0c\x43omponentDef\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x11\n\tdocstring\x18\x02 \x01(\t\x12%\n\nadmin_refs\x18\x03 \x03(\x0b\x32\x11.monorail.UserRef\x12\"\n\x07\x63\x63_refs\x18\x04 \x03(\x0b\x32\x11.monorail.UserRef\x12\x12\n\ndeprecated\x18\x05 \x01(\x08\x12\x0f\n\x07\x63reated\x18\x06 \x01(\x07\x12&\n\x0b\x63reator_ref\x18\x07 \x01(\x0b\x32\x11.monorail.UserRef\x12\x10\n\x08modified\x18\x08 \x01(\x07\x12\'\n\x0cmodifier_ref\x18\t \x01(\x0b\x32\x11.monorail.UserRef\x12&\n\nlabel_refs\x18\n \x03(\x0b\x32\x12.monorail.LabelRef\"\xae\x02\n\x08\x46ieldDef\x12%\n\tfield_ref\x18\x01 \x01(\x0b\x32\x12.monorail.FieldRef\x12\x17\n\x0f\x61pplicable_type\x18\x02 \x01(\t\x12\x13\n\x0bis_required\x18\x03 \x01(\x08\x12\x10\n\x08is_niche\x18\x04 \x01(\x08\x12\x16\n\x0eis_multivalued\x18\x05 \x01(\x08\x12\x11\n\tdocstring\x18\x06 \x01(\t\x12%\n\nadmin_refs\x18\x07 \x03(\x0b\x32\x11.monorail.UserRef\x12\x16\n\x0eis_phase_field\x18\x08 \x01(\x08\x12\'\n\x0cuser_choices\x18\t \x03(\x0b\x32\x11.monorail.UserRef\x12(\n\x0c\x65num_choices\x18\n \x03(\x0b\x32\x12.monorail.LabelDef\"[\n\x0c\x46ieldOptions\x12%\n\tfield_ref\x18\x01 \x01(\x0b\x32\x12.monorail.FieldRef\x12$\n\tuser_refs\x18\x02 \x03(\x0b\x32\x11.monorail.UserRef\"n\n\x0b\x41pprovalDef\x12%\n\tfield_ref\x18\x01 \x01(\x0b\x32\x12.monorail.FieldRef\x12(\n\rapprover_refs\x18\x02 \x03(\x0b\x32\x11.monorail.UserRef\x12\x0e\n\x06survey\x18\x03 \x01(\t\"\xe6\x02\n\x06\x43onfig\x12\x14\n\x0cproject_name\x18\x01 \x01(\t\x12(\n\x0bstatus_defs\x18\x02 \x03(\x0b\x32\x13.monorail.StatusDef\x12\x31\n\x14statuses_offer_merge\x18\x03 \x03(\x0b\x32\x13.monorail.StatusRef\x12&\n\nlabel_defs\x18\x04 \x03(\x0b\x32\x12.monorail.LabelDef\x12 \n\x18\x65xclusive_label_prefixes\x18\x05 \x03(\t\x12.\n\x0e\x63omponent_defs\x18\x06 \x03(\x0b\x32\x16.monorail.ComponentDef\x12&\n\nfield_defs\x18\x07 \x03(\x0b\x32\x12.monorail.FieldDef\x12,\n\rapproval_defs\x18\x08 \x03(\x0b\x32\x15.monorail.ApprovalDef\x12\x19\n\x11restrict_to_known\x18\t \x01(\x08\"\xb2\x02\n\x12PresentationConfig\x12\x1d\n\x15project_thumbnail_url\x18\x01 \x01(\t\x12\x17\n\x0fproject_summary\x18\x02 \x01(\t\x12\x1e\n\x16\x63ustom_issue_entry_url\x18\x03 \x01(\t\x12\x15\n\rdefault_query\x18\x04 \x01(\t\x12+\n\rsaved_queries\x18\x05 \x03(\x0b\x32\x14.monorail.SavedQuery\x12\x1b\n\x13revision_url_format\x18\x06 \x01(\t\x12\x18\n\x10\x64\x65\x66\x61ult_col_spec\x18\x07 \x01(\t\x12\x19\n\x11\x64\x65\x66\x61ult_sort_spec\x18\x08 \x01(\t\x12\x16\n\x0e\x64\x65\x66\x61ult_x_attr\x18\t \x01(\t\x12\x16\n\x0e\x64\x65\x66\x61ult_y_attr\x18\n \x01(\t\"\x85\x04\n\x0bTemplateDef\x12\x15\n\rtemplate_name\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\x12\x0f\n\x07summary\x18\x03 \x01(\t\x12\x1e\n\x16summary_must_be_edited\x18\x04 \x01(\x08\x12$\n\towner_ref\x18\x05 \x01(\x0b\x32\x11.monorail.UserRef\x12\'\n\nstatus_ref\x18\x06 \x01(\x0b\x32\x13.monorail.StatusRef\x12&\n\nlabel_refs\x18\x07 \x03(\x0b\x32\x12.monorail.LabelRef\x12\x14\n\x0cmembers_only\x18\x08 \x01(\x08\x12 \n\x18owner_defaults_to_member\x18\t \x01(\x08\x12%\n\nadmin_refs\x18\n \x03(\x0b\x32\x11.monorail.UserRef\x12*\n\x0c\x66ield_values\x18\x0b \x03(\x0b\x32\x14.monorail.FieldValue\x12.\n\x0e\x63omponent_refs\x18\x0c \x03(\x0b\x32\x16.monorail.ComponentRef\x12\x1a\n\x12\x63omponent_required\x18\r \x01(\x08\x12+\n\x0f\x61pproval_values\x18\x0e \x03(\x0b\x32\x12.monorail.Approval\x12\"\n\x06phases\x18\x0f \x03(\x0b\x32\x12.monorail.PhaseDefb\x06proto3')
+ ,
+ dependencies=[api_dot_api__proto_dot_common__pb2.DESCRIPTOR,api_dot_api__proto_dot_issue__objects__pb2.DESCRIPTOR,])
+
+
+
+
+_PROJECT = _descriptor.Descriptor(
+ name='Project',
+ full_name='monorail.Project',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.Project.name', 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),
+ _descriptor.FieldDescriptor(
+ name='summary', full_name='monorail.Project.summary', 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),
+ _descriptor.FieldDescriptor(
+ name='description', full_name='monorail.Project.description', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=112,
+ serialized_end=173,
+)
+
+
+_STATUSDEF = _descriptor.Descriptor(
+ name='StatusDef',
+ full_name='monorail.StatusDef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='status', full_name='monorail.StatusDef.status', 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),
+ _descriptor.FieldDescriptor(
+ name='means_open', full_name='monorail.StatusDef.means_open', index=1,
+ number=2, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='rank', full_name='monorail.StatusDef.rank', index=2,
+ number=3, type=13, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='docstring', full_name='monorail.StatusDef.docstring', 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),
+ _descriptor.FieldDescriptor(
+ name='deprecated', full_name='monorail.StatusDef.deprecated', index=4,
+ number=5, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=175,
+ serialized_end=275,
+)
+
+
+_LABELDEF = _descriptor.Descriptor(
+ name='LabelDef',
+ full_name='monorail.LabelDef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='label', full_name='monorail.LabelDef.label', 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),
+ _descriptor.FieldDescriptor(
+ name='docstring', full_name='monorail.LabelDef.docstring', index=1,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='deprecated', full_name='monorail.LabelDef.deprecated', index=2,
+ number=4, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=277,
+ serialized_end=341,
+)
+
+
+_COMPONENTDEF = _descriptor.Descriptor(
+ name='ComponentDef',
+ full_name='monorail.ComponentDef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='path', full_name='monorail.ComponentDef.path', 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),
+ _descriptor.FieldDescriptor(
+ name='docstring', full_name='monorail.ComponentDef.docstring', 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),
+ _descriptor.FieldDescriptor(
+ name='admin_refs', full_name='monorail.ComponentDef.admin_refs', index=2,
+ number=3, 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),
+ _descriptor.FieldDescriptor(
+ name='cc_refs', full_name='monorail.ComponentDef.cc_refs', index=3,
+ number=4, 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),
+ _descriptor.FieldDescriptor(
+ name='deprecated', full_name='monorail.ComponentDef.deprecated', index=4,
+ number=5, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='created', full_name='monorail.ComponentDef.created', index=5,
+ number=6, type=7, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='creator_ref', full_name='monorail.ComponentDef.creator_ref', index=6,
+ number=7, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='modified', full_name='monorail.ComponentDef.modified', index=7,
+ number=8, type=7, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='modifier_ref', full_name='monorail.ComponentDef.modifier_ref', index=8,
+ number=9, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='label_refs', full_name='monorail.ComponentDef.label_refs', index=9,
+ number=10, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=344,
+ serialized_end=642,
+)
+
+
+_FIELDDEF = _descriptor.Descriptor(
+ name='FieldDef',
+ full_name='monorail.FieldDef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='field_ref', full_name='monorail.FieldDef.field_ref', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='applicable_type', full_name='monorail.FieldDef.applicable_type', 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),
+ _descriptor.FieldDescriptor(
+ name='is_required', full_name='monorail.FieldDef.is_required', index=2,
+ number=3, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='is_niche', full_name='monorail.FieldDef.is_niche', index=3,
+ number=4, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='is_multivalued', full_name='monorail.FieldDef.is_multivalued', index=4,
+ number=5, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='docstring', full_name='monorail.FieldDef.docstring', 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),
+ _descriptor.FieldDescriptor(
+ name='admin_refs', full_name='monorail.FieldDef.admin_refs', index=6,
+ number=7, 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),
+ _descriptor.FieldDescriptor(
+ name='is_phase_field', full_name='monorail.FieldDef.is_phase_field', index=7,
+ number=8, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='user_choices', full_name='monorail.FieldDef.user_choices', index=8,
+ number=9, 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),
+ _descriptor.FieldDescriptor(
+ name='enum_choices', full_name='monorail.FieldDef.enum_choices', index=9,
+ number=10, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=645,
+ serialized_end=947,
+)
+
+
+_FIELDOPTIONS = _descriptor.Descriptor(
+ name='FieldOptions',
+ full_name='monorail.FieldOptions',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='field_ref', full_name='monorail.FieldOptions.field_ref', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='user_refs', full_name='monorail.FieldOptions.user_refs', index=1,
+ number=2, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=949,
+ serialized_end=1040,
+)
+
+
+_APPROVALDEF = _descriptor.Descriptor(
+ name='ApprovalDef',
+ full_name='monorail.ApprovalDef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='field_ref', full_name='monorail.ApprovalDef.field_ref', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='approver_refs', full_name='monorail.ApprovalDef.approver_refs', index=1,
+ number=2, 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),
+ _descriptor.FieldDescriptor(
+ name='survey', full_name='monorail.ApprovalDef.survey', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1042,
+ serialized_end=1152,
+)
+
+
+_CONFIG = _descriptor.Descriptor(
+ name='Config',
+ full_name='monorail.Config',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.Config.project_name', 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),
+ _descriptor.FieldDescriptor(
+ name='status_defs', full_name='monorail.Config.status_defs', index=1,
+ number=2, 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),
+ _descriptor.FieldDescriptor(
+ name='statuses_offer_merge', full_name='monorail.Config.statuses_offer_merge', index=2,
+ number=3, 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),
+ _descriptor.FieldDescriptor(
+ name='label_defs', full_name='monorail.Config.label_defs', index=3,
+ number=4, 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),
+ _descriptor.FieldDescriptor(
+ name='exclusive_label_prefixes', full_name='monorail.Config.exclusive_label_prefixes', 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),
+ _descriptor.FieldDescriptor(
+ name='component_defs', full_name='monorail.Config.component_defs', index=5,
+ number=6, 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),
+ _descriptor.FieldDescriptor(
+ name='field_defs', full_name='monorail.Config.field_defs', index=6,
+ number=7, 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),
+ _descriptor.FieldDescriptor(
+ name='approval_defs', full_name='monorail.Config.approval_defs', index=7,
+ number=8, 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),
+ _descriptor.FieldDescriptor(
+ name='restrict_to_known', full_name='monorail.Config.restrict_to_known', index=8,
+ number=9, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1155,
+ serialized_end=1513,
+)
+
+
+_PRESENTATIONCONFIG = _descriptor.Descriptor(
+ name='PresentationConfig',
+ full_name='monorail.PresentationConfig',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_thumbnail_url', full_name='monorail.PresentationConfig.project_thumbnail_url', 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),
+ _descriptor.FieldDescriptor(
+ name='project_summary', full_name='monorail.PresentationConfig.project_summary', 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),
+ _descriptor.FieldDescriptor(
+ name='custom_issue_entry_url', full_name='monorail.PresentationConfig.custom_issue_entry_url', 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),
+ _descriptor.FieldDescriptor(
+ name='default_query', full_name='monorail.PresentationConfig.default_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),
+ _descriptor.FieldDescriptor(
+ name='saved_queries', full_name='monorail.PresentationConfig.saved_queries', index=4,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='revision_url_format', full_name='monorail.PresentationConfig.revision_url_format', 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),
+ _descriptor.FieldDescriptor(
+ name='default_col_spec', full_name='monorail.PresentationConfig.default_col_spec', index=6,
+ number=7, 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),
+ _descriptor.FieldDescriptor(
+ name='default_sort_spec', full_name='monorail.PresentationConfig.default_sort_spec', index=7,
+ number=8, 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),
+ _descriptor.FieldDescriptor(
+ name='default_x_attr', full_name='monorail.PresentationConfig.default_x_attr', index=8,
+ number=9, 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),
+ _descriptor.FieldDescriptor(
+ name='default_y_attr', full_name='monorail.PresentationConfig.default_y_attr', index=9,
+ number=10, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1516,
+ serialized_end=1822,
+)
+
+
+_TEMPLATEDEF = _descriptor.Descriptor(
+ name='TemplateDef',
+ full_name='monorail.TemplateDef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='template_name', full_name='monorail.TemplateDef.template_name', 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),
+ _descriptor.FieldDescriptor(
+ name='content', full_name='monorail.TemplateDef.content', 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),
+ _descriptor.FieldDescriptor(
+ name='summary', full_name='monorail.TemplateDef.summary', 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),
+ _descriptor.FieldDescriptor(
+ name='summary_must_be_edited', full_name='monorail.TemplateDef.summary_must_be_edited', index=3,
+ number=4, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='owner_ref', full_name='monorail.TemplateDef.owner_ref', index=4,
+ number=5, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='status_ref', full_name='monorail.TemplateDef.status_ref', index=5,
+ number=6, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='label_refs', full_name='monorail.TemplateDef.label_refs', index=6,
+ number=7, 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),
+ _descriptor.FieldDescriptor(
+ name='members_only', full_name='monorail.TemplateDef.members_only', index=7,
+ number=8, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='owner_defaults_to_member', full_name='monorail.TemplateDef.owner_defaults_to_member', index=8,
+ number=9, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='admin_refs', full_name='monorail.TemplateDef.admin_refs', index=9,
+ number=10, 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),
+ _descriptor.FieldDescriptor(
+ name='field_values', full_name='monorail.TemplateDef.field_values', index=10,
+ number=11, 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),
+ _descriptor.FieldDescriptor(
+ name='component_refs', full_name='monorail.TemplateDef.component_refs', index=11,
+ number=12, 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),
+ _descriptor.FieldDescriptor(
+ name='component_required', full_name='monorail.TemplateDef.component_required', index=12,
+ number=13, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='approval_values', full_name='monorail.TemplateDef.approval_values', index=13,
+ number=14, 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),
+ _descriptor.FieldDescriptor(
+ name='phases', full_name='monorail.TemplateDef.phases', index=14,
+ number=15, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1825,
+ serialized_end=2342,
+)
+
+_COMPONENTDEF.fields_by_name['admin_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_COMPONENTDEF.fields_by_name['cc_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_COMPONENTDEF.fields_by_name['creator_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_COMPONENTDEF.fields_by_name['modifier_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_COMPONENTDEF.fields_by_name['label_refs'].message_type = api_dot_api__proto_dot_common__pb2._LABELREF
+_FIELDDEF.fields_by_name['field_ref'].message_type = api_dot_api__proto_dot_common__pb2._FIELDREF
+_FIELDDEF.fields_by_name['admin_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_FIELDDEF.fields_by_name['user_choices'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_FIELDDEF.fields_by_name['enum_choices'].message_type = _LABELDEF
+_FIELDOPTIONS.fields_by_name['field_ref'].message_type = api_dot_api__proto_dot_common__pb2._FIELDREF
+_FIELDOPTIONS.fields_by_name['user_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_APPROVALDEF.fields_by_name['field_ref'].message_type = api_dot_api__proto_dot_common__pb2._FIELDREF
+_APPROVALDEF.fields_by_name['approver_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_CONFIG.fields_by_name['status_defs'].message_type = _STATUSDEF
+_CONFIG.fields_by_name['statuses_offer_merge'].message_type = api_dot_api__proto_dot_common__pb2._STATUSREF
+_CONFIG.fields_by_name['label_defs'].message_type = _LABELDEF
+_CONFIG.fields_by_name['component_defs'].message_type = _COMPONENTDEF
+_CONFIG.fields_by_name['field_defs'].message_type = _FIELDDEF
+_CONFIG.fields_by_name['approval_defs'].message_type = _APPROVALDEF
+_PRESENTATIONCONFIG.fields_by_name['saved_queries'].message_type = api_dot_api__proto_dot_common__pb2._SAVEDQUERY
+_TEMPLATEDEF.fields_by_name['owner_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_TEMPLATEDEF.fields_by_name['status_ref'].message_type = api_dot_api__proto_dot_common__pb2._STATUSREF
+_TEMPLATEDEF.fields_by_name['label_refs'].message_type = api_dot_api__proto_dot_common__pb2._LABELREF
+_TEMPLATEDEF.fields_by_name['admin_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_TEMPLATEDEF.fields_by_name['field_values'].message_type = api_dot_api__proto_dot_issue__objects__pb2._FIELDVALUE
+_TEMPLATEDEF.fields_by_name['component_refs'].message_type = api_dot_api__proto_dot_common__pb2._COMPONENTREF
+_TEMPLATEDEF.fields_by_name['approval_values'].message_type = api_dot_api__proto_dot_issue__objects__pb2._APPROVAL
+_TEMPLATEDEF.fields_by_name['phases'].message_type = api_dot_api__proto_dot_issue__objects__pb2._PHASEDEF
+DESCRIPTOR.message_types_by_name['Project'] = _PROJECT
+DESCRIPTOR.message_types_by_name['StatusDef'] = _STATUSDEF
+DESCRIPTOR.message_types_by_name['LabelDef'] = _LABELDEF
+DESCRIPTOR.message_types_by_name['ComponentDef'] = _COMPONENTDEF
+DESCRIPTOR.message_types_by_name['FieldDef'] = _FIELDDEF
+DESCRIPTOR.message_types_by_name['FieldOptions'] = _FIELDOPTIONS
+DESCRIPTOR.message_types_by_name['ApprovalDef'] = _APPROVALDEF
+DESCRIPTOR.message_types_by_name['Config'] = _CONFIG
+DESCRIPTOR.message_types_by_name['PresentationConfig'] = _PRESENTATIONCONFIG
+DESCRIPTOR.message_types_by_name['TemplateDef'] = _TEMPLATEDEF
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+Project = _reflection.GeneratedProtocolMessageType('Project', (_message.Message,), dict(
+ DESCRIPTOR = _PROJECT,
+ __module__ = 'api.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.Project)
+ ))
+_sym_db.RegisterMessage(Project)
+
+StatusDef = _reflection.GeneratedProtocolMessageType('StatusDef', (_message.Message,), dict(
+ DESCRIPTOR = _STATUSDEF,
+ __module__ = 'api.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.StatusDef)
+ ))
+_sym_db.RegisterMessage(StatusDef)
+
+LabelDef = _reflection.GeneratedProtocolMessageType('LabelDef', (_message.Message,), dict(
+ DESCRIPTOR = _LABELDEF,
+ __module__ = 'api.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.LabelDef)
+ ))
+_sym_db.RegisterMessage(LabelDef)
+
+ComponentDef = _reflection.GeneratedProtocolMessageType('ComponentDef', (_message.Message,), dict(
+ DESCRIPTOR = _COMPONENTDEF,
+ __module__ = 'api.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ComponentDef)
+ ))
+_sym_db.RegisterMessage(ComponentDef)
+
+FieldDef = _reflection.GeneratedProtocolMessageType('FieldDef', (_message.Message,), dict(
+ DESCRIPTOR = _FIELDDEF,
+ __module__ = 'api.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.FieldDef)
+ ))
+_sym_db.RegisterMessage(FieldDef)
+
+FieldOptions = _reflection.GeneratedProtocolMessageType('FieldOptions', (_message.Message,), dict(
+ DESCRIPTOR = _FIELDOPTIONS,
+ __module__ = 'api.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.FieldOptions)
+ ))
+_sym_db.RegisterMessage(FieldOptions)
+
+ApprovalDef = _reflection.GeneratedProtocolMessageType('ApprovalDef', (_message.Message,), dict(
+ DESCRIPTOR = _APPROVALDEF,
+ __module__ = 'api.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ApprovalDef)
+ ))
+_sym_db.RegisterMessage(ApprovalDef)
+
+Config = _reflection.GeneratedProtocolMessageType('Config', (_message.Message,), dict(
+ DESCRIPTOR = _CONFIG,
+ __module__ = 'api.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.Config)
+ ))
+_sym_db.RegisterMessage(Config)
+
+PresentationConfig = _reflection.GeneratedProtocolMessageType('PresentationConfig', (_message.Message,), dict(
+ DESCRIPTOR = _PRESENTATIONCONFIG,
+ __module__ = 'api.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.PresentationConfig)
+ ))
+_sym_db.RegisterMessage(PresentationConfig)
+
+TemplateDef = _reflection.GeneratedProtocolMessageType('TemplateDef', (_message.Message,), dict(
+ DESCRIPTOR = _TEMPLATEDEF,
+ __module__ = 'api.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.TemplateDef)
+ ))
+_sym_db.RegisterMessage(TemplateDef)
+
+
+# @@protoc_insertion_point(module_scope)
diff --git a/api/api_proto/projects.proto b/api/api_proto/projects.proto
new file mode 100644
index 0000000..bcd1167
--- /dev/null
+++ b/api/api_proto/projects.proto
@@ -0,0 +1,211 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+syntax = "proto3";
+
+
+package monorail;
+
+import "api/api_proto/common.proto";
+import "api/api_proto/project_objects.proto";
+
+
+service Projects {
+ rpc ListProjects (ListProjectsRequest) returns (ListProjectsResponse) {}
+ rpc ListProjectTemplates (ListProjectTemplatesRequest) returns (ListProjectTemplatesResponse) {}
+ rpc GetConfig (GetConfigRequest) returns (Config) {}
+ rpc GetPresentationConfig (GetPresentationConfigRequest) returns (PresentationConfig) {}
+ rpc GetCustomPermissions (GetCustomPermissionsRequest) returns (GetCustomPermissionsResponse) {}
+ rpc GetVisibleMembers (GetVisibleMembersRequest) returns (GetVisibleMembersResponse) {}
+ rpc GetLabelOptions (GetLabelOptionsRequest) returns (GetLabelOptionsResponse) {}
+ rpc ListStatuses (ListStatusesRequest) returns (ListStatusesResponse) {}
+ rpc ListComponents (ListComponentsRequest) returns (ListComponentsResponse) {}
+ rpc ListFields (ListFieldsRequest) returns (ListFieldsResponse) {}
+ rpc GetProjectStarCount (GetProjectStarCountRequest) returns (GetProjectStarCountResponse) {}
+ rpc StarProject (StarProjectRequest) returns (StarProjectResponse) {}
+ rpc CheckProjectName (CheckProjectNameRequest) returns (CheckProjectNameResponse) {}
+ rpc CheckComponentName (CheckComponentNameRequest) returns (CheckComponentNameResponse) {}
+ rpc CheckFieldName (CheckFieldNameRequest) returns (CheckFieldNameResponse) {}
+}
+
+
+// Next available tag: 3
+message ListProjectsRequest {
+ int32 page_size = 1;
+ string page_token = 2;
+}
+
+
+// Next available tag: 3
+message ListProjectsResponse {
+ repeated Project projects = 1;
+ string next_page_token = 2;
+}
+
+
+// Next available tag: 3
+message ListProjectTemplatesRequest {
+ string project_name = 2;
+}
+
+
+// Next available tag: 2
+message ListProjectTemplatesResponse {
+ repeated TemplateDef templates = 1;
+}
+
+
+// Next available tag: 3
+message GetConfigRequest {
+ string project_name = 2;
+}
+
+// Next available tag: 3
+message GetPresentationConfigRequest {
+ string project_name = 2;
+}
+
+
+// Next available tag: 3
+message GetCustomPermissionsRequest {
+ string project_name = 2;
+}
+
+
+// Next available tag: 2
+message GetCustomPermissionsResponse {
+ repeated string permissions = 1;
+}
+
+
+// Next available tag: 3
+message GetVisibleMembersRequest {
+ string project_name = 2;
+}
+
+
+// Next available tag: 3
+message GetVisibleMembersResponse {
+ repeated UserRef user_refs = 1;
+ repeated UserRef group_refs = 2;
+}
+
+
+// Next available tag: 3
+message GetLabelOptionsRequest {
+ string project_name = 2;
+}
+
+
+// Next available tag: 3
+message GetLabelOptionsResponse {
+ repeated LabelDef label_options = 1;
+ repeated string exclusive_label_prefixes = 2;
+}
+
+
+// Next available tag: 3
+message ListStatusesRequest {
+ string project_name = 2;
+}
+
+
+// Next available tag: 4
+message ListStatusesResponse {
+ repeated StatusDef status_defs = 1;
+ repeated StatusRef statuses_offer_merge = 2;
+ bool restrict_to_known = 3;
+}
+
+
+// Next available tag: 4
+message ListComponentsRequest {
+ string project_name = 2;
+ bool include_admin_info = 3;
+}
+
+
+// Next available tag: 2
+message ListComponentsResponse {
+ repeated ComponentDef component_defs = 1;
+}
+
+
+// Next available tag: 5
+message ListFieldsRequest {
+ string project_name = 2;
+ bool include_admin_info = 3;
+ bool include_user_choices = 4;
+}
+
+
+// Next available tag: 2
+message ListFieldsResponse {
+ repeated FieldDef field_defs = 1;
+}
+
+
+// Next available tag: 3
+message GetProjectStarCountRequest {
+ string project_name = 2;
+}
+
+
+// Next available tag: 2
+message GetProjectStarCountResponse {
+ uint32 star_count = 1;
+}
+
+
+// Next available tag: 3
+message StarProjectRequest {
+ string project_name = 2;
+ bool starred = 3;
+}
+
+
+// Next available tag: 2
+message StarProjectResponse {
+ uint32 star_count = 1;
+}
+
+
+// Next available tag: 3
+message CheckProjectNameRequest {
+ string project_name = 2;
+}
+
+
+// Next available tag: 1
+message CheckProjectNameResponse {
+ string error = 1;
+}
+
+
+// Next available tag: 5
+message CheckComponentNameRequest {
+ string project_name = 2;
+ string parent_path = 3;
+ string component_name = 4;
+}
+
+
+// Next available tag: 2
+message CheckComponentNameResponse {
+ string error = 1;
+}
+
+
+// Next available tag: 4
+message CheckFieldNameRequest {
+ string project_name = 2;
+ string field_name = 3;
+}
+
+
+// Next available tag: 2
+message CheckFieldNameResponse {
+ string error = 1;
+}
diff --git a/api/api_proto/projects_pb2.py b/api/api_proto/projects_pb2.py
new file mode 100644
index 0000000..fa8a3fc
--- /dev/null
+++ b/api/api_proto/projects_pb2.py
@@ -0,0 +1,1375 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/api_proto/projects.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+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()
+
+
+from api.api_proto import common_pb2 as api_dot_api__proto_dot_common__pb2
+from api.api_proto import project_objects_pb2 as api_dot_api__proto_dot_project__objects__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='api/api_proto/projects.proto',
+ package='monorail',
+ syntax='proto3',
+ serialized_options=None,
+ serialized_pb=_b('\n\x1c\x61pi/api_proto/projects.proto\x12\x08monorail\x1a\x1a\x61pi/api_proto/common.proto\x1a#api/api_proto/project_objects.proto\"<\n\x13ListProjectsRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\"T\n\x14ListProjectsResponse\x12#\n\x08projects\x18\x01 \x03(\x0b\x32\x11.monorail.Project\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\"3\n\x1bListProjectTemplatesRequest\x12\x14\n\x0cproject_name\x18\x02 \x01(\t\"H\n\x1cListProjectTemplatesResponse\x12(\n\ttemplates\x18\x01 \x03(\x0b\x32\x15.monorail.TemplateDef\"(\n\x10GetConfigRequest\x12\x14\n\x0cproject_name\x18\x02 \x01(\t\"4\n\x1cGetPresentationConfigRequest\x12\x14\n\x0cproject_name\x18\x02 \x01(\t\"3\n\x1bGetCustomPermissionsRequest\x12\x14\n\x0cproject_name\x18\x02 \x01(\t\"3\n\x1cGetCustomPermissionsResponse\x12\x13\n\x0bpermissions\x18\x01 \x03(\t\"0\n\x18GetVisibleMembersRequest\x12\x14\n\x0cproject_name\x18\x02 \x01(\t\"h\n\x19GetVisibleMembersResponse\x12$\n\tuser_refs\x18\x01 \x03(\x0b\x32\x11.monorail.UserRef\x12%\n\ngroup_refs\x18\x02 \x03(\x0b\x32\x11.monorail.UserRef\".\n\x16GetLabelOptionsRequest\x12\x14\n\x0cproject_name\x18\x02 \x01(\t\"f\n\x17GetLabelOptionsResponse\x12)\n\rlabel_options\x18\x01 \x03(\x0b\x32\x12.monorail.LabelDef\x12 \n\x18\x65xclusive_label_prefixes\x18\x02 \x03(\t\"+\n\x13ListStatusesRequest\x12\x14\n\x0cproject_name\x18\x02 \x01(\t\"\x8e\x01\n\x14ListStatusesResponse\x12(\n\x0bstatus_defs\x18\x01 \x03(\x0b\x32\x13.monorail.StatusDef\x12\x31\n\x14statuses_offer_merge\x18\x02 \x03(\x0b\x32\x13.monorail.StatusRef\x12\x19\n\x11restrict_to_known\x18\x03 \x01(\x08\"I\n\x15ListComponentsRequest\x12\x14\n\x0cproject_name\x18\x02 \x01(\t\x12\x1a\n\x12include_admin_info\x18\x03 \x01(\x08\"H\n\x16ListComponentsResponse\x12.\n\x0e\x63omponent_defs\x18\x01 \x03(\x0b\x32\x16.monorail.ComponentDef\"c\n\x11ListFieldsRequest\x12\x14\n\x0cproject_name\x18\x02 \x01(\t\x12\x1a\n\x12include_admin_info\x18\x03 \x01(\x08\x12\x1c\n\x14include_user_choices\x18\x04 \x01(\x08\"<\n\x12ListFieldsResponse\x12&\n\nfield_defs\x18\x01 \x03(\x0b\x32\x12.monorail.FieldDef\"2\n\x1aGetProjectStarCountRequest\x12\x14\n\x0cproject_name\x18\x02 \x01(\t\"1\n\x1bGetProjectStarCountResponse\x12\x12\n\nstar_count\x18\x01 \x01(\r\";\n\x12StarProjectRequest\x12\x14\n\x0cproject_name\x18\x02 \x01(\t\x12\x0f\n\x07starred\x18\x03 \x01(\x08\")\n\x13StarProjectResponse\x12\x12\n\nstar_count\x18\x01 \x01(\r\"/\n\x17\x43heckProjectNameRequest\x12\x14\n\x0cproject_name\x18\x02 \x01(\t\")\n\x18\x43heckProjectNameResponse\x12\r\n\x05\x65rror\x18\x01 \x01(\t\"^\n\x19\x43heckComponentNameRequest\x12\x14\n\x0cproject_name\x18\x02 \x01(\t\x12\x13\n\x0bparent_path\x18\x03 \x01(\t\x12\x16\n\x0e\x63omponent_name\x18\x04 \x01(\t\"+\n\x1a\x43heckComponentNameResponse\x12\r\n\x05\x65rror\x18\x01 \x01(\t\"A\n\x15\x43heckFieldNameRequest\x12\x14\n\x0cproject_name\x18\x02 \x01(\t\x12\x12\n\nfield_name\x18\x03 \x01(\t\"\'\n\x16\x43heckFieldNameResponse\x12\r\n\x05\x65rror\x18\x01 \x01(\t2\xc3\n\n\x08Projects\x12O\n\x0cListProjects\x12\x1d.monorail.ListProjectsRequest\x1a\x1e.monorail.ListProjectsResponse\"\x00\x12g\n\x14ListProjectTemplates\x12%.monorail.ListProjectTemplatesRequest\x1a&.monorail.ListProjectTemplatesResponse\"\x00\x12;\n\tGetConfig\x12\x1a.monorail.GetConfigRequest\x1a\x10.monorail.Config\"\x00\x12_\n\x15GetPresentationConfig\x12&.monorail.GetPresentationConfigRequest\x1a\x1c.monorail.PresentationConfig\"\x00\x12g\n\x14GetCustomPermissions\x12%.monorail.GetCustomPermissionsRequest\x1a&.monorail.GetCustomPermissionsResponse\"\x00\x12^\n\x11GetVisibleMembers\x12\".monorail.GetVisibleMembersRequest\x1a#.monorail.GetVisibleMembersResponse\"\x00\x12X\n\x0fGetLabelOptions\x12 .monorail.GetLabelOptionsRequest\x1a!.monorail.GetLabelOptionsResponse\"\x00\x12O\n\x0cListStatuses\x12\x1d.monorail.ListStatusesRequest\x1a\x1e.monorail.ListStatusesResponse\"\x00\x12U\n\x0eListComponents\x12\x1f.monorail.ListComponentsRequest\x1a .monorail.ListComponentsResponse\"\x00\x12I\n\nListFields\x12\x1b.monorail.ListFieldsRequest\x1a\x1c.monorail.ListFieldsResponse\"\x00\x12\x64\n\x13GetProjectStarCount\x12$.monorail.GetProjectStarCountRequest\x1a%.monorail.GetProjectStarCountResponse\"\x00\x12L\n\x0bStarProject\x12\x1c.monorail.StarProjectRequest\x1a\x1d.monorail.StarProjectResponse\"\x00\x12[\n\x10\x43heckProjectName\x12!.monorail.CheckProjectNameRequest\x1a\".monorail.CheckProjectNameResponse\"\x00\x12\x61\n\x12\x43heckComponentName\x12#.monorail.CheckComponentNameRequest\x1a$.monorail.CheckComponentNameResponse\"\x00\x12U\n\x0e\x43heckFieldName\x12\x1f.monorail.CheckFieldNameRequest\x1a .monorail.CheckFieldNameResponse\"\x00\x62\x06proto3')
+ ,
+ dependencies=[api_dot_api__proto_dot_common__pb2.DESCRIPTOR,api_dot_api__proto_dot_project__objects__pb2.DESCRIPTOR,])
+
+
+
+
+_LISTPROJECTSREQUEST = _descriptor.Descriptor(
+ name='ListProjectsRequest',
+ full_name='monorail.ListProjectsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='page_size', full_name='monorail.ListProjectsRequest.page_size', 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),
+ _descriptor.FieldDescriptor(
+ name='page_token', full_name='monorail.ListProjectsRequest.page_token', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=107,
+ serialized_end=167,
+)
+
+
+_LISTPROJECTSRESPONSE = _descriptor.Descriptor(
+ name='ListProjectsResponse',
+ full_name='monorail.ListProjectsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='projects', full_name='monorail.ListProjectsResponse.projects', 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),
+ _descriptor.FieldDescriptor(
+ name='next_page_token', full_name='monorail.ListProjectsResponse.next_page_token', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=169,
+ serialized_end=253,
+)
+
+
+_LISTPROJECTTEMPLATESREQUEST = _descriptor.Descriptor(
+ name='ListProjectTemplatesRequest',
+ full_name='monorail.ListProjectTemplatesRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.ListProjectTemplatesRequest.project_name', index=0,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=255,
+ serialized_end=306,
+)
+
+
+_LISTPROJECTTEMPLATESRESPONSE = _descriptor.Descriptor(
+ name='ListProjectTemplatesResponse',
+ full_name='monorail.ListProjectTemplatesResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='templates', full_name='monorail.ListProjectTemplatesResponse.templates', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=308,
+ serialized_end=380,
+)
+
+
+_GETCONFIGREQUEST = _descriptor.Descriptor(
+ name='GetConfigRequest',
+ full_name='monorail.GetConfigRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.GetConfigRequest.project_name', index=0,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=382,
+ serialized_end=422,
+)
+
+
+_GETPRESENTATIONCONFIGREQUEST = _descriptor.Descriptor(
+ name='GetPresentationConfigRequest',
+ full_name='monorail.GetPresentationConfigRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.GetPresentationConfigRequest.project_name', index=0,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=424,
+ serialized_end=476,
+)
+
+
+_GETCUSTOMPERMISSIONSREQUEST = _descriptor.Descriptor(
+ name='GetCustomPermissionsRequest',
+ full_name='monorail.GetCustomPermissionsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.GetCustomPermissionsRequest.project_name', index=0,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=478,
+ serialized_end=529,
+)
+
+
+_GETCUSTOMPERMISSIONSRESPONSE = _descriptor.Descriptor(
+ name='GetCustomPermissionsResponse',
+ full_name='monorail.GetCustomPermissionsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='permissions', full_name='monorail.GetCustomPermissionsResponse.permissions', index=0,
+ number=1, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=531,
+ serialized_end=582,
+)
+
+
+_GETVISIBLEMEMBERSREQUEST = _descriptor.Descriptor(
+ name='GetVisibleMembersRequest',
+ full_name='monorail.GetVisibleMembersRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.GetVisibleMembersRequest.project_name', index=0,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=584,
+ serialized_end=632,
+)
+
+
+_GETVISIBLEMEMBERSRESPONSE = _descriptor.Descriptor(
+ name='GetVisibleMembersResponse',
+ full_name='monorail.GetVisibleMembersResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='user_refs', full_name='monorail.GetVisibleMembersResponse.user_refs', 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),
+ _descriptor.FieldDescriptor(
+ name='group_refs', full_name='monorail.GetVisibleMembersResponse.group_refs', index=1,
+ number=2, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=634,
+ serialized_end=738,
+)
+
+
+_GETLABELOPTIONSREQUEST = _descriptor.Descriptor(
+ name='GetLabelOptionsRequest',
+ full_name='monorail.GetLabelOptionsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.GetLabelOptionsRequest.project_name', index=0,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=740,
+ serialized_end=786,
+)
+
+
+_GETLABELOPTIONSRESPONSE = _descriptor.Descriptor(
+ name='GetLabelOptionsResponse',
+ full_name='monorail.GetLabelOptionsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='label_options', full_name='monorail.GetLabelOptionsResponse.label_options', 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),
+ _descriptor.FieldDescriptor(
+ name='exclusive_label_prefixes', full_name='monorail.GetLabelOptionsResponse.exclusive_label_prefixes', index=1,
+ number=2, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=788,
+ serialized_end=890,
+)
+
+
+_LISTSTATUSESREQUEST = _descriptor.Descriptor(
+ name='ListStatusesRequest',
+ full_name='monorail.ListStatusesRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.ListStatusesRequest.project_name', index=0,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=892,
+ serialized_end=935,
+)
+
+
+_LISTSTATUSESRESPONSE = _descriptor.Descriptor(
+ name='ListStatusesResponse',
+ full_name='monorail.ListStatusesResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='status_defs', full_name='monorail.ListStatusesResponse.status_defs', 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),
+ _descriptor.FieldDescriptor(
+ name='statuses_offer_merge', full_name='monorail.ListStatusesResponse.statuses_offer_merge', index=1,
+ number=2, 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),
+ _descriptor.FieldDescriptor(
+ name='restrict_to_known', full_name='monorail.ListStatusesResponse.restrict_to_known', index=2,
+ number=3, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=938,
+ serialized_end=1080,
+)
+
+
+_LISTCOMPONENTSREQUEST = _descriptor.Descriptor(
+ name='ListComponentsRequest',
+ full_name='monorail.ListComponentsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.ListComponentsRequest.project_name', index=0,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='include_admin_info', full_name='monorail.ListComponentsRequest.include_admin_info', index=1,
+ number=3, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1082,
+ serialized_end=1155,
+)
+
+
+_LISTCOMPONENTSRESPONSE = _descriptor.Descriptor(
+ name='ListComponentsResponse',
+ full_name='monorail.ListComponentsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='component_defs', full_name='monorail.ListComponentsResponse.component_defs', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1157,
+ serialized_end=1229,
+)
+
+
+_LISTFIELDSREQUEST = _descriptor.Descriptor(
+ name='ListFieldsRequest',
+ full_name='monorail.ListFieldsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.ListFieldsRequest.project_name', index=0,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='include_admin_info', full_name='monorail.ListFieldsRequest.include_admin_info', index=1,
+ number=3, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='include_user_choices', full_name='monorail.ListFieldsRequest.include_user_choices', index=2,
+ number=4, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1231,
+ serialized_end=1330,
+)
+
+
+_LISTFIELDSRESPONSE = _descriptor.Descriptor(
+ name='ListFieldsResponse',
+ full_name='monorail.ListFieldsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='field_defs', full_name='monorail.ListFieldsResponse.field_defs', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1332,
+ serialized_end=1392,
+)
+
+
+_GETPROJECTSTARCOUNTREQUEST = _descriptor.Descriptor(
+ name='GetProjectStarCountRequest',
+ full_name='monorail.GetProjectStarCountRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.GetProjectStarCountRequest.project_name', index=0,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1394,
+ serialized_end=1444,
+)
+
+
+_GETPROJECTSTARCOUNTRESPONSE = _descriptor.Descriptor(
+ name='GetProjectStarCountResponse',
+ full_name='monorail.GetProjectStarCountResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='star_count', full_name='monorail.GetProjectStarCountResponse.star_count', index=0,
+ number=1, type=13, cpp_type=3, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1446,
+ serialized_end=1495,
+)
+
+
+_STARPROJECTREQUEST = _descriptor.Descriptor(
+ name='StarProjectRequest',
+ full_name='monorail.StarProjectRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.StarProjectRequest.project_name', index=0,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='starred', full_name='monorail.StarProjectRequest.starred', index=1,
+ number=3, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1497,
+ serialized_end=1556,
+)
+
+
+_STARPROJECTRESPONSE = _descriptor.Descriptor(
+ name='StarProjectResponse',
+ full_name='monorail.StarProjectResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='star_count', full_name='monorail.StarProjectResponse.star_count', index=0,
+ number=1, type=13, cpp_type=3, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1558,
+ serialized_end=1599,
+)
+
+
+_CHECKPROJECTNAMEREQUEST = _descriptor.Descriptor(
+ name='CheckProjectNameRequest',
+ full_name='monorail.CheckProjectNameRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.CheckProjectNameRequest.project_name', index=0,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1601,
+ serialized_end=1648,
+)
+
+
+_CHECKPROJECTNAMERESPONSE = _descriptor.Descriptor(
+ name='CheckProjectNameResponse',
+ full_name='monorail.CheckProjectNameResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='error', full_name='monorail.CheckProjectNameResponse.error', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1650,
+ serialized_end=1691,
+)
+
+
+_CHECKCOMPONENTNAMEREQUEST = _descriptor.Descriptor(
+ name='CheckComponentNameRequest',
+ full_name='monorail.CheckComponentNameRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.CheckComponentNameRequest.project_name', index=0,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='parent_path', full_name='monorail.CheckComponentNameRequest.parent_path', index=1,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='component_name', full_name='monorail.CheckComponentNameRequest.component_name', index=2,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1693,
+ serialized_end=1787,
+)
+
+
+_CHECKCOMPONENTNAMERESPONSE = _descriptor.Descriptor(
+ name='CheckComponentNameResponse',
+ full_name='monorail.CheckComponentNameResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='error', full_name='monorail.CheckComponentNameResponse.error', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1789,
+ serialized_end=1832,
+)
+
+
+_CHECKFIELDNAMEREQUEST = _descriptor.Descriptor(
+ name='CheckFieldNameRequest',
+ full_name='monorail.CheckFieldNameRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_name', full_name='monorail.CheckFieldNameRequest.project_name', index=0,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='field_name', full_name='monorail.CheckFieldNameRequest.field_name', index=1,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1834,
+ serialized_end=1899,
+)
+
+
+_CHECKFIELDNAMERESPONSE = _descriptor.Descriptor(
+ name='CheckFieldNameResponse',
+ full_name='monorail.CheckFieldNameResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='error', full_name='monorail.CheckFieldNameResponse.error', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1901,
+ serialized_end=1940,
+)
+
+_LISTPROJECTSRESPONSE.fields_by_name['projects'].message_type = api_dot_api__proto_dot_project__objects__pb2._PROJECT
+_LISTPROJECTTEMPLATESRESPONSE.fields_by_name['templates'].message_type = api_dot_api__proto_dot_project__objects__pb2._TEMPLATEDEF
+_GETVISIBLEMEMBERSRESPONSE.fields_by_name['user_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_GETVISIBLEMEMBERSRESPONSE.fields_by_name['group_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_GETLABELOPTIONSRESPONSE.fields_by_name['label_options'].message_type = api_dot_api__proto_dot_project__objects__pb2._LABELDEF
+_LISTSTATUSESRESPONSE.fields_by_name['status_defs'].message_type = api_dot_api__proto_dot_project__objects__pb2._STATUSDEF
+_LISTSTATUSESRESPONSE.fields_by_name['statuses_offer_merge'].message_type = api_dot_api__proto_dot_common__pb2._STATUSREF
+_LISTCOMPONENTSRESPONSE.fields_by_name['component_defs'].message_type = api_dot_api__proto_dot_project__objects__pb2._COMPONENTDEF
+_LISTFIELDSRESPONSE.fields_by_name['field_defs'].message_type = api_dot_api__proto_dot_project__objects__pb2._FIELDDEF
+DESCRIPTOR.message_types_by_name['ListProjectsRequest'] = _LISTPROJECTSREQUEST
+DESCRIPTOR.message_types_by_name['ListProjectsResponse'] = _LISTPROJECTSRESPONSE
+DESCRIPTOR.message_types_by_name['ListProjectTemplatesRequest'] = _LISTPROJECTTEMPLATESREQUEST
+DESCRIPTOR.message_types_by_name['ListProjectTemplatesResponse'] = _LISTPROJECTTEMPLATESRESPONSE
+DESCRIPTOR.message_types_by_name['GetConfigRequest'] = _GETCONFIGREQUEST
+DESCRIPTOR.message_types_by_name['GetPresentationConfigRequest'] = _GETPRESENTATIONCONFIGREQUEST
+DESCRIPTOR.message_types_by_name['GetCustomPermissionsRequest'] = _GETCUSTOMPERMISSIONSREQUEST
+DESCRIPTOR.message_types_by_name['GetCustomPermissionsResponse'] = _GETCUSTOMPERMISSIONSRESPONSE
+DESCRIPTOR.message_types_by_name['GetVisibleMembersRequest'] = _GETVISIBLEMEMBERSREQUEST
+DESCRIPTOR.message_types_by_name['GetVisibleMembersResponse'] = _GETVISIBLEMEMBERSRESPONSE
+DESCRIPTOR.message_types_by_name['GetLabelOptionsRequest'] = _GETLABELOPTIONSREQUEST
+DESCRIPTOR.message_types_by_name['GetLabelOptionsResponse'] = _GETLABELOPTIONSRESPONSE
+DESCRIPTOR.message_types_by_name['ListStatusesRequest'] = _LISTSTATUSESREQUEST
+DESCRIPTOR.message_types_by_name['ListStatusesResponse'] = _LISTSTATUSESRESPONSE
+DESCRIPTOR.message_types_by_name['ListComponentsRequest'] = _LISTCOMPONENTSREQUEST
+DESCRIPTOR.message_types_by_name['ListComponentsResponse'] = _LISTCOMPONENTSRESPONSE
+DESCRIPTOR.message_types_by_name['ListFieldsRequest'] = _LISTFIELDSREQUEST
+DESCRIPTOR.message_types_by_name['ListFieldsResponse'] = _LISTFIELDSRESPONSE
+DESCRIPTOR.message_types_by_name['GetProjectStarCountRequest'] = _GETPROJECTSTARCOUNTREQUEST
+DESCRIPTOR.message_types_by_name['GetProjectStarCountResponse'] = _GETPROJECTSTARCOUNTRESPONSE
+DESCRIPTOR.message_types_by_name['StarProjectRequest'] = _STARPROJECTREQUEST
+DESCRIPTOR.message_types_by_name['StarProjectResponse'] = _STARPROJECTRESPONSE
+DESCRIPTOR.message_types_by_name['CheckProjectNameRequest'] = _CHECKPROJECTNAMEREQUEST
+DESCRIPTOR.message_types_by_name['CheckProjectNameResponse'] = _CHECKPROJECTNAMERESPONSE
+DESCRIPTOR.message_types_by_name['CheckComponentNameRequest'] = _CHECKCOMPONENTNAMEREQUEST
+DESCRIPTOR.message_types_by_name['CheckComponentNameResponse'] = _CHECKCOMPONENTNAMERESPONSE
+DESCRIPTOR.message_types_by_name['CheckFieldNameRequest'] = _CHECKFIELDNAMEREQUEST
+DESCRIPTOR.message_types_by_name['CheckFieldNameResponse'] = _CHECKFIELDNAMERESPONSE
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+ListProjectsRequest = _reflection.GeneratedProtocolMessageType('ListProjectsRequest', (_message.Message,), dict(
+ DESCRIPTOR = _LISTPROJECTSREQUEST,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListProjectsRequest)
+ ))
+_sym_db.RegisterMessage(ListProjectsRequest)
+
+ListProjectsResponse = _reflection.GeneratedProtocolMessageType('ListProjectsResponse', (_message.Message,), dict(
+ DESCRIPTOR = _LISTPROJECTSRESPONSE,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListProjectsResponse)
+ ))
+_sym_db.RegisterMessage(ListProjectsResponse)
+
+ListProjectTemplatesRequest = _reflection.GeneratedProtocolMessageType('ListProjectTemplatesRequest', (_message.Message,), dict(
+ DESCRIPTOR = _LISTPROJECTTEMPLATESREQUEST,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListProjectTemplatesRequest)
+ ))
+_sym_db.RegisterMessage(ListProjectTemplatesRequest)
+
+ListProjectTemplatesResponse = _reflection.GeneratedProtocolMessageType('ListProjectTemplatesResponse', (_message.Message,), dict(
+ DESCRIPTOR = _LISTPROJECTTEMPLATESRESPONSE,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListProjectTemplatesResponse)
+ ))
+_sym_db.RegisterMessage(ListProjectTemplatesResponse)
+
+GetConfigRequest = _reflection.GeneratedProtocolMessageType('GetConfigRequest', (_message.Message,), dict(
+ DESCRIPTOR = _GETCONFIGREQUEST,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetConfigRequest)
+ ))
+_sym_db.RegisterMessage(GetConfigRequest)
+
+GetPresentationConfigRequest = _reflection.GeneratedProtocolMessageType('GetPresentationConfigRequest', (_message.Message,), dict(
+ DESCRIPTOR = _GETPRESENTATIONCONFIGREQUEST,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetPresentationConfigRequest)
+ ))
+_sym_db.RegisterMessage(GetPresentationConfigRequest)
+
+GetCustomPermissionsRequest = _reflection.GeneratedProtocolMessageType('GetCustomPermissionsRequest', (_message.Message,), dict(
+ DESCRIPTOR = _GETCUSTOMPERMISSIONSREQUEST,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetCustomPermissionsRequest)
+ ))
+_sym_db.RegisterMessage(GetCustomPermissionsRequest)
+
+GetCustomPermissionsResponse = _reflection.GeneratedProtocolMessageType('GetCustomPermissionsResponse', (_message.Message,), dict(
+ DESCRIPTOR = _GETCUSTOMPERMISSIONSRESPONSE,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetCustomPermissionsResponse)
+ ))
+_sym_db.RegisterMessage(GetCustomPermissionsResponse)
+
+GetVisibleMembersRequest = _reflection.GeneratedProtocolMessageType('GetVisibleMembersRequest', (_message.Message,), dict(
+ DESCRIPTOR = _GETVISIBLEMEMBERSREQUEST,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetVisibleMembersRequest)
+ ))
+_sym_db.RegisterMessage(GetVisibleMembersRequest)
+
+GetVisibleMembersResponse = _reflection.GeneratedProtocolMessageType('GetVisibleMembersResponse', (_message.Message,), dict(
+ DESCRIPTOR = _GETVISIBLEMEMBERSRESPONSE,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetVisibleMembersResponse)
+ ))
+_sym_db.RegisterMessage(GetVisibleMembersResponse)
+
+GetLabelOptionsRequest = _reflection.GeneratedProtocolMessageType('GetLabelOptionsRequest', (_message.Message,), dict(
+ DESCRIPTOR = _GETLABELOPTIONSREQUEST,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetLabelOptionsRequest)
+ ))
+_sym_db.RegisterMessage(GetLabelOptionsRequest)
+
+GetLabelOptionsResponse = _reflection.GeneratedProtocolMessageType('GetLabelOptionsResponse', (_message.Message,), dict(
+ DESCRIPTOR = _GETLABELOPTIONSRESPONSE,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetLabelOptionsResponse)
+ ))
+_sym_db.RegisterMessage(GetLabelOptionsResponse)
+
+ListStatusesRequest = _reflection.GeneratedProtocolMessageType('ListStatusesRequest', (_message.Message,), dict(
+ DESCRIPTOR = _LISTSTATUSESREQUEST,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListStatusesRequest)
+ ))
+_sym_db.RegisterMessage(ListStatusesRequest)
+
+ListStatusesResponse = _reflection.GeneratedProtocolMessageType('ListStatusesResponse', (_message.Message,), dict(
+ DESCRIPTOR = _LISTSTATUSESRESPONSE,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListStatusesResponse)
+ ))
+_sym_db.RegisterMessage(ListStatusesResponse)
+
+ListComponentsRequest = _reflection.GeneratedProtocolMessageType('ListComponentsRequest', (_message.Message,), dict(
+ DESCRIPTOR = _LISTCOMPONENTSREQUEST,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListComponentsRequest)
+ ))
+_sym_db.RegisterMessage(ListComponentsRequest)
+
+ListComponentsResponse = _reflection.GeneratedProtocolMessageType('ListComponentsResponse', (_message.Message,), dict(
+ DESCRIPTOR = _LISTCOMPONENTSRESPONSE,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListComponentsResponse)
+ ))
+_sym_db.RegisterMessage(ListComponentsResponse)
+
+ListFieldsRequest = _reflection.GeneratedProtocolMessageType('ListFieldsRequest', (_message.Message,), dict(
+ DESCRIPTOR = _LISTFIELDSREQUEST,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListFieldsRequest)
+ ))
+_sym_db.RegisterMessage(ListFieldsRequest)
+
+ListFieldsResponse = _reflection.GeneratedProtocolMessageType('ListFieldsResponse', (_message.Message,), dict(
+ DESCRIPTOR = _LISTFIELDSRESPONSE,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListFieldsResponse)
+ ))
+_sym_db.RegisterMessage(ListFieldsResponse)
+
+GetProjectStarCountRequest = _reflection.GeneratedProtocolMessageType('GetProjectStarCountRequest', (_message.Message,), dict(
+ DESCRIPTOR = _GETPROJECTSTARCOUNTREQUEST,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetProjectStarCountRequest)
+ ))
+_sym_db.RegisterMessage(GetProjectStarCountRequest)
+
+GetProjectStarCountResponse = _reflection.GeneratedProtocolMessageType('GetProjectStarCountResponse', (_message.Message,), dict(
+ DESCRIPTOR = _GETPROJECTSTARCOUNTRESPONSE,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetProjectStarCountResponse)
+ ))
+_sym_db.RegisterMessage(GetProjectStarCountResponse)
+
+StarProjectRequest = _reflection.GeneratedProtocolMessageType('StarProjectRequest', (_message.Message,), dict(
+ DESCRIPTOR = _STARPROJECTREQUEST,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.StarProjectRequest)
+ ))
+_sym_db.RegisterMessage(StarProjectRequest)
+
+StarProjectResponse = _reflection.GeneratedProtocolMessageType('StarProjectResponse', (_message.Message,), dict(
+ DESCRIPTOR = _STARPROJECTRESPONSE,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.StarProjectResponse)
+ ))
+_sym_db.RegisterMessage(StarProjectResponse)
+
+CheckProjectNameRequest = _reflection.GeneratedProtocolMessageType('CheckProjectNameRequest', (_message.Message,), dict(
+ DESCRIPTOR = _CHECKPROJECTNAMEREQUEST,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.CheckProjectNameRequest)
+ ))
+_sym_db.RegisterMessage(CheckProjectNameRequest)
+
+CheckProjectNameResponse = _reflection.GeneratedProtocolMessageType('CheckProjectNameResponse', (_message.Message,), dict(
+ DESCRIPTOR = _CHECKPROJECTNAMERESPONSE,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.CheckProjectNameResponse)
+ ))
+_sym_db.RegisterMessage(CheckProjectNameResponse)
+
+CheckComponentNameRequest = _reflection.GeneratedProtocolMessageType('CheckComponentNameRequest', (_message.Message,), dict(
+ DESCRIPTOR = _CHECKCOMPONENTNAMEREQUEST,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.CheckComponentNameRequest)
+ ))
+_sym_db.RegisterMessage(CheckComponentNameRequest)
+
+CheckComponentNameResponse = _reflection.GeneratedProtocolMessageType('CheckComponentNameResponse', (_message.Message,), dict(
+ DESCRIPTOR = _CHECKCOMPONENTNAMERESPONSE,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.CheckComponentNameResponse)
+ ))
+_sym_db.RegisterMessage(CheckComponentNameResponse)
+
+CheckFieldNameRequest = _reflection.GeneratedProtocolMessageType('CheckFieldNameRequest', (_message.Message,), dict(
+ DESCRIPTOR = _CHECKFIELDNAMEREQUEST,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.CheckFieldNameRequest)
+ ))
+_sym_db.RegisterMessage(CheckFieldNameRequest)
+
+CheckFieldNameResponse = _reflection.GeneratedProtocolMessageType('CheckFieldNameResponse', (_message.Message,), dict(
+ DESCRIPTOR = _CHECKFIELDNAMERESPONSE,
+ __module__ = 'api.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.CheckFieldNameResponse)
+ ))
+_sym_db.RegisterMessage(CheckFieldNameResponse)
+
+
+
+_PROJECTS = _descriptor.ServiceDescriptor(
+ name='Projects',
+ full_name='monorail.Projects',
+ file=DESCRIPTOR,
+ index=0,
+ serialized_options=None,
+ serialized_start=1943,
+ serialized_end=3290,
+ methods=[
+ _descriptor.MethodDescriptor(
+ name='ListProjects',
+ full_name='monorail.Projects.ListProjects',
+ index=0,
+ containing_service=None,
+ input_type=_LISTPROJECTSREQUEST,
+ output_type=_LISTPROJECTSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListProjectTemplates',
+ full_name='monorail.Projects.ListProjectTemplates',
+ index=1,
+ containing_service=None,
+ input_type=_LISTPROJECTTEMPLATESREQUEST,
+ output_type=_LISTPROJECTTEMPLATESRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='GetConfig',
+ full_name='monorail.Projects.GetConfig',
+ index=2,
+ containing_service=None,
+ input_type=_GETCONFIGREQUEST,
+ output_type=api_dot_api__proto_dot_project__objects__pb2._CONFIG,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='GetPresentationConfig',
+ full_name='monorail.Projects.GetPresentationConfig',
+ index=3,
+ containing_service=None,
+ input_type=_GETPRESENTATIONCONFIGREQUEST,
+ output_type=api_dot_api__proto_dot_project__objects__pb2._PRESENTATIONCONFIG,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='GetCustomPermissions',
+ full_name='monorail.Projects.GetCustomPermissions',
+ index=4,
+ containing_service=None,
+ input_type=_GETCUSTOMPERMISSIONSREQUEST,
+ output_type=_GETCUSTOMPERMISSIONSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='GetVisibleMembers',
+ full_name='monorail.Projects.GetVisibleMembers',
+ index=5,
+ containing_service=None,
+ input_type=_GETVISIBLEMEMBERSREQUEST,
+ output_type=_GETVISIBLEMEMBERSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='GetLabelOptions',
+ full_name='monorail.Projects.GetLabelOptions',
+ index=6,
+ containing_service=None,
+ input_type=_GETLABELOPTIONSREQUEST,
+ output_type=_GETLABELOPTIONSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListStatuses',
+ full_name='monorail.Projects.ListStatuses',
+ index=7,
+ containing_service=None,
+ input_type=_LISTSTATUSESREQUEST,
+ output_type=_LISTSTATUSESRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListComponents',
+ full_name='monorail.Projects.ListComponents',
+ index=8,
+ containing_service=None,
+ input_type=_LISTCOMPONENTSREQUEST,
+ output_type=_LISTCOMPONENTSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListFields',
+ full_name='monorail.Projects.ListFields',
+ index=9,
+ containing_service=None,
+ input_type=_LISTFIELDSREQUEST,
+ output_type=_LISTFIELDSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='GetProjectStarCount',
+ full_name='monorail.Projects.GetProjectStarCount',
+ index=10,
+ containing_service=None,
+ input_type=_GETPROJECTSTARCOUNTREQUEST,
+ output_type=_GETPROJECTSTARCOUNTRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='StarProject',
+ full_name='monorail.Projects.StarProject',
+ index=11,
+ containing_service=None,
+ input_type=_STARPROJECTREQUEST,
+ output_type=_STARPROJECTRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='CheckProjectName',
+ full_name='monorail.Projects.CheckProjectName',
+ index=12,
+ containing_service=None,
+ input_type=_CHECKPROJECTNAMEREQUEST,
+ output_type=_CHECKPROJECTNAMERESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='CheckComponentName',
+ full_name='monorail.Projects.CheckComponentName',
+ index=13,
+ containing_service=None,
+ input_type=_CHECKCOMPONENTNAMEREQUEST,
+ output_type=_CHECKCOMPONENTNAMERESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='CheckFieldName',
+ full_name='monorail.Projects.CheckFieldName',
+ index=14,
+ containing_service=None,
+ input_type=_CHECKFIELDNAMEREQUEST,
+ output_type=_CHECKFIELDNAMERESPONSE,
+ serialized_options=None,
+ ),
+])
+_sym_db.RegisterServiceDescriptor(_PROJECTS)
+
+DESCRIPTOR.services_by_name['Projects'] = _PROJECTS
+
+# @@protoc_insertion_point(module_scope)
diff --git a/api/api_proto/projects_prpc_pb2.py b/api/api_proto/projects_prpc_pb2.py
new file mode 100644
index 0000000..fee63f3
--- /dev/null
+++ b/api/api_proto/projects_prpc_pb2.py
@@ -0,0 +1,313 @@
+# Generated by the pRPC protocol buffer compiler plugin. DO NOT EDIT!
+# source: api/api_proto/projects.proto
+
+import base64
+import zlib
+
+from google.protobuf import descriptor_pb2
+
+# Includes description of the api/api_proto/projects.proto and all of its transitive
+# dependencies. Includes source code info.
+FILE_DESCRIPTOR_SET = descriptor_pb2.FileDescriptorSet()
+FILE_DESCRIPTOR_SET.ParseFromString(zlib.decompress(base64.b64decode(
+ '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'
+ 'dZ5C+ki+emubbJz8bwoBFG4=')))
+_INDEX = {
+ f.name: {
+ 'descriptor': f,
+ 'services': {s.name: s for s in f.service},
+ }
+ for f in FILE_DESCRIPTOR_SET.file
+}
+
+
+ProjectsServiceDescription = {
+ 'file_descriptor_set': FILE_DESCRIPTOR_SET,
+ 'file_descriptor': _INDEX[u'api/api_proto/projects.proto']['descriptor'],
+ 'service_descriptor': _INDEX[u'api/api_proto/projects.proto']['services'][u'Projects'],
+}
diff --git a/api/api_proto/sitewide.proto b/api/api_proto/sitewide.proto
new file mode 100644
index 0000000..f378ad5
--- /dev/null
+++ b/api/api_proto/sitewide.proto
@@ -0,0 +1,40 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+syntax = "proto3";
+
+package monorail;
+
+service Sitewide {
+ rpc RefreshToken (RefreshTokenRequest) returns (RefreshTokenResponse) {}
+ rpc GetServerStatus (GetServerStatusRequest) returns (GetServerStatusResponse) {}
+}
+
+
+// Next available tag: 4
+message RefreshTokenRequest {
+ string token = 2;
+ string token_path = 3;
+}
+
+
+// Next available tag: 3
+message RefreshTokenResponse {
+ string token = 1;
+ uint32 token_expires_sec = 2;
+}
+
+
+// Next available tag: 1
+message GetServerStatusRequest {
+}
+
+
+// Next available tag: 4
+message GetServerStatusResponse {
+ string banner_message = 1;
+ fixed32 banner_time = 2;
+ bool read_only = 3;
+}
diff --git a/api/api_proto/sitewide_pb2.py b/api/api_proto/sitewide_pb2.py
new file mode 100644
index 0000000..acb7e88
--- /dev/null
+++ b/api/api_proto/sitewide_pb2.py
@@ -0,0 +1,240 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/api_proto/sitewide.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+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/api_proto/sitewide.proto',
+ package='monorail',
+ syntax='proto3',
+ serialized_options=None,
+ serialized_pb=_b('\n\x1c\x61pi/api_proto/sitewide.proto\x12\x08monorail\"8\n\x13RefreshTokenRequest\x12\r\n\x05token\x18\x02 \x01(\t\x12\x12\n\ntoken_path\x18\x03 \x01(\t\"@\n\x14RefreshTokenResponse\x12\r\n\x05token\x18\x01 \x01(\t\x12\x19\n\x11token_expires_sec\x18\x02 \x01(\r\"\x18\n\x16GetServerStatusRequest\"Y\n\x17GetServerStatusResponse\x12\x16\n\x0e\x62\x61nner_message\x18\x01 \x01(\t\x12\x13\n\x0b\x62\x61nner_time\x18\x02 \x01(\x07\x12\x11\n\tread_only\x18\x03 \x01(\x08\x32\xb5\x01\n\x08Sitewide\x12O\n\x0cRefreshToken\x12\x1d.monorail.RefreshTokenRequest\x1a\x1e.monorail.RefreshTokenResponse\"\x00\x12X\n\x0fGetServerStatus\x12 .monorail.GetServerStatusRequest\x1a!.monorail.GetServerStatusResponse\"\x00\x62\x06proto3')
+)
+
+
+
+
+_REFRESHTOKENREQUEST = _descriptor.Descriptor(
+ name='RefreshTokenRequest',
+ full_name='monorail.RefreshTokenRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='token', full_name='monorail.RefreshTokenRequest.token', index=0,
+ 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),
+ _descriptor.FieldDescriptor(
+ name='token_path', full_name='monorail.RefreshTokenRequest.token_path', index=1,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=42,
+ serialized_end=98,
+)
+
+
+_REFRESHTOKENRESPONSE = _descriptor.Descriptor(
+ name='RefreshTokenResponse',
+ full_name='monorail.RefreshTokenResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='token', full_name='monorail.RefreshTokenResponse.token', 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),
+ _descriptor.FieldDescriptor(
+ name='token_expires_sec', full_name='monorail.RefreshTokenResponse.token_expires_sec', index=1,
+ number=2, type=13, cpp_type=3, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=100,
+ serialized_end=164,
+)
+
+
+_GETSERVERSTATUSREQUEST = _descriptor.Descriptor(
+ name='GetServerStatusRequest',
+ full_name='monorail.GetServerStatusRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=166,
+ serialized_end=190,
+)
+
+
+_GETSERVERSTATUSRESPONSE = _descriptor.Descriptor(
+ name='GetServerStatusResponse',
+ full_name='monorail.GetServerStatusResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='banner_message', full_name='monorail.GetServerStatusResponse.banner_message', 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),
+ _descriptor.FieldDescriptor(
+ name='banner_time', full_name='monorail.GetServerStatusResponse.banner_time', index=1,
+ number=2, type=7, cpp_type=3, 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),
+ _descriptor.FieldDescriptor(
+ name='read_only', full_name='monorail.GetServerStatusResponse.read_only', index=2,
+ number=3, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=192,
+ serialized_end=281,
+)
+
+DESCRIPTOR.message_types_by_name['RefreshTokenRequest'] = _REFRESHTOKENREQUEST
+DESCRIPTOR.message_types_by_name['RefreshTokenResponse'] = _REFRESHTOKENRESPONSE
+DESCRIPTOR.message_types_by_name['GetServerStatusRequest'] = _GETSERVERSTATUSREQUEST
+DESCRIPTOR.message_types_by_name['GetServerStatusResponse'] = _GETSERVERSTATUSRESPONSE
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+RefreshTokenRequest = _reflection.GeneratedProtocolMessageType('RefreshTokenRequest', (_message.Message,), dict(
+ DESCRIPTOR = _REFRESHTOKENREQUEST,
+ __module__ = 'api.api_proto.sitewide_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.RefreshTokenRequest)
+ ))
+_sym_db.RegisterMessage(RefreshTokenRequest)
+
+RefreshTokenResponse = _reflection.GeneratedProtocolMessageType('RefreshTokenResponse', (_message.Message,), dict(
+ DESCRIPTOR = _REFRESHTOKENRESPONSE,
+ __module__ = 'api.api_proto.sitewide_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.RefreshTokenResponse)
+ ))
+_sym_db.RegisterMessage(RefreshTokenResponse)
+
+GetServerStatusRequest = _reflection.GeneratedProtocolMessageType('GetServerStatusRequest', (_message.Message,), dict(
+ DESCRIPTOR = _GETSERVERSTATUSREQUEST,
+ __module__ = 'api.api_proto.sitewide_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetServerStatusRequest)
+ ))
+_sym_db.RegisterMessage(GetServerStatusRequest)
+
+GetServerStatusResponse = _reflection.GeneratedProtocolMessageType('GetServerStatusResponse', (_message.Message,), dict(
+ DESCRIPTOR = _GETSERVERSTATUSRESPONSE,
+ __module__ = 'api.api_proto.sitewide_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetServerStatusResponse)
+ ))
+_sym_db.RegisterMessage(GetServerStatusResponse)
+
+
+
+_SITEWIDE = _descriptor.ServiceDescriptor(
+ name='Sitewide',
+ full_name='monorail.Sitewide',
+ file=DESCRIPTOR,
+ index=0,
+ serialized_options=None,
+ serialized_start=284,
+ serialized_end=465,
+ methods=[
+ _descriptor.MethodDescriptor(
+ name='RefreshToken',
+ full_name='monorail.Sitewide.RefreshToken',
+ index=0,
+ containing_service=None,
+ input_type=_REFRESHTOKENREQUEST,
+ output_type=_REFRESHTOKENRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='GetServerStatus',
+ full_name='monorail.Sitewide.GetServerStatus',
+ index=1,
+ containing_service=None,
+ input_type=_GETSERVERSTATUSREQUEST,
+ output_type=_GETSERVERSTATUSRESPONSE,
+ serialized_options=None,
+ ),
+])
+_sym_db.RegisterServiceDescriptor(_SITEWIDE)
+
+DESCRIPTOR.services_by_name['Sitewide'] = _SITEWIDE
+
+# @@protoc_insertion_point(module_scope)
diff --git a/api/api_proto/sitewide_prpc_pb2.py b/api/api_proto/sitewide_prpc_pb2.py
new file mode 100644
index 0000000..6d999b4
--- /dev/null
+++ b/api/api_proto/sitewide_prpc_pb2.py
@@ -0,0 +1,42 @@
+# Generated by the pRPC protocol buffer compiler plugin. DO NOT EDIT!
+# source: api/api_proto/sitewide.proto
+
+import base64
+import zlib
+
+from google.protobuf import descriptor_pb2
+
+# Includes description of the api/api_proto/sitewide.proto and all of its transitive
+# dependencies. Includes source code info.
+FILE_DESCRIPTOR_SET = descriptor_pb2.FileDescriptorSet()
+FILE_DESCRIPTOR_SET.ParseFromString(zlib.decompress(base64.b64decode(
+ 'eJx9lV1z2kYUhrUrgaUDTmCxg8BfGwxOpjOGxulFJnep63bqaeMOuDO5YwSsQRPQUmlx45v+pt'
+ '7017T3/Rs9u1olaRvnghm95+M9zzmCAf6swn60jgf4Ga9TqeQgi5X4NZ6JvpHMX8lEplG87FxC'
+ 'YyhuUpEtruVbkQzFLxuRKbYDJaV1SDl5GgxzwQ4AzMN4HalF6JpUYCI/YaDzBnb+7ZWtZZKJD2'
+ 'bkY7MvoJ6biXfrGJvGmZiacdvDhyZxkcdHYtoJ4dF3Qo1EeivSkYrUJrOgnd+g+b+MHduDB5Mo'
+ 'SUQ6Xoksi+bCzt/Ooz/mQXYEFVum4pUwBFtDyEPXGGF7EKQimo1lsrwzS/tDXweuUJ/9QcAf2e'
+ 'OyK6h+fAB20C/u3P/EkduH96XzBToOewMP/7Md4x+aPn2S9uPPVBTOl7/7UGZeyXlC4G8CpMrc'
+ 'ksPO/iL8XK7v0ni+UPzsy2cv+PVC8PNFKlfxZsVfbdRCplmfv1ouuSnKOGLrCbM+8J8zweUNV4'
+ 's445ncpFPBp3ImOMq5RIhEzPjkjkf869E3p5m6Wwrgy3gqkAibIsWnUcIngt/ITTLjcYJBwX/4'
+ '/vzi9eiC38RLdE95pIAvlFpnLweDmbgVS7kWiDSXcr4U/alcDTCQnObzB9Y+G0yyGYAPhDJ3y6'
+ '8BAC07zAucKsFnt+wQ5gYYr4BXdqjDXKCXUIWSFpjCWxUKDWDnSaFcVGff2jYsrNCRTRGtyruF'
+ 'wrZK87RQ2FZ5cQXHQD2kqDkN0m7y1+Kd4tEtvrdogruqaP6SfwUaz9MMNX9Pz/EMXp02YBtKWn'
+ 'jMq9PagbbWsqSTfqGwrx48KBSOrdeZdcEUo6F1IejCaL1hK0lJJwsXvQoL3ufQhT1qGnjCvKbT'
+ 'vg/+eQ6v25v+vhlLNHxo4YmBD2nz0FgTAx/ascTAhxaeGPjQwhMN36JH1kXDt2jYsJUavvXeRU'
+ '9vBe1CoUvr4NDAU+YdOkf3wT/L4fWkQz9vcJl3jD+Yz78qFxuO/SPD6eptu3TfcLpm2y49fmxY'
+ 'XLNt13K6Zttu0CwUcnbbe9YFUz3ati562x7t7ttKvW2PBoXSpbBbKHTphS3rgl/BE7prXSi6nN'
+ 'Be21bSkk6WC4UuJ1u1QqHLSWNnUjb/Hc//AbuGQxI=')))
+_INDEX = {
+ f.name: {
+ 'descriptor': f,
+ 'services': {s.name: s for s in f.service},
+ }
+ for f in FILE_DESCRIPTOR_SET.file
+}
+
+
+SitewideServiceDescription = {
+ 'file_descriptor_set': FILE_DESCRIPTOR_SET,
+ 'file_descriptor': _INDEX[u'api/api_proto/sitewide.proto']['descriptor'],
+ 'service_descriptor': _INDEX[u'api/api_proto/sitewide.proto']['services'][u'Sitewide'],
+}
diff --git a/api/api_proto/user_objects.proto b/api/api_proto/user_objects.proto
new file mode 100644
index 0000000..20f0eed
--- /dev/null
+++ b/api/api_proto/user_objects.proto
@@ -0,0 +1,42 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+// This file defines protobufs for issues and related business
+// objects, e.g., field values, comments, and attachments.
+
+syntax = "proto3";
+
+import "api/api_proto/common.proto";
+
+package monorail;
+
+// TODO(jojwang): monorail:1701, fill User with all info necessary for
+// creating a user profile page.
+// Next available tag: 7
+message User {
+ string display_name = 1;
+ int64 user_id = 2;
+ bool is_site_admin = 3;
+ string availability = 4;
+ UserRef linked_parent_ref = 5;
+ repeated UserRef linked_child_refs = 6;
+}
+
+
+// Next available tag: 3
+message UserPrefValue {
+ string name = 1;
+ string value = 2;
+}
+
+
+// Next available tag: 6
+message UserProjects {
+ UserRef user_ref = 1;
+ repeated string owner_of = 2;
+ repeated string member_of = 3;
+ repeated string contributor_to = 4;
+ repeated string starred_projects = 5;
+}
diff --git a/api/api_proto/user_objects_pb2.py b/api/api_proto/user_objects_pb2.py
new file mode 100644
index 0000000..c4e09f1
--- /dev/null
+++ b/api/api_proto/user_objects_pb2.py
@@ -0,0 +1,222 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/api_proto/user_objects.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+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()
+
+
+from api.api_proto import common_pb2 as api_dot_api__proto_dot_common__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='api/api_proto/user_objects.proto',
+ package='monorail',
+ syntax='proto3',
+ serialized_options=None,
+ serialized_pb=_b('\n api/api_proto/user_objects.proto\x12\x08monorail\x1a\x1a\x61pi/api_proto/common.proto\"\xb6\x01\n\x04User\x12\x14\n\x0c\x64isplay_name\x18\x01 \x01(\t\x12\x0f\n\x07user_id\x18\x02 \x01(\x03\x12\x15\n\ris_site_admin\x18\x03 \x01(\x08\x12\x14\n\x0c\x61vailability\x18\x04 \x01(\t\x12,\n\x11linked_parent_ref\x18\x05 \x01(\x0b\x32\x11.monorail.UserRef\x12,\n\x11linked_child_refs\x18\x06 \x03(\x0b\x32\x11.monorail.UserRef\",\n\rUserPrefValue\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"\x8a\x01\n\x0cUserProjects\x12#\n\x08user_ref\x18\x01 \x01(\x0b\x32\x11.monorail.UserRef\x12\x10\n\x08owner_of\x18\x02 \x03(\t\x12\x11\n\tmember_of\x18\x03 \x03(\t\x12\x16\n\x0e\x63ontributor_to\x18\x04 \x03(\t\x12\x18\n\x10starred_projects\x18\x05 \x03(\tb\x06proto3')
+ ,
+ dependencies=[api_dot_api__proto_dot_common__pb2.DESCRIPTOR,])
+
+
+
+
+_USER = _descriptor.Descriptor(
+ name='User',
+ full_name='monorail.User',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='display_name', full_name='monorail.User.display_name', 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),
+ _descriptor.FieldDescriptor(
+ name='user_id', full_name='monorail.User.user_id', index=1,
+ number=2, type=3, cpp_type=2, 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),
+ _descriptor.FieldDescriptor(
+ name='is_site_admin', full_name='monorail.User.is_site_admin', index=2,
+ number=3, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='availability', full_name='monorail.User.availability', 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),
+ _descriptor.FieldDescriptor(
+ name='linked_parent_ref', full_name='monorail.User.linked_parent_ref', index=4,
+ number=5, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='linked_child_refs', full_name='monorail.User.linked_child_refs', index=5,
+ number=6, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=75,
+ serialized_end=257,
+)
+
+
+_USERPREFVALUE = _descriptor.Descriptor(
+ name='UserPrefValue',
+ full_name='monorail.UserPrefValue',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.UserPrefValue.name', 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),
+ _descriptor.FieldDescriptor(
+ name='value', full_name='monorail.UserPrefValue.value', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=259,
+ serialized_end=303,
+)
+
+
+_USERPROJECTS = _descriptor.Descriptor(
+ name='UserProjects',
+ full_name='monorail.UserProjects',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='user_ref', full_name='monorail.UserProjects.user_ref', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='owner_of', full_name='monorail.UserProjects.owner_of', index=1,
+ number=2, 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),
+ _descriptor.FieldDescriptor(
+ name='member_of', full_name='monorail.UserProjects.member_of', 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),
+ _descriptor.FieldDescriptor(
+ name='contributor_to', full_name='monorail.UserProjects.contributor_to', index=3,
+ number=4, 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),
+ _descriptor.FieldDescriptor(
+ name='starred_projects', full_name='monorail.UserProjects.starred_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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=306,
+ serialized_end=444,
+)
+
+_USER.fields_by_name['linked_parent_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_USER.fields_by_name['linked_child_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_USERPROJECTS.fields_by_name['user_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+DESCRIPTOR.message_types_by_name['User'] = _USER
+DESCRIPTOR.message_types_by_name['UserPrefValue'] = _USERPREFVALUE
+DESCRIPTOR.message_types_by_name['UserProjects'] = _USERPROJECTS
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+User = _reflection.GeneratedProtocolMessageType('User', (_message.Message,), dict(
+ DESCRIPTOR = _USER,
+ __module__ = 'api.api_proto.user_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.User)
+ ))
+_sym_db.RegisterMessage(User)
+
+UserPrefValue = _reflection.GeneratedProtocolMessageType('UserPrefValue', (_message.Message,), dict(
+ DESCRIPTOR = _USERPREFVALUE,
+ __module__ = 'api.api_proto.user_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.UserPrefValue)
+ ))
+_sym_db.RegisterMessage(UserPrefValue)
+
+UserProjects = _reflection.GeneratedProtocolMessageType('UserProjects', (_message.Message,), dict(
+ DESCRIPTOR = _USERPROJECTS,
+ __module__ = 'api.api_proto.user_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.UserProjects)
+ ))
+_sym_db.RegisterMessage(UserProjects)
+
+
+# @@protoc_insertion_point(module_scope)
diff --git a/api/api_proto/users.proto b/api/api_proto/users.proto
new file mode 100644
index 0000000..574dccd
--- /dev/null
+++ b/api/api_proto/users.proto
@@ -0,0 +1,193 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+syntax = "proto3";
+
+package monorail;
+
+import "api/api_proto/user_objects.proto";
+import "api/api_proto/common.proto";
+
+service Users {
+ rpc GetUser (GetUserRequest) returns (User) {}
+ rpc ListReferencedUsers (ListReferencedUsersRequest) returns (ListReferencedUsersResponse) {}
+ rpc GetMemberships (GetMembershipsRequest) returns (GetMembershipsResponse) {}
+ rpc GetSavedQueries (GetSavedQueriesRequest) returns (GetSavedQueriesResponse) {}
+ rpc GetUserStarCount (GetUserStarCountRequest) returns (GetUserStarCountResponse) {}
+ rpc StarUser (StarUserRequest) returns (StarUserResponse) {}
+ rpc GetUserPrefs (GetUserPrefsRequest) returns (GetUserPrefsResponse) {}
+ rpc SetUserPrefs (SetUserPrefsRequest) returns (SetUserPrefsResponse) {}
+ // TODO(jrobbins): Merge this into SetUserPrefs.
+ rpc SetExpandPermsPreference (SetExpandPermsPreferenceRequest) returns (SetExpandPermsPreferenceResponse) {}
+ rpc InviteLinkedParent (InviteLinkedParentRequest) returns (InviteLinkedParentResponse) {}
+ rpc AcceptLinkedChild (AcceptLinkedChildRequest) returns (AcceptLinkedChildResponse) {}
+ rpc UnlinkAccounts (UnlinkAccountsRequest) returns (UnlinkAccountsResponse) {}
+ rpc GetUsersProjects (GetUsersProjectsRequest) returns (GetUsersProjectsResponse) {}
+ rpc ExpungeUser (ExpungeUserRequest) returns (ExpungeUserResponse) {}
+}
+
+
+// Next available tag: 4
+message ListReferencedUsersRequest {
+ // emails is deprecated. Use user_refs instead.
+ repeated string emails = 2;
+ repeated UserRef user_refs = 3;
+}
+
+
+message ListReferencedUsersResponse {
+ repeated User users = 1;
+}
+
+
+// Next available tag: 3
+message GetUserRequest {
+ UserRef user_ref = 2;
+}
+
+
+// Next available tag: 3
+message GetMembershipsRequest {
+ UserRef user_ref = 2;
+}
+
+
+// Next available tag: 2
+message GetMembershipsResponse {
+ repeated UserRef group_refs = 1;
+}
+
+
+// Next available tag: 3
+message GetSavedQueriesRequest {
+ UserRef user_ref = 2;
+}
+
+
+// Next available tag: 2
+message GetSavedQueriesResponse {
+ repeated SavedQuery saved_queries = 1;
+}
+
+// Next available tag: 3
+message GetUserStarCountRequest {
+ UserRef user_ref = 2;
+}
+
+
+// Next available tag: 2
+message GetUserStarCountResponse {
+ uint32 star_count = 1;
+}
+
+
+// Next available tag: 4
+message StarUserRequest {
+ UserRef user_ref = 2;
+ bool starred = 3;
+}
+
+
+// Next available tag: 2
+message StarUserResponse {
+ uint32 star_count = 1;
+}
+
+
+// Next available tag: 3
+message SetExpandPermsPreferenceRequest {
+ bool expand_perms = 2;
+}
+
+
+// Next available tag: 1
+message SetExpandPermsPreferenceResponse {
+}
+
+
+// Next available tag: 3
+message GetUserPrefsRequest {
+ // Site admins may get prefs for specific users. Otherwise, it gets
+ // prefs for the signed-in user.
+ UserRef user_ref = 2;
+}
+
+
+// Next available tag: 2
+message GetUserPrefsResponse {
+ repeated UserPrefValue prefs = 1;
+}
+
+
+// Next available tag: 5
+message SetUserPrefsRequest {
+ // Site admins may set prefs for specific users. Otherwise, it sets
+ // prefs for the signed-in user.
+ UserRef user_ref = 2;
+ // The given prefs add to or overwrite current user prefs.
+ repeated UserPrefValue prefs = 3;
+}
+
+
+// Next available tag: 1
+message SetUserPrefsResponse {
+}
+
+
+// Next available tag: 3
+message InviteLinkedParentRequest {
+ string email = 2;
+}
+
+
+// Next available tag: 1
+message InviteLinkedParentResponse {
+}
+
+
+// Next available tag: 3
+message AcceptLinkedChildRequest {
+ string email = 2;
+}
+
+
+// Next available tag: 1
+message AcceptLinkedChildResponse {
+}
+
+
+// Next available tag: 4
+message UnlinkAccountsRequest {
+ UserRef parent = 2;
+ UserRef child = 3;
+}
+
+
+// Next available tag: 1
+message UnlinkAccountsResponse {
+}
+
+
+// Next available tag: 2
+message GetUsersProjectsRequest {
+ repeated UserRef user_refs = 1;
+}
+
+
+// Next available tag: 5
+message GetUsersProjectsResponse {
+ repeated UserProjects users_projects = 1;
+}
+
+
+// Next available tag: 2
+message ExpungeUserRequest {
+ string email = 1;
+}
+
+
+// Next available tag: 1
+message ExpungeUserResponse {
+}
\ No newline at end of file
diff --git a/api/api_proto/users_pb2.py b/api/api_proto/users_pb2.py
new file mode 100644
index 0000000..8d84700
--- /dev/null
+++ b/api/api_proto/users_pb2.py
@@ -0,0 +1,1230 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/api_proto/users.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+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()
+
+
+from api.api_proto import user_objects_pb2 as api_dot_api__proto_dot_user__objects__pb2
+from api.api_proto import common_pb2 as api_dot_api__proto_dot_common__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='api/api_proto/users.proto',
+ package='monorail',
+ syntax='proto3',
+ serialized_options=None,
+ serialized_pb=_b('\n\x19\x61pi/api_proto/users.proto\x12\x08monorail\x1a api/api_proto/user_objects.proto\x1a\x1a\x61pi/api_proto/common.proto\"R\n\x1aListReferencedUsersRequest\x12\x0e\n\x06\x65mails\x18\x02 \x03(\t\x12$\n\tuser_refs\x18\x03 \x03(\x0b\x32\x11.monorail.UserRef\"<\n\x1bListReferencedUsersResponse\x12\x1d\n\x05users\x18\x01 \x03(\x0b\x32\x0e.monorail.User\"5\n\x0eGetUserRequest\x12#\n\x08user_ref\x18\x02 \x01(\x0b\x32\x11.monorail.UserRef\"<\n\x15GetMembershipsRequest\x12#\n\x08user_ref\x18\x02 \x01(\x0b\x32\x11.monorail.UserRef\"?\n\x16GetMembershipsResponse\x12%\n\ngroup_refs\x18\x01 \x03(\x0b\x32\x11.monorail.UserRef\"=\n\x16GetSavedQueriesRequest\x12#\n\x08user_ref\x18\x02 \x01(\x0b\x32\x11.monorail.UserRef\"F\n\x17GetSavedQueriesResponse\x12+\n\rsaved_queries\x18\x01 \x03(\x0b\x32\x14.monorail.SavedQuery\">\n\x17GetUserStarCountRequest\x12#\n\x08user_ref\x18\x02 \x01(\x0b\x32\x11.monorail.UserRef\".\n\x18GetUserStarCountResponse\x12\x12\n\nstar_count\x18\x01 \x01(\r\"G\n\x0fStarUserRequest\x12#\n\x08user_ref\x18\x02 \x01(\x0b\x32\x11.monorail.UserRef\x12\x0f\n\x07starred\x18\x03 \x01(\x08\"&\n\x10StarUserResponse\x12\x12\n\nstar_count\x18\x01 \x01(\r\"7\n\x1fSetExpandPermsPreferenceRequest\x12\x14\n\x0c\x65xpand_perms\x18\x02 \x01(\x08\"\"\n SetExpandPermsPreferenceResponse\":\n\x13GetUserPrefsRequest\x12#\n\x08user_ref\x18\x02 \x01(\x0b\x32\x11.monorail.UserRef\">\n\x14GetUserPrefsResponse\x12&\n\x05prefs\x18\x01 \x03(\x0b\x32\x17.monorail.UserPrefValue\"b\n\x13SetUserPrefsRequest\x12#\n\x08user_ref\x18\x02 \x01(\x0b\x32\x11.monorail.UserRef\x12&\n\x05prefs\x18\x03 \x03(\x0b\x32\x17.monorail.UserPrefValue\"\x16\n\x14SetUserPrefsResponse\"*\n\x19InviteLinkedParentRequest\x12\r\n\x05\x65mail\x18\x02 \x01(\t\"\x1c\n\x1aInviteLinkedParentResponse\")\n\x18\x41\x63\x63\x65ptLinkedChildRequest\x12\r\n\x05\x65mail\x18\x02 \x01(\t\"\x1b\n\x19\x41\x63\x63\x65ptLinkedChildResponse\"\\\n\x15UnlinkAccountsRequest\x12!\n\x06parent\x18\x02 \x01(\x0b\x32\x11.monorail.UserRef\x12 \n\x05\x63hild\x18\x03 \x01(\x0b\x32\x11.monorail.UserRef\"\x18\n\x16UnlinkAccountsResponse\"?\n\x17GetUsersProjectsRequest\x12$\n\tuser_refs\x18\x01 \x03(\x0b\x32\x11.monorail.UserRef\"J\n\x18GetUsersProjectsResponse\x12.\n\x0eusers_projects\x18\x01 \x03(\x0b\x32\x16.monorail.UserProjects\"#\n\x12\x45xpungeUserRequest\x12\r\n\x05\x65mail\x18\x01 \x01(\t\"\x15\n\x13\x45xpungeUserResponse2\xd3\t\n\x05Users\x12\x35\n\x07GetUser\x12\x18.monorail.GetUserRequest\x1a\x0e.monorail.User\"\x00\x12\x64\n\x13ListReferencedUsers\x12$.monorail.ListReferencedUsersRequest\x1a%.monorail.ListReferencedUsersResponse\"\x00\x12U\n\x0eGetMemberships\x12\x1f.monorail.GetMembershipsRequest\x1a .monorail.GetMembershipsResponse\"\x00\x12X\n\x0fGetSavedQueries\x12 .monorail.GetSavedQueriesRequest\x1a!.monorail.GetSavedQueriesResponse\"\x00\x12[\n\x10GetUserStarCount\x12!.monorail.GetUserStarCountRequest\x1a\".monorail.GetUserStarCountResponse\"\x00\x12\x43\n\x08StarUser\x12\x19.monorail.StarUserRequest\x1a\x1a.monorail.StarUserResponse\"\x00\x12O\n\x0cGetUserPrefs\x12\x1d.monorail.GetUserPrefsRequest\x1a\x1e.monorail.GetUserPrefsResponse\"\x00\x12O\n\x0cSetUserPrefs\x12\x1d.monorail.SetUserPrefsRequest\x1a\x1e.monorail.SetUserPrefsResponse\"\x00\x12s\n\x18SetExpandPermsPreference\x12).monorail.SetExpandPermsPreferenceRequest\x1a*.monorail.SetExpandPermsPreferenceResponse\"\x00\x12\x61\n\x12InviteLinkedParent\x12#.monorail.InviteLinkedParentRequest\x1a$.monorail.InviteLinkedParentResponse\"\x00\x12^\n\x11\x41\x63\x63\x65ptLinkedChild\x12\".monorail.AcceptLinkedChildRequest\x1a#.monorail.AcceptLinkedChildResponse\"\x00\x12U\n\x0eUnlinkAccounts\x12\x1f.monorail.UnlinkAccountsRequest\x1a .monorail.UnlinkAccountsResponse\"\x00\x12[\n\x10GetUsersProjects\x12!.monorail.GetUsersProjectsRequest\x1a\".monorail.GetUsersProjectsResponse\"\x00\x12L\n\x0b\x45xpungeUser\x12\x1c.monorail.ExpungeUserRequest\x1a\x1d.monorail.ExpungeUserResponse\"\x00\x62\x06proto3')
+ ,
+ dependencies=[api_dot_api__proto_dot_user__objects__pb2.DESCRIPTOR,api_dot_api__proto_dot_common__pb2.DESCRIPTOR,])
+
+
+
+
+_LISTREFERENCEDUSERSREQUEST = _descriptor.Descriptor(
+ name='ListReferencedUsersRequest',
+ full_name='monorail.ListReferencedUsersRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='emails', full_name='monorail.ListReferencedUsersRequest.emails', index=0,
+ number=2, 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),
+ _descriptor.FieldDescriptor(
+ name='user_refs', full_name='monorail.ListReferencedUsersRequest.user_refs', index=1,
+ number=3, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=101,
+ serialized_end=183,
+)
+
+
+_LISTREFERENCEDUSERSRESPONSE = _descriptor.Descriptor(
+ name='ListReferencedUsersResponse',
+ full_name='monorail.ListReferencedUsersResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='users', full_name='monorail.ListReferencedUsersResponse.users', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=185,
+ serialized_end=245,
+)
+
+
+_GETUSERREQUEST = _descriptor.Descriptor(
+ name='GetUserRequest',
+ full_name='monorail.GetUserRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='user_ref', full_name='monorail.GetUserRequest.user_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=247,
+ serialized_end=300,
+)
+
+
+_GETMEMBERSHIPSREQUEST = _descriptor.Descriptor(
+ name='GetMembershipsRequest',
+ full_name='monorail.GetMembershipsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='user_ref', full_name='monorail.GetMembershipsRequest.user_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=302,
+ serialized_end=362,
+)
+
+
+_GETMEMBERSHIPSRESPONSE = _descriptor.Descriptor(
+ name='GetMembershipsResponse',
+ full_name='monorail.GetMembershipsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='group_refs', full_name='monorail.GetMembershipsResponse.group_refs', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=364,
+ serialized_end=427,
+)
+
+
+_GETSAVEDQUERIESREQUEST = _descriptor.Descriptor(
+ name='GetSavedQueriesRequest',
+ full_name='monorail.GetSavedQueriesRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='user_ref', full_name='monorail.GetSavedQueriesRequest.user_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=429,
+ serialized_end=490,
+)
+
+
+_GETSAVEDQUERIESRESPONSE = _descriptor.Descriptor(
+ name='GetSavedQueriesResponse',
+ full_name='monorail.GetSavedQueriesResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='saved_queries', full_name='monorail.GetSavedQueriesResponse.saved_queries', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=492,
+ serialized_end=562,
+)
+
+
+_GETUSERSTARCOUNTREQUEST = _descriptor.Descriptor(
+ name='GetUserStarCountRequest',
+ full_name='monorail.GetUserStarCountRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='user_ref', full_name='monorail.GetUserStarCountRequest.user_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=564,
+ serialized_end=626,
+)
+
+
+_GETUSERSTARCOUNTRESPONSE = _descriptor.Descriptor(
+ name='GetUserStarCountResponse',
+ full_name='monorail.GetUserStarCountResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='star_count', full_name='monorail.GetUserStarCountResponse.star_count', index=0,
+ number=1, type=13, cpp_type=3, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=628,
+ serialized_end=674,
+)
+
+
+_STARUSERREQUEST = _descriptor.Descriptor(
+ name='StarUserRequest',
+ full_name='monorail.StarUserRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='user_ref', full_name='monorail.StarUserRequest.user_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='starred', full_name='monorail.StarUserRequest.starred', index=1,
+ number=3, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=676,
+ serialized_end=747,
+)
+
+
+_STARUSERRESPONSE = _descriptor.Descriptor(
+ name='StarUserResponse',
+ full_name='monorail.StarUserResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='star_count', full_name='monorail.StarUserResponse.star_count', index=0,
+ number=1, type=13, cpp_type=3, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=749,
+ serialized_end=787,
+)
+
+
+_SETEXPANDPERMSPREFERENCEREQUEST = _descriptor.Descriptor(
+ name='SetExpandPermsPreferenceRequest',
+ full_name='monorail.SetExpandPermsPreferenceRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='expand_perms', full_name='monorail.SetExpandPermsPreferenceRequest.expand_perms', index=0,
+ number=2, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=789,
+ serialized_end=844,
+)
+
+
+_SETEXPANDPERMSPREFERENCERESPONSE = _descriptor.Descriptor(
+ name='SetExpandPermsPreferenceResponse',
+ full_name='monorail.SetExpandPermsPreferenceResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=846,
+ serialized_end=880,
+)
+
+
+_GETUSERPREFSREQUEST = _descriptor.Descriptor(
+ name='GetUserPrefsRequest',
+ full_name='monorail.GetUserPrefsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='user_ref', full_name='monorail.GetUserPrefsRequest.user_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=882,
+ serialized_end=940,
+)
+
+
+_GETUSERPREFSRESPONSE = _descriptor.Descriptor(
+ name='GetUserPrefsResponse',
+ full_name='monorail.GetUserPrefsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='prefs', full_name='monorail.GetUserPrefsResponse.prefs', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=942,
+ serialized_end=1004,
+)
+
+
+_SETUSERPREFSREQUEST = _descriptor.Descriptor(
+ name='SetUserPrefsRequest',
+ full_name='monorail.SetUserPrefsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='user_ref', full_name='monorail.SetUserPrefsRequest.user_ref', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='prefs', full_name='monorail.SetUserPrefsRequest.prefs', index=1,
+ number=3, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1006,
+ serialized_end=1104,
+)
+
+
+_SETUSERPREFSRESPONSE = _descriptor.Descriptor(
+ name='SetUserPrefsResponse',
+ full_name='monorail.SetUserPrefsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1106,
+ serialized_end=1128,
+)
+
+
+_INVITELINKEDPARENTREQUEST = _descriptor.Descriptor(
+ name='InviteLinkedParentRequest',
+ full_name='monorail.InviteLinkedParentRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='email', full_name='monorail.InviteLinkedParentRequest.email', index=0,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1130,
+ serialized_end=1172,
+)
+
+
+_INVITELINKEDPARENTRESPONSE = _descriptor.Descriptor(
+ name='InviteLinkedParentResponse',
+ full_name='monorail.InviteLinkedParentResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1174,
+ serialized_end=1202,
+)
+
+
+_ACCEPTLINKEDCHILDREQUEST = _descriptor.Descriptor(
+ name='AcceptLinkedChildRequest',
+ full_name='monorail.AcceptLinkedChildRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='email', full_name='monorail.AcceptLinkedChildRequest.email', index=0,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1204,
+ serialized_end=1245,
+)
+
+
+_ACCEPTLINKEDCHILDRESPONSE = _descriptor.Descriptor(
+ name='AcceptLinkedChildResponse',
+ full_name='monorail.AcceptLinkedChildResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1247,
+ serialized_end=1274,
+)
+
+
+_UNLINKACCOUNTSREQUEST = _descriptor.Descriptor(
+ name='UnlinkAccountsRequest',
+ full_name='monorail.UnlinkAccountsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='parent', full_name='monorail.UnlinkAccountsRequest.parent', index=0,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ _descriptor.FieldDescriptor(
+ name='child', full_name='monorail.UnlinkAccountsRequest.child', index=1,
+ number=3, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=None, file=DESCRIPTOR),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1276,
+ serialized_end=1368,
+)
+
+
+_UNLINKACCOUNTSRESPONSE = _descriptor.Descriptor(
+ name='UnlinkAccountsResponse',
+ full_name='monorail.UnlinkAccountsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1370,
+ serialized_end=1394,
+)
+
+
+_GETUSERSPROJECTSREQUEST = _descriptor.Descriptor(
+ name='GetUsersProjectsRequest',
+ full_name='monorail.GetUsersProjectsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='user_refs', full_name='monorail.GetUsersProjectsRequest.user_refs', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1396,
+ serialized_end=1459,
+)
+
+
+_GETUSERSPROJECTSRESPONSE = _descriptor.Descriptor(
+ name='GetUsersProjectsResponse',
+ full_name='monorail.GetUsersProjectsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='users_projects', full_name='monorail.GetUsersProjectsResponse.users_projects', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1461,
+ serialized_end=1535,
+)
+
+
+_EXPUNGEUSERREQUEST = _descriptor.Descriptor(
+ name='ExpungeUserRequest',
+ full_name='monorail.ExpungeUserRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='email', full_name='monorail.ExpungeUserRequest.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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1537,
+ serialized_end=1572,
+)
+
+
+_EXPUNGEUSERRESPONSE = _descriptor.Descriptor(
+ name='ExpungeUserResponse',
+ full_name='monorail.ExpungeUserResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1574,
+ serialized_end=1595,
+)
+
+_LISTREFERENCEDUSERSREQUEST.fields_by_name['user_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_LISTREFERENCEDUSERSRESPONSE.fields_by_name['users'].message_type = api_dot_api__proto_dot_user__objects__pb2._USER
+_GETUSERREQUEST.fields_by_name['user_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_GETMEMBERSHIPSREQUEST.fields_by_name['user_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_GETMEMBERSHIPSRESPONSE.fields_by_name['group_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_GETSAVEDQUERIESREQUEST.fields_by_name['user_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_GETSAVEDQUERIESRESPONSE.fields_by_name['saved_queries'].message_type = api_dot_api__proto_dot_common__pb2._SAVEDQUERY
+_GETUSERSTARCOUNTREQUEST.fields_by_name['user_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_STARUSERREQUEST.fields_by_name['user_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_GETUSERPREFSREQUEST.fields_by_name['user_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_GETUSERPREFSRESPONSE.fields_by_name['prefs'].message_type = api_dot_api__proto_dot_user__objects__pb2._USERPREFVALUE
+_SETUSERPREFSREQUEST.fields_by_name['user_ref'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_SETUSERPREFSREQUEST.fields_by_name['prefs'].message_type = api_dot_api__proto_dot_user__objects__pb2._USERPREFVALUE
+_UNLINKACCOUNTSREQUEST.fields_by_name['parent'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_UNLINKACCOUNTSREQUEST.fields_by_name['child'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_GETUSERSPROJECTSREQUEST.fields_by_name['user_refs'].message_type = api_dot_api__proto_dot_common__pb2._USERREF
+_GETUSERSPROJECTSRESPONSE.fields_by_name['users_projects'].message_type = api_dot_api__proto_dot_user__objects__pb2._USERPROJECTS
+DESCRIPTOR.message_types_by_name['ListReferencedUsersRequest'] = _LISTREFERENCEDUSERSREQUEST
+DESCRIPTOR.message_types_by_name['ListReferencedUsersResponse'] = _LISTREFERENCEDUSERSRESPONSE
+DESCRIPTOR.message_types_by_name['GetUserRequest'] = _GETUSERREQUEST
+DESCRIPTOR.message_types_by_name['GetMembershipsRequest'] = _GETMEMBERSHIPSREQUEST
+DESCRIPTOR.message_types_by_name['GetMembershipsResponse'] = _GETMEMBERSHIPSRESPONSE
+DESCRIPTOR.message_types_by_name['GetSavedQueriesRequest'] = _GETSAVEDQUERIESREQUEST
+DESCRIPTOR.message_types_by_name['GetSavedQueriesResponse'] = _GETSAVEDQUERIESRESPONSE
+DESCRIPTOR.message_types_by_name['GetUserStarCountRequest'] = _GETUSERSTARCOUNTREQUEST
+DESCRIPTOR.message_types_by_name['GetUserStarCountResponse'] = _GETUSERSTARCOUNTRESPONSE
+DESCRIPTOR.message_types_by_name['StarUserRequest'] = _STARUSERREQUEST
+DESCRIPTOR.message_types_by_name['StarUserResponse'] = _STARUSERRESPONSE
+DESCRIPTOR.message_types_by_name['SetExpandPermsPreferenceRequest'] = _SETEXPANDPERMSPREFERENCEREQUEST
+DESCRIPTOR.message_types_by_name['SetExpandPermsPreferenceResponse'] = _SETEXPANDPERMSPREFERENCERESPONSE
+DESCRIPTOR.message_types_by_name['GetUserPrefsRequest'] = _GETUSERPREFSREQUEST
+DESCRIPTOR.message_types_by_name['GetUserPrefsResponse'] = _GETUSERPREFSRESPONSE
+DESCRIPTOR.message_types_by_name['SetUserPrefsRequest'] = _SETUSERPREFSREQUEST
+DESCRIPTOR.message_types_by_name['SetUserPrefsResponse'] = _SETUSERPREFSRESPONSE
+DESCRIPTOR.message_types_by_name['InviteLinkedParentRequest'] = _INVITELINKEDPARENTREQUEST
+DESCRIPTOR.message_types_by_name['InviteLinkedParentResponse'] = _INVITELINKEDPARENTRESPONSE
+DESCRIPTOR.message_types_by_name['AcceptLinkedChildRequest'] = _ACCEPTLINKEDCHILDREQUEST
+DESCRIPTOR.message_types_by_name['AcceptLinkedChildResponse'] = _ACCEPTLINKEDCHILDRESPONSE
+DESCRIPTOR.message_types_by_name['UnlinkAccountsRequest'] = _UNLINKACCOUNTSREQUEST
+DESCRIPTOR.message_types_by_name['UnlinkAccountsResponse'] = _UNLINKACCOUNTSRESPONSE
+DESCRIPTOR.message_types_by_name['GetUsersProjectsRequest'] = _GETUSERSPROJECTSREQUEST
+DESCRIPTOR.message_types_by_name['GetUsersProjectsResponse'] = _GETUSERSPROJECTSRESPONSE
+DESCRIPTOR.message_types_by_name['ExpungeUserRequest'] = _EXPUNGEUSERREQUEST
+DESCRIPTOR.message_types_by_name['ExpungeUserResponse'] = _EXPUNGEUSERRESPONSE
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+ListReferencedUsersRequest = _reflection.GeneratedProtocolMessageType('ListReferencedUsersRequest', (_message.Message,), dict(
+ DESCRIPTOR = _LISTREFERENCEDUSERSREQUEST,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListReferencedUsersRequest)
+ ))
+_sym_db.RegisterMessage(ListReferencedUsersRequest)
+
+ListReferencedUsersResponse = _reflection.GeneratedProtocolMessageType('ListReferencedUsersResponse', (_message.Message,), dict(
+ DESCRIPTOR = _LISTREFERENCEDUSERSRESPONSE,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ListReferencedUsersResponse)
+ ))
+_sym_db.RegisterMessage(ListReferencedUsersResponse)
+
+GetUserRequest = _reflection.GeneratedProtocolMessageType('GetUserRequest', (_message.Message,), dict(
+ DESCRIPTOR = _GETUSERREQUEST,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetUserRequest)
+ ))
+_sym_db.RegisterMessage(GetUserRequest)
+
+GetMembershipsRequest = _reflection.GeneratedProtocolMessageType('GetMembershipsRequest', (_message.Message,), dict(
+ DESCRIPTOR = _GETMEMBERSHIPSREQUEST,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetMembershipsRequest)
+ ))
+_sym_db.RegisterMessage(GetMembershipsRequest)
+
+GetMembershipsResponse = _reflection.GeneratedProtocolMessageType('GetMembershipsResponse', (_message.Message,), dict(
+ DESCRIPTOR = _GETMEMBERSHIPSRESPONSE,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetMembershipsResponse)
+ ))
+_sym_db.RegisterMessage(GetMembershipsResponse)
+
+GetSavedQueriesRequest = _reflection.GeneratedProtocolMessageType('GetSavedQueriesRequest', (_message.Message,), dict(
+ DESCRIPTOR = _GETSAVEDQUERIESREQUEST,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetSavedQueriesRequest)
+ ))
+_sym_db.RegisterMessage(GetSavedQueriesRequest)
+
+GetSavedQueriesResponse = _reflection.GeneratedProtocolMessageType('GetSavedQueriesResponse', (_message.Message,), dict(
+ DESCRIPTOR = _GETSAVEDQUERIESRESPONSE,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetSavedQueriesResponse)
+ ))
+_sym_db.RegisterMessage(GetSavedQueriesResponse)
+
+GetUserStarCountRequest = _reflection.GeneratedProtocolMessageType('GetUserStarCountRequest', (_message.Message,), dict(
+ DESCRIPTOR = _GETUSERSTARCOUNTREQUEST,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetUserStarCountRequest)
+ ))
+_sym_db.RegisterMessage(GetUserStarCountRequest)
+
+GetUserStarCountResponse = _reflection.GeneratedProtocolMessageType('GetUserStarCountResponse', (_message.Message,), dict(
+ DESCRIPTOR = _GETUSERSTARCOUNTRESPONSE,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetUserStarCountResponse)
+ ))
+_sym_db.RegisterMessage(GetUserStarCountResponse)
+
+StarUserRequest = _reflection.GeneratedProtocolMessageType('StarUserRequest', (_message.Message,), dict(
+ DESCRIPTOR = _STARUSERREQUEST,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.StarUserRequest)
+ ))
+_sym_db.RegisterMessage(StarUserRequest)
+
+StarUserResponse = _reflection.GeneratedProtocolMessageType('StarUserResponse', (_message.Message,), dict(
+ DESCRIPTOR = _STARUSERRESPONSE,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.StarUserResponse)
+ ))
+_sym_db.RegisterMessage(StarUserResponse)
+
+SetExpandPermsPreferenceRequest = _reflection.GeneratedProtocolMessageType('SetExpandPermsPreferenceRequest', (_message.Message,), dict(
+ DESCRIPTOR = _SETEXPANDPERMSPREFERENCEREQUEST,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.SetExpandPermsPreferenceRequest)
+ ))
+_sym_db.RegisterMessage(SetExpandPermsPreferenceRequest)
+
+SetExpandPermsPreferenceResponse = _reflection.GeneratedProtocolMessageType('SetExpandPermsPreferenceResponse', (_message.Message,), dict(
+ DESCRIPTOR = _SETEXPANDPERMSPREFERENCERESPONSE,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.SetExpandPermsPreferenceResponse)
+ ))
+_sym_db.RegisterMessage(SetExpandPermsPreferenceResponse)
+
+GetUserPrefsRequest = _reflection.GeneratedProtocolMessageType('GetUserPrefsRequest', (_message.Message,), dict(
+ DESCRIPTOR = _GETUSERPREFSREQUEST,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetUserPrefsRequest)
+ ))
+_sym_db.RegisterMessage(GetUserPrefsRequest)
+
+GetUserPrefsResponse = _reflection.GeneratedProtocolMessageType('GetUserPrefsResponse', (_message.Message,), dict(
+ DESCRIPTOR = _GETUSERPREFSRESPONSE,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetUserPrefsResponse)
+ ))
+_sym_db.RegisterMessage(GetUserPrefsResponse)
+
+SetUserPrefsRequest = _reflection.GeneratedProtocolMessageType('SetUserPrefsRequest', (_message.Message,), dict(
+ DESCRIPTOR = _SETUSERPREFSREQUEST,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.SetUserPrefsRequest)
+ ))
+_sym_db.RegisterMessage(SetUserPrefsRequest)
+
+SetUserPrefsResponse = _reflection.GeneratedProtocolMessageType('SetUserPrefsResponse', (_message.Message,), dict(
+ DESCRIPTOR = _SETUSERPREFSRESPONSE,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.SetUserPrefsResponse)
+ ))
+_sym_db.RegisterMessage(SetUserPrefsResponse)
+
+InviteLinkedParentRequest = _reflection.GeneratedProtocolMessageType('InviteLinkedParentRequest', (_message.Message,), dict(
+ DESCRIPTOR = _INVITELINKEDPARENTREQUEST,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.InviteLinkedParentRequest)
+ ))
+_sym_db.RegisterMessage(InviteLinkedParentRequest)
+
+InviteLinkedParentResponse = _reflection.GeneratedProtocolMessageType('InviteLinkedParentResponse', (_message.Message,), dict(
+ DESCRIPTOR = _INVITELINKEDPARENTRESPONSE,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.InviteLinkedParentResponse)
+ ))
+_sym_db.RegisterMessage(InviteLinkedParentResponse)
+
+AcceptLinkedChildRequest = _reflection.GeneratedProtocolMessageType('AcceptLinkedChildRequest', (_message.Message,), dict(
+ DESCRIPTOR = _ACCEPTLINKEDCHILDREQUEST,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.AcceptLinkedChildRequest)
+ ))
+_sym_db.RegisterMessage(AcceptLinkedChildRequest)
+
+AcceptLinkedChildResponse = _reflection.GeneratedProtocolMessageType('AcceptLinkedChildResponse', (_message.Message,), dict(
+ DESCRIPTOR = _ACCEPTLINKEDCHILDRESPONSE,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.AcceptLinkedChildResponse)
+ ))
+_sym_db.RegisterMessage(AcceptLinkedChildResponse)
+
+UnlinkAccountsRequest = _reflection.GeneratedProtocolMessageType('UnlinkAccountsRequest', (_message.Message,), dict(
+ DESCRIPTOR = _UNLINKACCOUNTSREQUEST,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.UnlinkAccountsRequest)
+ ))
+_sym_db.RegisterMessage(UnlinkAccountsRequest)
+
+UnlinkAccountsResponse = _reflection.GeneratedProtocolMessageType('UnlinkAccountsResponse', (_message.Message,), dict(
+ DESCRIPTOR = _UNLINKACCOUNTSRESPONSE,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.UnlinkAccountsResponse)
+ ))
+_sym_db.RegisterMessage(UnlinkAccountsResponse)
+
+GetUsersProjectsRequest = _reflection.GeneratedProtocolMessageType('GetUsersProjectsRequest', (_message.Message,), dict(
+ DESCRIPTOR = _GETUSERSPROJECTSREQUEST,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetUsersProjectsRequest)
+ ))
+_sym_db.RegisterMessage(GetUsersProjectsRequest)
+
+GetUsersProjectsResponse = _reflection.GeneratedProtocolMessageType('GetUsersProjectsResponse', (_message.Message,), dict(
+ DESCRIPTOR = _GETUSERSPROJECTSRESPONSE,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.GetUsersProjectsResponse)
+ ))
+_sym_db.RegisterMessage(GetUsersProjectsResponse)
+
+ExpungeUserRequest = _reflection.GeneratedProtocolMessageType('ExpungeUserRequest', (_message.Message,), dict(
+ DESCRIPTOR = _EXPUNGEUSERREQUEST,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ExpungeUserRequest)
+ ))
+_sym_db.RegisterMessage(ExpungeUserRequest)
+
+ExpungeUserResponse = _reflection.GeneratedProtocolMessageType('ExpungeUserResponse', (_message.Message,), dict(
+ DESCRIPTOR = _EXPUNGEUSERRESPONSE,
+ __module__ = 'api.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.ExpungeUserResponse)
+ ))
+_sym_db.RegisterMessage(ExpungeUserResponse)
+
+
+
+_USERS = _descriptor.ServiceDescriptor(
+ name='Users',
+ full_name='monorail.Users',
+ file=DESCRIPTOR,
+ index=0,
+ serialized_options=None,
+ serialized_start=1598,
+ serialized_end=2833,
+ methods=[
+ _descriptor.MethodDescriptor(
+ name='GetUser',
+ full_name='monorail.Users.GetUser',
+ index=0,
+ containing_service=None,
+ input_type=_GETUSERREQUEST,
+ output_type=api_dot_api__proto_dot_user__objects__pb2._USER,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListReferencedUsers',
+ full_name='monorail.Users.ListReferencedUsers',
+ index=1,
+ containing_service=None,
+ input_type=_LISTREFERENCEDUSERSREQUEST,
+ output_type=_LISTREFERENCEDUSERSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='GetMemberships',
+ full_name='monorail.Users.GetMemberships',
+ index=2,
+ containing_service=None,
+ input_type=_GETMEMBERSHIPSREQUEST,
+ output_type=_GETMEMBERSHIPSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='GetSavedQueries',
+ full_name='monorail.Users.GetSavedQueries',
+ index=3,
+ containing_service=None,
+ input_type=_GETSAVEDQUERIESREQUEST,
+ output_type=_GETSAVEDQUERIESRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='GetUserStarCount',
+ full_name='monorail.Users.GetUserStarCount',
+ index=4,
+ containing_service=None,
+ input_type=_GETUSERSTARCOUNTREQUEST,
+ output_type=_GETUSERSTARCOUNTRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='StarUser',
+ full_name='monorail.Users.StarUser',
+ index=5,
+ containing_service=None,
+ input_type=_STARUSERREQUEST,
+ output_type=_STARUSERRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='GetUserPrefs',
+ full_name='monorail.Users.GetUserPrefs',
+ index=6,
+ containing_service=None,
+ input_type=_GETUSERPREFSREQUEST,
+ output_type=_GETUSERPREFSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='SetUserPrefs',
+ full_name='monorail.Users.SetUserPrefs',
+ index=7,
+ containing_service=None,
+ input_type=_SETUSERPREFSREQUEST,
+ output_type=_SETUSERPREFSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='SetExpandPermsPreference',
+ full_name='monorail.Users.SetExpandPermsPreference',
+ index=8,
+ containing_service=None,
+ input_type=_SETEXPANDPERMSPREFERENCEREQUEST,
+ output_type=_SETEXPANDPERMSPREFERENCERESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='InviteLinkedParent',
+ full_name='monorail.Users.InviteLinkedParent',
+ index=9,
+ containing_service=None,
+ input_type=_INVITELINKEDPARENTREQUEST,
+ output_type=_INVITELINKEDPARENTRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='AcceptLinkedChild',
+ full_name='monorail.Users.AcceptLinkedChild',
+ index=10,
+ containing_service=None,
+ input_type=_ACCEPTLINKEDCHILDREQUEST,
+ output_type=_ACCEPTLINKEDCHILDRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='UnlinkAccounts',
+ full_name='monorail.Users.UnlinkAccounts',
+ index=11,
+ containing_service=None,
+ input_type=_UNLINKACCOUNTSREQUEST,
+ output_type=_UNLINKACCOUNTSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='GetUsersProjects',
+ full_name='monorail.Users.GetUsersProjects',
+ index=12,
+ containing_service=None,
+ input_type=_GETUSERSPROJECTSREQUEST,
+ output_type=_GETUSERSPROJECTSRESPONSE,
+ serialized_options=None,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ExpungeUser',
+ full_name='monorail.Users.ExpungeUser',
+ index=13,
+ containing_service=None,
+ input_type=_EXPUNGEUSERREQUEST,
+ output_type=_EXPUNGEUSERRESPONSE,
+ serialized_options=None,
+ ),
+])
+_sym_db.RegisterServiceDescriptor(_USERS)
+
+DESCRIPTOR.services_by_name['Users'] = _USERS
+
+# @@protoc_insertion_point(module_scope)
diff --git a/api/api_proto/users_prpc_pb2.py b/api/api_proto/users_prpc_pb2.py
new file mode 100644
index 0000000..9c99700
--- /dev/null
+++ b/api/api_proto/users_prpc_pb2.py
@@ -0,0 +1,129 @@
+# Generated by the pRPC protocol buffer compiler plugin. DO NOT EDIT!
+# source: api/api_proto/users.proto
+
+import base64
+import zlib
+
+from google.protobuf import descriptor_pb2
+
+# Includes description of the api/api_proto/users.proto and all of its transitive
+# dependencies. Includes source code info.
+FILE_DESCRIPTOR_SET = descriptor_pb2.FileDescriptorSet()
+FILE_DESCRIPTOR_SET.ParseFromString(zlib.decompress(base64.b64decode(
+ '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')))
+_INDEX = {
+ f.name: {
+ 'descriptor': f,
+ 'services': {s.name: s for s in f.service},
+ }
+ for f in FILE_DESCRIPTOR_SET.file
+}
+
+
+UsersServiceDescription = {
+ 'file_descriptor_set': FILE_DESCRIPTOR_SET,
+ 'file_descriptor': _INDEX[u'api/api_proto/users.proto']['descriptor'],
+ 'service_descriptor': _INDEX[u'api/api_proto/users.proto']['services'][u'Users'],
+}
diff --git a/api/api_routes.py b/api/api_routes.py
new file mode 100644
index 0000000..093323b
--- /dev/null
+++ b/api/api_routes.py
@@ -0,0 +1,46 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+# This file implements a pRPC API for Monorail.
+#
+# See the pRPC spec here: https://godoc.org/github.com/luci/luci-go/grpc/prpc
+#
+# Each Servicer corresponds to a service defined in a .proto file in this
+# directory. Each method on that Servicer corresponds to one of the rpcs
+# defined on the service.
+#
+# All APIs are served under the /prpc/* path space. Each service gets its own
+# namespace under that, and each method is an individual endpoints. For example,
+# POST https://bugs.chromium.org/prpc/monorail.Users/GetUser
+# would be a call to the api.users_servicer.UsersServicer.GetUser method.
+#
+# Note that this is not a RESTful API, although it is CRUDy. All requests are
+# POSTs, all methods take exactly one input, and all methods return exactly
+# one output.
+#
+# TODO(http://crbug.com/monorail/1703): Actually integrate the rpcexplorer.
+# You can use the API Explorer here: https://bugs.chromium.org/rpcexplorer
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from api import features_servicer
+from api import issues_servicer
+from api import projects_servicer
+from api import sitewide_servicer
+from api import users_servicer
+
+
+def RegisterApiHandlers(prpc_server, services):
+ """Registers pRPC API services. And makes their routes
+ available in prpc_server.get_routes().
+ """
+
+ prpc_server.add_service(features_servicer.FeaturesServicer(services))
+ prpc_server.add_service(issues_servicer.IssuesServicer(services))
+ prpc_server.add_service(projects_servicer.ProjectsServicer(services))
+ prpc_server.add_service(sitewide_servicer.SitewideServicer(services))
+ prpc_server.add_service(users_servicer.UsersServicer(services))
diff --git a/api/converters.py b/api/converters.py
new file mode 100644
index 0000000..4f01a8b
--- /dev/null
+++ b/api/converters.py
@@ -0,0 +1,1147 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Functions that convert protorpc business objects into protoc objects.
+
+Monorail uses protorpc objects internally, whereas the API uses protoc
+objects. The difference is not just the choice of protobuf library, there
+will always be a need for conversion because out internal objects may have
+field that we do not want to expose externally, or the format of some fields
+may be different than how we want to expose them.
+"""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+
+from six import string_types
+
+import settings
+from api.api_proto import common_pb2
+from api.api_proto import features_objects_pb2
+from api.api_proto import issue_objects_pb2
+from api.api_proto import project_objects_pb2
+from api.api_proto import user_objects_pb2
+from features import federated
+from framework import exceptions
+from framework import filecontent
+from framework import framework_constants
+from framework import framework_helpers
+from framework import permissions
+from framework import validate
+from services import features_svc
+from tracker import attachment_helpers
+from tracker import field_helpers
+from tracker import tracker_bizobj
+from tracker import tracker_helpers
+from proto import tracker_pb2
+from proto import user_pb2
+
+
+# Convert and ingest objects in issue_objects.proto.
+
+
+def ConvertApprovalValues(approval_values, phases, users_by_id, config):
+ """Convert a list of ApprovalValue into protoc Approvals."""
+ phases_by_id = {
+ phase.phase_id: phase
+ for phase in phases}
+ result = [
+ ConvertApproval(
+ av, users_by_id, config, phase=phases_by_id.get(av.phase_id))
+ for av in approval_values]
+ result = [av for av in result if av]
+ return result
+
+
+def ConvertApproval(approval_value, users_by_id, config, phase=None):
+ """Use the given ApprovalValue to create a protoc Approval."""
+ approval_name = ''
+ fd = tracker_bizobj.FindFieldDefByID(approval_value.approval_id, config)
+ if fd:
+ approval_name = fd.field_name
+ else:
+ logging.info(
+ 'Ignoring approval value referencing a non-existing field: %r',
+ approval_value)
+ return None
+
+ field_ref = ConvertFieldRef(
+ approval_value.approval_id, approval_name,
+ tracker_pb2.FieldTypes.APPROVAL_TYPE, None)
+ approver_refs = ConvertUserRefs(approval_value.approver_ids, [], users_by_id,
+ False)
+ setter_ref = ConvertUserRef(approval_value.setter_id, None, users_by_id)
+
+ status = ConvertApprovalStatus(approval_value.status)
+ set_on = approval_value.set_on
+
+ phase_ref = issue_objects_pb2.PhaseRef()
+ if phase:
+ phase_ref.phase_name = phase.name
+
+ result = issue_objects_pb2.Approval(
+ field_ref=field_ref, approver_refs=approver_refs,
+ status=status, set_on=set_on, setter_ref=setter_ref,
+ phase_ref=phase_ref)
+ return result
+
+
+def ConvertStatusRef(explicit_status, derived_status, config):
+ """Use the given status strings to create a StatusRef."""
+ status = explicit_status or derived_status
+ is_derived = not explicit_status
+ if not status:
+ return common_pb2.StatusRef(
+ status=framework_constants.NO_VALUES, is_derived=False, means_open=True)
+
+ return common_pb2.StatusRef(
+ status=status,
+ is_derived=is_derived,
+ means_open=tracker_helpers.MeansOpenInProject(status, config))
+
+
+def ConvertApprovalStatus(status):
+ """Use the given protorpc ApprovalStatus to create a protoc ApprovalStatus"""
+ return issue_objects_pb2.ApprovalStatus.Value(status.name)
+
+
+def ConvertUserRef(explicit_user_id, derived_user_id, users_by_id):
+ """Use the given user IDs to create a UserRef."""
+ user_id = explicit_user_id or derived_user_id
+ is_derived = not explicit_user_id
+ if not user_id:
+ return None;
+
+ return common_pb2.UserRef(
+ user_id=user_id,
+ is_derived=is_derived,
+ display_name=users_by_id[user_id].display_name)
+
+# TODO(jojwang): Rewrite method, ConvertUserRefs should be able to
+# call ConvertUserRef
+def ConvertUserRefs(explicit_user_ids, derived_user_ids, users_by_id,
+ use_email):
+ # (List(int), List(int), Dict(int: UserView), bool) -> List(UserRef)
+ """Use the given user ID lists to create a list of UserRef.
+
+ Args:
+ explicit_user_ids: list of user_ids for users that are not derived.
+ derived_user_ids: list of user_ids for users derived from FilterRules.
+ users_by_id: dict of {user_id: UserView, ...} for all users in
+ explicit_user_ids and derived_user_ids.
+ use_email: boolean true if the UserView.email should be used as
+ the display_name instead of UserView.display_name, which may be obscured.
+
+ Returns:
+ A single list of UserRefs.
+ """
+ result = []
+ for user_id in explicit_user_ids:
+ result.append(common_pb2.UserRef(
+ user_id=user_id,
+ is_derived=False,
+ display_name=(
+ users_by_id[user_id].email
+ if use_email
+ else users_by_id[user_id].display_name)))
+ for user_id in derived_user_ids:
+ result.append(common_pb2.UserRef(
+ user_id=user_id,
+ is_derived=True,
+ display_name=(
+ users_by_id[user_id].email
+ if use_email
+ else users_by_id[user_id].display_name)))
+ return result
+
+
+def ConvertUsers(users, users_by_id):
+ """Use the given protorpc Users to create protoc Users.
+
+ Args:
+ users: list of protorpc Users to convert.
+ users_by_id: dict {user_id: UserView} of all Users linked
+ from the users list.
+
+ Returns:
+ A list of protoc Users.
+ """
+ result = []
+ for user in users:
+ linked_parent_ref = None
+ if user.linked_parent_id:
+ linked_parent_ref = ConvertUserRefs(
+ [user.linked_parent_id], [], users_by_id, False)[0]
+ linked_child_refs = ConvertUserRefs(
+ user.linked_child_ids, [], users_by_id, False)
+ converted_user = user_objects_pb2.User(
+ user_id=user.user_id,
+ display_name=user.email,
+ is_site_admin=user.is_site_admin,
+ availability=framework_helpers.GetUserAvailability(user)[0],
+ linked_parent_ref=linked_parent_ref,
+ linked_child_refs=linked_child_refs)
+ result.append(converted_user)
+ return result
+
+
+def ConvertPrefValues(userprefvalues):
+ """Convert a list of protorpc UserPrefValue to protoc UserPrefValues."""
+ return [
+ user_objects_pb2.UserPrefValue(name=upv.name, value=upv.value)
+ for upv in userprefvalues]
+
+
+def ConvertLabels(explicit_labels, derived_labels):
+ """Combine the given explicit and derived lists into LabelRefs."""
+ explicit_refs = [common_pb2.LabelRef(label=lab, is_derived=False)
+ for lab in explicit_labels]
+ derived_refs = [common_pb2.LabelRef(label=lab, is_derived=True)
+ for lab in derived_labels]
+ return explicit_refs + derived_refs
+
+
+def ConvertComponentRef(component_id, config, is_derived=False):
+ """Make a ComponentRef from the component_id and project config."""
+ component_def = tracker_bizobj.FindComponentDefByID(component_id, config)
+ if not component_def:
+ logging.info('Ignoring non-existing component id %s', component_id)
+ return None
+ result = common_pb2.ComponentRef(
+ path=component_def.path,
+ is_derived=is_derived)
+ return result
+
+# TODO(jojwang): rename to ConvertComponentRefs
+def ConvertComponents(explicit_component_ids, derived_component_ids, config):
+ """Make a ComponentRef for each component_id."""
+ result = [ConvertComponentRef(cid, config) for cid in explicit_component_ids]
+ result += [
+ ConvertComponentRef(cid, config, is_derived=True)
+ for cid in derived_component_ids]
+ result = [cr for cr in result if cr]
+ return result
+
+
+def ConvertIssueRef(issue_ref_pair, ext_id=''):
+ """Convert (project_name, local_id) to an IssueRef protoc object.
+
+ With optional external ref in ext_id.
+ """
+ project_name, local_id = issue_ref_pair
+ ref = common_pb2.IssueRef(project_name=project_name, local_id=local_id,
+ ext_identifier=ext_id)
+ return ref
+
+
+def ConvertIssueRefs(issue_ids, related_refs_dict):
+ """Convert a list of iids to IssueRef protoc objects."""
+ return [ConvertIssueRef(related_refs_dict[iid]) for iid in issue_ids]
+
+
+def ConvertFieldValue(field_id, field_name, value, field_type,
+ approval_name=None, phase_name=None, is_derived=False):
+ """Convert one field value view item into a protoc FieldValue."""
+ if not isinstance(value, string_types):
+ value = str(value)
+ fv = issue_objects_pb2.FieldValue(
+ field_ref=ConvertFieldRef(field_id, field_name, field_type,
+ approval_name),
+ value=value,
+ is_derived=is_derived)
+ if phase_name:
+ fv.phase_ref.phase_name = phase_name
+
+ return fv
+
+
+def ConvertFieldType(field_type):
+ """Use the given protorpc FieldTypes enum to create a protoc FieldType."""
+ return common_pb2.FieldType.Value(field_type.name)
+
+
+def ConvertFieldRef(field_id, field_name, field_type, approval_name):
+ """Convert a field name and protorpc FieldType into a protoc FieldRef."""
+ return common_pb2.FieldRef(field_id=field_id,
+ field_name=field_name,
+ type=ConvertFieldType(field_type),
+ approval_name=approval_name)
+
+
+def ConvertFieldValues(
+ config, labels, derived_labels, field_values, users_by_id, phases=None):
+ """Convert lists of labels and field_values to protoc FieldValues."""
+ fvs = []
+ phase_names_by_id = {phase.phase_id: phase.name for phase in phases or []}
+ fds_by_id = {fd.field_id:fd for fd in config.field_defs}
+ fids_by_name = {fd.field_name:fd.field_id for fd in config.field_defs}
+ enum_names_by_lower = {
+ fd.field_name.lower(): fd.field_name for fd in config.field_defs
+ if fd.field_type == tracker_pb2.FieldTypes.ENUM_TYPE}
+
+ labels_by_prefix = tracker_bizobj.LabelsByPrefix(
+ labels, list(enum_names_by_lower.keys()))
+ der_labels_by_prefix = tracker_bizobj.LabelsByPrefix(
+ derived_labels, list(enum_names_by_lower.keys()))
+
+ for lower_field_name, values in labels_by_prefix.items():
+ field_name = enum_names_by_lower.get(lower_field_name)
+ if not field_name:
+ continue
+ fvs.extend(
+ [ConvertFieldValue(
+ fids_by_name.get(field_name), field_name, value,
+ tracker_pb2.FieldTypes.ENUM_TYPE)
+ for value in values])
+
+ for lower_field_name, values in der_labels_by_prefix.items():
+ field_name = enum_names_by_lower.get(lower_field_name)
+ if not field_name:
+ continue
+ fvs.extend(
+ [ConvertFieldValue(
+ fids_by_name.get(field_name), field_name, value,
+ tracker_pb2.FieldTypes.ENUM_TYPE, is_derived=True)
+ for value in values])
+
+ for fv in field_values:
+ field_def = fds_by_id.get(fv.field_id)
+ if not field_def:
+ logging.info(
+ 'Ignoring field value referencing a non-existent field: %r', fv)
+ continue
+
+ value = tracker_bizobj.GetFieldValue(fv, users_by_id)
+ field_name = field_def.field_name
+ field_type = field_def.field_type
+ approval_name = None
+
+ if field_def.approval_id is not None:
+ approval_def = fds_by_id.get(field_def.approval_id)
+ if approval_def:
+ approval_name = approval_def.field_name
+
+ fvs.append(ConvertFieldValue(
+ fv.field_id, field_name, value, field_type, approval_name=approval_name,
+ phase_name=phase_names_by_id.get(fv.phase_id), is_derived=fv.derived))
+
+ return fvs
+
+
+def ConvertIssue(issue, users_by_id, related_refs, config):
+ """Convert our protorpc Issue to a protoc Issue.
+
+ Args:
+ issue: protorpc issue used by monorail internally.
+ users_by_id: dict {user_id: UserViews} for all users mentioned in issue.
+ related_refs: dict {issue_id: (project_name, local_id)} of all blocked-on,
+ blocking, or merged-into issues referenced from this issue, regardless
+ of perms.
+ config: ProjectIssueConfig for this issue.
+
+ Returns: A protoc Issue object.
+ """
+ status_ref = ConvertStatusRef(issue.status, issue.derived_status, config)
+ owner_ref = ConvertUserRef(
+ issue.owner_id, issue.derived_owner_id, users_by_id)
+ cc_refs = ConvertUserRefs(
+ issue.cc_ids, issue.derived_cc_ids, users_by_id, False)
+ labels, derived_labels = tracker_bizobj.ExplicitAndDerivedNonMaskedLabels(
+ issue.labels, issue.derived_labels, config)
+ label_refs = ConvertLabels(labels, derived_labels)
+ component_refs = ConvertComponents(
+ issue.component_ids, issue.derived_component_ids, config)
+ blocked_on_issue_refs = ConvertIssueRefs(
+ issue.blocked_on_iids, related_refs)
+ dangling_blocked_on_refs = [
+ ConvertIssueRef((dangling_issue.project, dangling_issue.issue_id),
+ ext_id=dangling_issue.ext_issue_identifier)
+ for dangling_issue in issue.dangling_blocked_on_refs]
+ blocking_issue_refs = ConvertIssueRefs(
+ issue.blocking_iids, related_refs)
+ dangling_blocking_refs = [
+ ConvertIssueRef((dangling_issue.project, dangling_issue.issue_id),
+ ext_id=dangling_issue.ext_issue_identifier)
+ for dangling_issue in issue.dangling_blocking_refs]
+ merged_into_issue_ref = None
+ if issue.merged_into:
+ merged_into_issue_ref = ConvertIssueRef(related_refs[issue.merged_into])
+ if issue.merged_into_external:
+ merged_into_issue_ref = ConvertIssueRef((None, None),
+ ext_id=issue.merged_into_external)
+
+ field_values = ConvertFieldValues(
+ config, issue.labels, issue.derived_labels,
+ issue.field_values, users_by_id, phases=issue.phases)
+ approval_values = ConvertApprovalValues(
+ issue.approval_values, issue.phases, users_by_id, config)
+ reporter_ref = ConvertUserRef(issue.reporter_id, None, users_by_id)
+ phases = [ConvertPhaseDef(phase) for phase in issue.phases]
+ result = issue_objects_pb2.Issue(
+ project_name=issue.project_name, local_id=issue.local_id,
+ summary=issue.summary, status_ref=status_ref, owner_ref=owner_ref,
+ cc_refs=cc_refs, label_refs=label_refs, component_refs=component_refs,
+ blocked_on_issue_refs=blocked_on_issue_refs,
+ dangling_blocked_on_refs=dangling_blocked_on_refs,
+ blocking_issue_refs=blocking_issue_refs,
+ dangling_blocking_refs=dangling_blocking_refs,
+ merged_into_issue_ref=merged_into_issue_ref, field_values=field_values,
+ is_deleted=issue.deleted, reporter_ref=reporter_ref,
+ opened_timestamp=issue.opened_timestamp,
+ closed_timestamp=issue.closed_timestamp,
+ modified_timestamp=issue.modified_timestamp,
+ component_modified_timestamp=issue.component_modified_timestamp,
+ status_modified_timestamp=issue.status_modified_timestamp,
+ owner_modified_timestamp=issue.owner_modified_timestamp,
+ star_count=issue.star_count, is_spam=issue.is_spam,
+ approval_values=approval_values, phases=phases)
+
+ # TODO(crbug.com/monorail/5857): Set attachment_count unconditionally
+ # after the underlying source of negative attachment counts has been
+ # resolved and database has been repaired.
+ if issue.attachment_count >= 0:
+ result.attachment_count = issue.attachment_count
+
+ return result
+
+
+def ConvertPhaseDef(phase):
+ """Convert a protorpc Phase to a protoc PhaseDef."""
+ phase_def = issue_objects_pb2.PhaseDef(
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name=phase.name),
+ rank=phase.rank)
+ return phase_def
+
+
+def ConvertAmendment(amendment, users_by_id):
+ """Convert a protorpc Amendment to a protoc Amendment."""
+ field_name = tracker_bizobj.GetAmendmentFieldName(amendment)
+ new_value = tracker_bizobj.AmendmentString(amendment, users_by_id)
+ result = issue_objects_pb2.Amendment(
+ field_name=field_name, new_or_delta_value=new_value,
+ old_value=amendment.oldvalue)
+ return result
+
+
+def ConvertAttachment(attach, project_name):
+ """Convert a protorpc Attachment to a protoc Attachment."""
+ size, thumbnail_url, view_url, download_url = None, None, None, None
+ if not attach.deleted:
+ size = attach.filesize
+ download_url = attachment_helpers.GetDownloadURL(attach.attachment_id)
+ view_url = attachment_helpers.GetViewURL(attach, download_url, project_name)
+ thumbnail_url = attachment_helpers.GetThumbnailURL(attach, download_url)
+
+ result = issue_objects_pb2.Attachment(
+ attachment_id=attach.attachment_id, filename=attach.filename,
+ size=size, content_type=attach.mimetype,
+ is_deleted=attach.deleted, thumbnail_url=thumbnail_url,
+ view_url=view_url, download_url=download_url)
+ return result
+
+
+def ConvertComment(
+ issue, comment, config, users_by_id, comment_reporters, description_nums,
+ user_id, perms):
+ """Convert a protorpc IssueComment to a protoc Comment."""
+ commenter = users_by_id[comment.user_id]
+
+ can_delete = permissions.CanDeleteComment(
+ comment, commenter, user_id, perms)
+ can_flag, is_flagged = permissions.CanFlagComment(
+ comment, commenter, comment_reporters, user_id, perms)
+ can_view = permissions.CanViewComment(
+ comment, commenter, user_id, perms)
+ can_view_inbound_message = permissions.CanViewInboundMessage(
+ comment, user_id, perms)
+
+ is_deleted = bool(comment.deleted_by or is_flagged or commenter.banned)
+
+ result = issue_objects_pb2.Comment(
+ project_name=issue.project_name,
+ local_id=issue.local_id,
+ sequence_num=comment.sequence,
+ is_deleted=is_deleted,
+ can_delete=can_delete,
+ is_spam=is_flagged,
+ can_flag=can_flag,
+ timestamp=comment.timestamp)
+
+ if can_view:
+ result.commenter.CopyFrom(
+ ConvertUserRef(comment.user_id, None, users_by_id))
+ result.content = comment.content
+ if comment.inbound_message and can_view_inbound_message:
+ result.inbound_message = comment.inbound_message
+ result.amendments.extend([
+ ConvertAmendment(amend, users_by_id)
+ for amend in comment.amendments])
+ result.attachments.extend([
+ ConvertAttachment(attach, issue.project_name)
+ for attach in comment.attachments])
+
+ if comment.id in description_nums:
+ result.description_num = description_nums[comment.id]
+
+ fd = tracker_bizobj.FindFieldDefByID(comment.approval_id, config)
+ if fd:
+ result.approval_ref.field_name = fd.field_name
+
+ return result
+
+
+def ConvertCommentList(
+ issue, comments, config, users_by_id, comment_reporters, user_id, perms):
+ """Convert a list of protorpc IssueComments to protoc Comments."""
+ description_nums = {}
+ for comment in comments:
+ if (comment.is_description and not users_by_id[comment.user_id].banned and
+ not comment.deleted_by and not comment.is_spam):
+ description_nums[comment.id] = len(description_nums) + 1
+
+ result = [
+ ConvertComment(
+ issue, c, config, users_by_id, comment_reporters.get(c.id, []),
+ description_nums, user_id, perms)
+ for c in comments]
+ return result
+
+
+def IngestUserRef(cnxn, user_ref, user_service, autocreate=False):
+ """Return ID of specified user or raise NoSuchUserException."""
+ try:
+ return IngestUserRefs(
+ cnxn, [user_ref], user_service, autocreate=autocreate)[0]
+ except IndexError:
+ # user_ref.display_name was not a valid email.
+ raise exceptions.NoSuchUserException
+
+
+def IngestUserRefs(cnxn, user_refs, user_service, autocreate=False):
+ """Return IDs of specified users or raise NoSuchUserException."""
+
+ # Filter out user_refs with invalid display_names.
+ # Invalid emails won't get auto-created in LookupUserIds, but un-specified
+ # user_ref.user_id values have the zero-value 0. So invalid user_ref's
+ # need to be filtered out here to prevent these resulting in '0's in
+ # the 'result' array.
+ if autocreate:
+ user_refs = [user_ref for user_ref in user_refs
+ if (not user_ref.display_name) or
+ validate.IsValidEmail(user_ref.display_name)]
+
+ # 1. Verify that all specified user IDs actually match existing users.
+ user_ids_to_verify = [user_ref.user_id for user_ref in user_refs
+ if user_ref.user_id]
+ user_service.LookupUserEmails(cnxn, user_ids_to_verify)
+
+ # 2. Lookup or create any users that are specified by email address.
+ needed_emails = [user_ref.display_name for user_ref in user_refs
+ if not user_ref.user_id and user_ref.display_name]
+ emails_to_ids = user_service.LookupUserIDs(
+ cnxn, needed_emails, autocreate=autocreate)
+
+ # 3. Build the result from emails_to_ids or straight from user_ref's
+ # user_id.
+ # Note: user_id can be specified as 0 to clear the issue owner.
+ result = [
+ emails_to_ids.get(user_ref.display_name.lower(), user_ref.user_id)
+ for user_ref in user_refs
+ ]
+ return result
+
+
+def IngestPrefValues(pref_values):
+ """Return protorpc UserPrefValues for the given values."""
+ return [user_pb2.UserPrefValue(name=upv.name, value=upv.value)
+ for upv in pref_values]
+
+
+def IngestComponentRefs(comp_refs, config, ignore_missing_objects=False):
+ """Return IDs of specified components or raise NoSuchComponentException."""
+ cids_by_path = {cd.path.lower(): cd.component_id
+ for cd in config.component_defs}
+ result = []
+ for comp_ref in comp_refs:
+ cid = cids_by_path.get(comp_ref.path.lower())
+ if cid:
+ result.append(cid)
+ else:
+ if not ignore_missing_objects:
+ raise exceptions.NoSuchComponentException()
+ return result
+
+
+def IngestFieldRefs(field_refs, config):
+ """Return IDs of specified fields or raise NoSuchFieldDefException."""
+ fids_by_name = {fd.field_name.lower(): fd.field_id
+ for fd in config.field_defs}
+ result = []
+ for field_ref in field_refs:
+ fid = fids_by_name.get(field_ref.field_name.lower())
+ if fid:
+ result.append(fid)
+ else:
+ raise exceptions.NoSuchFieldDefException()
+ return result
+
+
+def IngestIssueRefs(cnxn, issue_refs, services):
+ """Look up issue IDs for the specified issues."""
+ project_names = set(ref.project_name for ref in issue_refs)
+ project_names_to_id = services.project.LookupProjectIDs(cnxn, project_names)
+ project_local_id_pairs = []
+ for ref in issue_refs:
+ if ref.ext_identifier:
+ # TODO(jeffcarp): For external tracker refs, once we have the classes
+ # set up, validate that the tracker for this specific ref is supported
+ # and store the external ref in the issue properly.
+ if '/' not in ref.ext_identifier:
+ raise exceptions.InvalidExternalIssueReference()
+ continue
+ if ref.project_name in project_names_to_id:
+ pair = (project_names_to_id[ref.project_name], ref.local_id)
+ project_local_id_pairs.append(pair)
+ else:
+ raise exceptions.NoSuchProjectException()
+ issue_ids, misses = services.issue.LookupIssueIDs(
+ cnxn, project_local_id_pairs)
+ if misses:
+ raise exceptions.NoSuchIssueException()
+ return issue_ids
+
+
+def IngestExtIssueRefs(issue_refs):
+ """Validate and return external issue refs."""
+ return [
+ ref.ext_identifier
+ for ref in issue_refs
+ if ref.ext_identifier
+ and federated.IsShortlinkValid(ref.ext_identifier)]
+
+
+def IngestIssueDelta(
+ cnxn, services, delta, config, phases, ignore_missing_objects=False):
+ """Ingest a protoc IssueDelta and create a protorpc IssueDelta."""
+ status = None
+ if delta.HasField('status'):
+ status = delta.status.value
+ owner_id = None
+ if delta.HasField('owner_ref'):
+ try:
+ owner_id = IngestUserRef(cnxn, delta.owner_ref, services.user)
+ except exceptions.NoSuchUserException as e:
+ if not ignore_missing_objects:
+ raise e
+ summary = None
+ if delta.HasField('summary'):
+ summary = delta.summary.value
+
+ cc_ids_add = IngestUserRefs(
+ cnxn, delta.cc_refs_add, services.user, autocreate=True)
+ cc_ids_remove = IngestUserRefs(cnxn, delta.cc_refs_remove, services.user)
+
+ comp_ids_add = IngestComponentRefs(
+ delta.comp_refs_add, config,
+ ignore_missing_objects=ignore_missing_objects)
+ comp_ids_remove = IngestComponentRefs(
+ delta.comp_refs_remove, config,
+ ignore_missing_objects=ignore_missing_objects)
+
+ labels_add = [lab_ref.label for lab_ref in delta.label_refs_add]
+ labels_remove = [lab_ref.label for lab_ref in delta.label_refs_remove]
+
+ field_vals_add, field_vals_remove = _RedistributeEnumFieldsIntoLabels(
+ labels_add, labels_remove,
+ delta.field_vals_add, delta.field_vals_remove,
+ config)
+
+ field_vals_add = IngestFieldValues(
+ cnxn, services.user, field_vals_add, config, phases=phases)
+ field_vals_remove = IngestFieldValues(
+ cnxn, services.user, field_vals_remove, config, phases=phases)
+ fields_clear = IngestFieldRefs(delta.fields_clear, config)
+
+ # Ingest intra-tracker issue refs.
+ blocked_on_add = IngestIssueRefs(
+ cnxn, delta.blocked_on_refs_add, services)
+ blocked_on_remove = IngestIssueRefs(
+ cnxn, delta.blocked_on_refs_remove, services)
+ blocking_add = IngestIssueRefs(
+ cnxn, delta.blocking_refs_add, services)
+ blocking_remove = IngestIssueRefs(
+ cnxn, delta.blocking_refs_remove, services)
+
+ # Ingest inter-tracker issue refs.
+ ext_blocked_on_add = IngestExtIssueRefs(delta.blocked_on_refs_add)
+ ext_blocked_on_remove = IngestExtIssueRefs(delta.blocked_on_refs_remove)
+ ext_blocking_add = IngestExtIssueRefs(delta.blocking_refs_add)
+ ext_blocking_remove = IngestExtIssueRefs(delta.blocking_refs_remove)
+
+ merged_into = None
+ merged_into_external = None
+ if delta.HasField('merged_into_ref'):
+ if delta.merged_into_ref.ext_identifier: # Adding an external merged.
+ merged_into_external = delta.merged_into_ref.ext_identifier
+ elif not delta.merged_into_ref.local_id: # Clearing an internal merged.
+ merged_into = 0
+ else: # Adding an internal merged.
+ merged_into = IngestIssueRefs(cnxn, [delta.merged_into_ref], services)[0]
+
+ result = tracker_bizobj.MakeIssueDelta(
+ status, owner_id, cc_ids_add, cc_ids_remove, comp_ids_add,
+ comp_ids_remove, labels_add, labels_remove, field_vals_add,
+ field_vals_remove, fields_clear, blocked_on_add, blocked_on_remove,
+ blocking_add, blocking_remove, merged_into, summary,
+ ext_blocked_on_add=ext_blocked_on_add,
+ ext_blocked_on_remove=ext_blocked_on_remove,
+ ext_blocking_add=ext_blocking_add,
+ ext_blocking_remove=ext_blocking_remove,
+ merged_into_external=merged_into_external)
+ return result
+
+def IngestAttachmentUploads(attachment_uploads):
+ """Ingest protoc AttachmentUpload objects as tuples."""
+ result = []
+ for up in attachment_uploads:
+ if not up.filename:
+ raise exceptions.InputException('Missing attachment name')
+ if not up.content:
+ raise exceptions.InputException('Missing attachment content')
+ mimetype = filecontent.GuessContentTypeFromFilename(up.filename)
+ attachment_tuple = (up.filename, up.content, mimetype)
+ result.append(attachment_tuple)
+ return result
+
+
+def IngestApprovalDelta(cnxn, user_service, approval_delta, setter_id, config):
+ """Ingest a protoc ApprovalDelta and create a protorpc ApprovalDelta."""
+ fids_by_name = {fd.field_name.lower(): fd.field_id for
+ fd in config.field_defs}
+
+ approver_ids_add = IngestUserRefs(
+ cnxn, approval_delta.approver_refs_add, user_service, autocreate=True)
+ approver_ids_remove = IngestUserRefs(
+ cnxn, approval_delta.approver_refs_remove, user_service, autocreate=True)
+
+ labels_add, labels_remove = [], []
+ # TODO(jojwang): monorail:4673, validate enum values all belong to approval.
+ field_vals_add, field_vals_remove = _RedistributeEnumFieldsIntoLabels(
+ labels_add, labels_remove,
+ approval_delta.field_vals_add, approval_delta.field_vals_remove,
+ config)
+
+ sub_fvs_add = IngestFieldValues(cnxn, user_service, field_vals_add, config)
+ sub_fvs_remove = IngestFieldValues(
+ cnxn, user_service, field_vals_remove, config)
+ sub_fields_clear = [fids_by_name.get(clear.field_name.lower()) for
+ clear in approval_delta.fields_clear
+ if clear.field_name.lower() in fids_by_name]
+
+ # protoc ENUMs default to the zero value (in this case: NOT_SET).
+ # NOT_SET should only be allowed when an issue is first created.
+ # Once a user changes it to something else, no one should be allowed
+ # to set it back.
+ status = None
+ if approval_delta.status != issue_objects_pb2.NOT_SET:
+ status = IngestApprovalStatus(approval_delta.status)
+
+ return tracker_bizobj.MakeApprovalDelta(
+ status, setter_id, approver_ids_add, approver_ids_remove,
+ sub_fvs_add, sub_fvs_remove, sub_fields_clear, labels_add, labels_remove)
+
+
+def IngestApprovalStatus(approval_status):
+ """Ingest a protoc ApprovalStatus and create a protorpc ApprovalStatus. """
+ if approval_status == issue_objects_pb2.NOT_SET:
+ return tracker_pb2.ApprovalStatus.NOT_SET
+ return tracker_pb2.ApprovalStatus(approval_status)
+
+
+def IngestFieldValues(cnxn, user_service, field_values, config, phases=None):
+ """Ingest a list of protoc FieldValues and create protorpc FieldValues.
+
+ Args:
+ cnxn: connection to the DB.
+ user_service: interface to user data storage.
+ field_values: a list of protoc FieldValue used by the API.
+ config: ProjectIssueConfig for this field_value's project.
+ phases: a list of the issue's protorpc Phases.
+
+
+ Returns: A protorpc FieldValue object.
+ """
+ fds_by_name = {fd.field_name.lower(): fd for fd in config.field_defs}
+ phases_by_name = {phase.name: phase.phase_id for phase in phases or []}
+
+ ingested_fvs = []
+ for fv in field_values:
+ fd = fds_by_name.get(fv.field_ref.field_name.lower())
+ if fd:
+ if not fv.value:
+ logging.info('Ignoring blank field value: %r', fv)
+ continue
+ ingested_fv = field_helpers.ParseOneFieldValue(
+ cnxn, user_service, fd, fv.value)
+ if not ingested_fv:
+ raise exceptions.InputException(
+ 'Unparsable value for field %s' % fv.field_ref.field_name)
+ if ingested_fv.user_id == field_helpers.INVALID_USER_ID:
+ raise exceptions.NoSuchUserException()
+ if fd.is_phase_field:
+ ingested_fv.phase_id = phases_by_name.get(fv.phase_ref.phase_name)
+ ingested_fvs.append(ingested_fv)
+
+ return ingested_fvs
+
+
+def IngestSavedQueries(cnxn, project_service, saved_queries):
+ """Ingest a list of protoc SavedQuery and create protorpc SavedQuery.
+
+ Args:
+ cnxn: connection to the DB.
+ project_service: interface to project data storage.
+ saved_queries: a list of protoc Savedquery.
+
+ Returns: A protorpc SavedQuery object.
+ """
+ if not saved_queries:
+ return []
+
+ project_ids = set()
+ for sq in saved_queries:
+ project_ids.update(sq.executes_in_project_ids)
+
+ project_name_dict = project_service.LookupProjectNames(cnxn,
+ project_ids)
+ return [
+ common_pb2.SavedQuery(
+ query_id=sq.query_id,
+ name=sq.name,
+ query=sq.query,
+ project_names=[project_name_dict[project_id]
+ for project_id in sq.executes_in_project_ids]
+ )
+ for sq in saved_queries]
+
+
+def IngestHotlistRefs(cnxn, user_service, features_service, hotlist_refs):
+ return [IngestHotlistRef(cnxn, user_service, features_service, hotlist_ref)
+ for hotlist_ref in hotlist_refs]
+
+
+def IngestHotlistRef(cnxn, user_service, features_service, hotlist_ref):
+ hotlist_id = None
+
+ if hotlist_ref.hotlist_id:
+ # If a hotlist ID was specified, verify it actually match existing hotlists.
+ features_service.GetHotlist(cnxn, hotlist_ref.hotlist_id)
+ hotlist_id = hotlist_ref.hotlist_id
+
+ if hotlist_ref.name and hotlist_ref.owner:
+ name = hotlist_ref.name
+ owner_id = IngestUserRef(cnxn, hotlist_ref.owner, user_service)
+ hotlists = features_service.LookupHotlistIDs(cnxn, [name], [owner_id])
+ # Verify there is a hotlist with that name and owner.
+ if (name.lower(), owner_id) not in hotlists:
+ raise features_svc.NoSuchHotlistException()
+ found_id = hotlists[name.lower(), owner_id]
+ # If a hotlist_id was also given, verify it correspond to the name and
+ # owner.
+ if hotlist_id is not None and found_id != hotlist_id:
+ raise features_svc.NoSuchHotlistException()
+ hotlist_id = found_id
+
+ # Neither an ID, nor a name-owner ref were given.
+ if hotlist_id is None:
+ raise features_svc.NoSuchHotlistException()
+
+ return hotlist_id
+
+
+def IngestPagination(pagination):
+ max_items = settings.max_artifact_search_results_per_page
+ if pagination.max_items:
+ max_items = min(max_items, pagination.max_items)
+ return pagination.start, max_items
+
+# Convert and ingest objects in project_objects.proto.
+
+def ConvertStatusDef(status_def):
+ """Convert a protorpc StatusDef into a protoc StatusDef."""
+ result = project_objects_pb2.StatusDef(
+ status=status_def.status,
+ means_open=status_def.means_open,
+ docstring=status_def.status_docstring,
+ deprecated=status_def.deprecated)
+ return result
+
+
+def ConvertLabelDef(label_def):
+ """Convert a protorpc LabelDef into a protoc LabelDef."""
+ result = project_objects_pb2.LabelDef(
+ label=label_def.label,
+ docstring=label_def.label_docstring,
+ deprecated=label_def.deprecated)
+ return result
+
+
+def ConvertComponentDef(
+ component_def, users_by_id, labels_by_id, include_admin_info):
+ """Convert a protorpc ComponentDef into a protoc ComponentDef."""
+ if not include_admin_info:
+ return project_objects_pb2.ComponentDef(
+ path=component_def.path,
+ docstring=component_def.docstring,
+ deprecated=component_def.deprecated)
+
+ admin_refs = ConvertUserRefs(component_def.admin_ids, [], users_by_id, False)
+ cc_refs = ConvertUserRefs(component_def.cc_ids, [], users_by_id, False)
+ labels = [labels_by_id[lid] for lid in component_def.label_ids]
+ label_refs = ConvertLabels(labels, [])
+ creator_ref = ConvertUserRef(component_def.creator_id, None, users_by_id)
+ modifier_ref = ConvertUserRef(component_def.modifier_id, None, users_by_id)
+ return project_objects_pb2.ComponentDef(
+ path=component_def.path,
+ docstring=component_def.docstring,
+ admin_refs=admin_refs,
+ cc_refs=cc_refs,
+ deprecated=component_def.deprecated,
+ created=component_def.created,
+ creator_ref=creator_ref,
+ modified=component_def.modified,
+ modifier_ref=modifier_ref,
+ label_refs=label_refs)
+
+
+def ConvertFieldDef(field_def, user_choices, users_by_id, config,
+ include_admin_info):
+ """Convert a protorpc FieldDef into a protoc FieldDef."""
+ parent_approval_name = None
+ if field_def.approval_id:
+ parent_fd = tracker_bizobj.FindFieldDefByID(field_def.approval_id, config)
+ if parent_fd:
+ parent_approval_name = parent_fd.field_name
+ field_ref = ConvertFieldRef(
+ field_def.field_id, field_def.field_name, field_def.field_type,
+ parent_approval_name)
+
+ enum_choices = []
+ if field_def.field_type == tracker_pb2.FieldTypes.ENUM_TYPE:
+ masked_labels = tracker_helpers.LabelsMaskedByFields(
+ config, [field_def.field_name], True)
+ enum_choices = [
+ project_objects_pb2.LabelDef(
+ label=label.name,
+ docstring=label.docstring,
+ deprecated=(label.commented == '#'))
+ for label in masked_labels]
+
+ if not include_admin_info:
+ return project_objects_pb2.FieldDef(
+ field_ref=field_ref,
+ docstring=field_def.docstring,
+ # Display full email address for user choices.
+ user_choices=ConvertUserRefs(user_choices, [], users_by_id, True),
+ enum_choices=enum_choices)
+
+ admin_refs = ConvertUserRefs(field_def.admin_ids, [], users_by_id, False)
+ # TODO(jrobbins): validation, permission granting, and notification options.
+
+ return project_objects_pb2.FieldDef(
+ field_ref=field_ref,
+ applicable_type=field_def.applicable_type,
+ is_required=field_def.is_required,
+ is_niche=field_def.is_niche,
+ is_multivalued=field_def.is_multivalued,
+ docstring=field_def.docstring,
+ admin_refs=admin_refs,
+ is_phase_field=field_def.is_phase_field,
+ enum_choices=enum_choices)
+
+
+def ConvertApprovalDef(approval_def, users_by_id, config, include_admin_info):
+ """Convert a protorpc ApprovalDef into a protoc ApprovalDef."""
+ field_def = tracker_bizobj.FindFieldDefByID(approval_def.approval_id, config)
+ field_ref = ConvertFieldRef(field_def.field_id, field_def.field_name,
+ field_def.field_type, None)
+ if not include_admin_info:
+ return project_objects_pb2.ApprovalDef(field_ref=field_ref)
+
+ approver_refs = ConvertUserRefs(approval_def.approver_ids, [], users_by_id,
+ False)
+ return project_objects_pb2.ApprovalDef(
+ field_ref=field_ref,
+ approver_refs=approver_refs,
+ survey=approval_def.survey)
+
+
+def ConvertConfig(
+ project, config, users_by_id, labels_by_id):
+ """Convert a protorpc ProjectIssueConfig into a protoc Config."""
+ status_defs = [
+ ConvertStatusDef(sd)
+ for sd in config.well_known_statuses]
+ statuses_offer_merge = [
+ ConvertStatusRef(sd.status, None, config)
+ for sd in config.well_known_statuses
+ if sd.status in config.statuses_offer_merge]
+ label_defs = [
+ ConvertLabelDef(ld)
+ for ld in config.well_known_labels]
+ component_defs = [
+ ConvertComponentDef(
+ cd, users_by_id, labels_by_id, True)
+ for cd in config.component_defs]
+ field_defs = [
+ ConvertFieldDef(fd, [], users_by_id, config, True)
+ for fd in config.field_defs
+ if not fd.is_deleted]
+ approval_defs = [
+ ConvertApprovalDef(ad, users_by_id, config, True)
+ for ad in config.approval_defs]
+ result = project_objects_pb2.Config(
+ project_name=project.project_name,
+ status_defs=status_defs,
+ statuses_offer_merge=statuses_offer_merge,
+ label_defs=label_defs,
+ exclusive_label_prefixes=config.exclusive_label_prefixes,
+ component_defs=component_defs,
+ field_defs=field_defs,
+ approval_defs=approval_defs,
+ restrict_to_known=config.restrict_to_known)
+ return result
+
+
+def ConvertProjectTemplateDefs(templates, users_by_id, config):
+ """Convert a project's protorpc TemplateDefs into protoc TemplateDefs."""
+ converted_templates = []
+ for template in templates:
+ owner_ref = ConvertUserRef(template.owner_id, None, users_by_id)
+ status_ref = ConvertStatusRef(template.status, None, config)
+ labels, _derived_labels = tracker_bizobj.ExplicitAndDerivedNonMaskedLabels(
+ template.labels, [], config)
+ label_refs = ConvertLabels(labels, [])
+ admin_refs = ConvertUserRefs(template.admin_ids, [], users_by_id, False)
+ field_values = ConvertFieldValues(
+ config, template.labels, [], template.field_values, users_by_id,
+ phases=template.phases)
+ component_refs = ConvertComponents(template.component_ids, [], config)
+ approval_values = ConvertApprovalValues(
+ template.approval_values, template.phases, users_by_id, config)
+ phases = [ConvertPhaseDef(phase) for phase in template.phases]
+
+ converted_templates.append(
+ project_objects_pb2.TemplateDef(
+ template_name=template.name, content=template.content,
+ summary=template.summary,
+ summary_must_be_edited=template.summary_must_be_edited,
+ owner_ref=owner_ref, status_ref=status_ref, label_refs=label_refs,
+ members_only=template.members_only,
+ owner_defaults_to_member=template.owner_defaults_to_member,
+ admin_refs=admin_refs, field_values=field_values,
+ component_refs=component_refs,
+ component_required=template.component_required,
+ approval_values=approval_values, phases=phases)
+ )
+ return converted_templates
+
+
+def ConvertHotlist(hotlist, users_by_id):
+ """Convert a protorpc Hotlist into a protoc Hotlist."""
+ owner_ref = ConvertUserRef(
+ hotlist.owner_ids[0], None, users_by_id)
+ editor_refs = ConvertUserRefs(hotlist.editor_ids, [], users_by_id, False)
+ follower_refs = ConvertUserRefs(
+ hotlist.follower_ids, [], users_by_id, False)
+ result = features_objects_pb2.Hotlist(
+ owner_ref=owner_ref,
+ editor_refs=editor_refs,
+ follower_refs=follower_refs,
+ name=hotlist.name,
+ summary=hotlist.summary,
+ description=hotlist.description,
+ default_col_spec=hotlist.default_col_spec,
+ is_private=hotlist.is_private,
+ )
+ return result
+
+
+def ConvertHotlistItems(hotlist_items, issues_by_id, users_by_id, related_refs,
+ harmonized_config):
+ # Note: hotlist_items are not always sorted by 'rank'
+ sorted_ranks = sorted(item.rank for item in hotlist_items)
+ friendly_ranks_dict = {
+ rank: friendly_rank for friendly_rank, rank in
+ enumerate(sorted_ranks, 1)}
+ converted_items = []
+ for item in hotlist_items:
+ issue_pb = issues_by_id[item.issue_id]
+ issue = ConvertIssue(
+ issue_pb, users_by_id, related_refs, harmonized_config)
+ adder_ref = ConvertUserRef(item.adder_id, None, users_by_id)
+ converted_items.append(features_objects_pb2.HotlistItem(
+ issue=issue,
+ rank=friendly_ranks_dict[item.rank],
+ adder_ref=adder_ref,
+ added_timestamp=item.date_added,
+ note=item.note))
+ return converted_items
+
+
+def ConvertValueAndWhy(value_and_why):
+ return common_pb2.ValueAndWhy(
+ value=value_and_why.get('value'),
+ why=value_and_why.get('why'))
+
+
+def ConvertValueAndWhyList(value_and_why_list):
+ return [ConvertValueAndWhy(vnw) for vnw in value_and_why_list]
+
+
+def _RedistributeEnumFieldsIntoLabels(
+ labels_add, labels_remove, field_vals_add, field_vals_remove, config):
+ """Look at the custom field values and treat enum fields as labels.
+
+ Args:
+ labels_add: list of labels to add/set on the issue.
+ labels_remove: list of labels to remove from the issue.
+ field_val_add: list of protoc FieldValues to be added.
+ field_val_remove: list of protoc FieldValues to be removed.
+ remove.
+ config: ProjectIssueConfig PB including custom field definitions.
+
+ Returns:
+ Two revised lists of protoc FieldValues to be added and removed,
+ without enum_types.
+
+ SIDE-EFFECT: the labels and labels_remove lists will be extended with
+ key-value labels corresponding to the enum field values.
+ """
+ field_val_strs_add = {}
+ for field_val in field_vals_add:
+ field_val_strs_add.setdefault(field_val.field_ref.field_id, []).append(
+ field_val.value)
+
+ field_val_strs_remove = {}
+ for field_val in field_vals_remove:
+ field_val_strs_remove.setdefault(field_val.field_ref.field_id, []).append(
+ field_val.value)
+
+ field_helpers.ShiftEnumFieldsIntoLabels(
+ labels_add, labels_remove, field_val_strs_add, field_val_strs_remove,
+ config)
+
+ # Filter out the fields that were shifted into labels
+ updated_field_vals_add = [
+ fv for fv in field_vals_add
+ if fv.field_ref.field_id in field_val_strs_add]
+ updated_field_vals_remove = [
+ fv for fv in field_vals_remove
+ if fv.field_ref.field_id in field_val_strs_remove]
+
+ return updated_field_vals_add, updated_field_vals_remove
diff --git a/api/features_servicer.py b/api/features_servicer.py
new file mode 100644
index 0000000..2ae49c5
--- /dev/null
+++ b/api/features_servicer.py
@@ -0,0 +1,323 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+
+from api import monorail_servicer
+from api import converters
+from api.api_proto import common_pb2
+from api.api_proto import features_pb2
+from api.api_proto import features_prpc_pb2
+from businesslogic import work_env
+from features import component_helpers
+from features import features_bizobj
+from framework import exceptions
+from framework import framework_bizobj
+from framework import framework_views
+from framework import paginate
+from framework import permissions
+from services import features_svc
+from tracker import tracker_bizobj
+
+
+class FeaturesServicer(monorail_servicer.MonorailServicer):
+ """Handle API requests related to Features objects.
+
+ Each API request is implemented with a method as defined in the .proto
+ file that does any request-specific validation, uses work_env to
+ safely operate on business objects, and returns a response proto.
+ """
+
+ DESCRIPTION = features_prpc_pb2.FeaturesServiceDescription
+
+ @monorail_servicer.PRPCMethod
+ def ListHotlistsByUser(self, mc, request):
+ """Return the hotlists for the given user."""
+ user_id = converters.IngestUserRef(
+ mc.cnxn, request.user, self.services.user)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ mc.LookupLoggedInUserPerms(None)
+ hotlists = we.ListHotlistsByUser(user_id)
+
+ with mc.profiler.Phase('making user views'):
+ users_involved = features_bizobj.UsersInvolvedInHotlists(hotlists)
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, users_involved)
+ framework_views.RevealAllEmailsToMembers(
+ mc.cnxn, self.services, mc.auth, users_by_id)
+
+ converted_hotlists = [
+ converters.ConvertHotlist(hotlist, users_by_id)
+ for hotlist in hotlists]
+
+ result = features_pb2.ListHotlistsByUserResponse(
+ hotlists=converted_hotlists)
+
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def ListHotlistsByIssue(self, mc, request):
+ # type: (MonorailConnection, proto.features.ListHotlistsByIssueRequest) ->
+ # proto.features.ListHotlistsByIssueResponse
+ """Return the hotlists the given issue is part of."""
+ issue_id = converters.IngestIssueRefs(
+ mc.cnxn, [request.issue], self.services)[0]
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ project = we.GetProjectByName(request.issue.project_name)
+ mc.LookupLoggedInUserPerms(project)
+ hotlists = we.ListHotlistsByIssue(issue_id)
+
+ # Reduce spam by only showing hotlists that belong to a project member.
+ project_hotlists = [
+ hotlist for hotlist in hotlists if framework_bizobj.UserIsInProject(
+ project, set(hotlist.owner_ids + hotlist.editor_ids)) or
+ features_bizobj.UserIsInHotlist(hotlist, mc.auth.effective_ids)
+ ]
+ if project_hotlists and len(hotlists) / len(project_hotlists) > 10:
+ logging.warning(
+ 'Unusual hotlist activity in %s:%s' %
+ (request.issue.project_name, issue_id))
+
+ with mc.profiler.Phase('making user views'):
+ users_involved = features_bizobj.UsersInvolvedInHotlists(project_hotlists)
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, users_involved)
+ framework_views.RevealAllEmailsToMembers(
+ mc.cnxn, self.services, mc.auth, users_by_id)
+
+ converted_hotlists = [
+ converters.ConvertHotlist(hotlist, users_by_id)
+ for hotlist in project_hotlists
+ ]
+
+ result = features_pb2.ListHotlistsByIssueResponse(
+ hotlists=converted_hotlists)
+
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def ListRecentlyVisitedHotlists(self, mc, _request):
+ """Return the recently visited hotlists for the logged in user."""
+ with work_env.WorkEnv(mc, self.services) as we:
+ mc.LookupLoggedInUserPerms(None)
+ hotlists = we.ListRecentlyVisitedHotlists()
+
+ with mc.profiler.Phase('making user views'):
+ users_involved = features_bizobj.UsersInvolvedInHotlists(hotlists)
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, users_involved)
+ framework_views.RevealAllEmailsToMembers(
+ mc.cnxn, self.services, mc.auth, users_by_id)
+
+ converted_hotlists = [
+ converters.ConvertHotlist(hotlist, users_by_id)
+ for hotlist in hotlists]
+
+ result = features_pb2.ListRecentlyVisitedHotlistsResponse(
+ hotlists=converted_hotlists)
+
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def ListStarredHotlists(self, mc, _request):
+ """Return the starred hotlists for the logged in user."""
+ with work_env.WorkEnv(mc, self.services) as we:
+ mc.LookupLoggedInUserPerms(None)
+ hotlists = we.ListStarredHotlists()
+
+ with mc.profiler.Phase('maknig user views'):
+ users_involved = features_bizobj.UsersInvolvedInHotlists(hotlists)
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, users_involved)
+ framework_views.RevealAllEmailsToMembers(
+ mc.cnxn, self.services, mc.auth, users_by_id)
+
+ converted_hotlists = [
+ converters.ConvertHotlist(hotlist, users_by_id)
+ for hotlist in hotlists]
+
+ result = features_pb2.ListStarredHotlistsResponse(
+ hotlists=converted_hotlists)
+
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def GetHotlistStarCount(self, mc, request):
+ """Get the star count for the specified hotlist."""
+ hotlist_id = converters.IngestHotlistRef(
+ mc.cnxn, self.services.user, self.services.features,
+ request.hotlist_ref)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ mc.LookupLoggedInUserPerms(None)
+ star_count = we.GetHotlistStarCount(hotlist_id)
+
+ result = features_pb2.GetHotlistStarCountResponse(star_count=star_count)
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def StarHotlist(self, mc, request):
+ """Star the specified hotlist."""
+ hotlist_id = converters.IngestHotlistRef(
+ mc.cnxn, self.services.user, self.services.features,
+ request.hotlist_ref)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ mc.LookupLoggedInUserPerms(None)
+ we.StarHotlist(hotlist_id, request.starred)
+ star_count = we.GetHotlistStarCount(hotlist_id)
+
+ result = features_pb2.StarHotlistResponse(star_count=star_count)
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def GetHotlist(self, mc, request):
+ """Get the Hotlist metadata for the specified hotlist."""
+ hotlist_id = converters.IngestHotlistRef(
+ mc.cnxn, self.services.user, self.services.features,
+ request.hotlist_ref)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ mc.LookupLoggedInUserPerms(None)
+ hotlist = we.GetHotlist(hotlist_id)
+
+ with mc.profiler.Phase('making user views'):
+ users_involved = features_bizobj.UsersInvolvedInHotlists([hotlist])
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, users_involved)
+ framework_views.RevealAllEmailsToMembers(
+ mc.cnxn, self.services, mc.auth, users_by_id)
+
+ converted_hotlist = converters.ConvertHotlist(hotlist, users_by_id)
+ return features_pb2.GetHotlistResponse(hotlist=converted_hotlist)
+
+ @monorail_servicer.PRPCMethod
+ def CreateHotlist(self, mc, request):
+ """Create a new hotlist."""
+ editor_ids = converters.IngestUserRefs(
+ mc.cnxn, request.editor_refs, self.services.user)
+ issue_ids = converters.IngestIssueRefs(
+ mc.cnxn, request.issue_refs, self.services)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.CreateHotlist(
+ request.name, request.summary, request.description, editor_ids,
+ issue_ids, request.is_private, '')
+
+ result = features_pb2.CreateHotlistResponse()
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def CheckHotlistName(self, mc, request):
+ """Check that a hotlist name is valid and not already in use."""
+ with work_env.WorkEnv(mc, self.services) as we:
+ error = we.CheckHotlistName(request.name)
+ result = features_pb2.CheckHotlistNameResponse(error=error)
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def RemoveIssuesFromHotlists(self, mc, request):
+ """Remove the given issues from the given hotlists."""
+ hotlist_ids = converters.IngestHotlistRefs(
+ mc.cnxn, self.services.user, self.services.features,
+ request.hotlist_refs)
+ issue_ids = converters.IngestIssueRefs(
+ mc.cnxn, request.issue_refs, self.services)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ mc.LookupLoggedInUserPerms(None)
+ we.RemoveIssuesFromHotlists(hotlist_ids, issue_ids)
+
+ result = features_pb2.RemoveIssuesFromHotlistsResponse()
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def AddIssuesToHotlists(self, mc, request):
+ """Add the given issues to the given hotlists."""
+ hotlist_ids = converters.IngestHotlistRefs(
+ mc.cnxn, self.services.user, self.services.features,
+ request.hotlist_refs)
+ issue_ids = converters.IngestIssueRefs(
+ mc.cnxn, request.issue_refs, self.services)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ mc.LookupLoggedInUserPerms(None)
+ we.AddIssuesToHotlists(hotlist_ids, issue_ids, request.note)
+
+ result = features_pb2.AddIssuesToHotlistsResponse()
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def RerankHotlistIssues(self, mc, request):
+ """Rerank issues in the given hotlist."""
+ hotlist_id = converters.IngestHotlistRef(
+ mc.cnxn, self.services.user, self.services.features,
+ request.hotlist_ref)
+ moved_issue_ids = converters.IngestIssueRefs(
+ mc.cnxn, request.moved_refs, self.services)
+ [target_issue_id] = converters.IngestIssueRefs(
+ mc.cnxn, [request.target_ref], self.services)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.RerankHotlistIssues(
+ hotlist_id, moved_issue_ids, target_issue_id, request.split_above)
+
+ # TODO(jojwang): return updated hotlist items.
+ with mc.profiler.Phase('converting to response objects'):
+ result = features_pb2.RerankHotlistIssuesResponse()
+
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def UpdateHotlistIssueNote(self, mc, request):
+ """Update the note for the given issue in the given hotlist."""
+ hotlist_id = converters.IngestHotlistRef(
+ mc.cnxn, self.services.user, self.services.features,
+ request.hotlist_ref)
+ issue_id = converters.IngestIssueRefs(
+ mc.cnxn, [request.issue_ref], self.services)[0]
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ project = we.GetProjectByName(request.issue_ref.project_name)
+ mc.LookupLoggedInUserPerms(project)
+ we.UpdateHotlistIssueNote(hotlist_id, issue_id, request.note)
+
+ result = features_pb2.UpdateHotlistIssueNoteResponse()
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def DeleteHotlist(self, mc, request):
+ """Delete the given hotlist"""
+ hotlist_id = converters.IngestHotlistRef(
+ mc.cnxn, self.services.user, self.services.features,
+ request.hotlist_ref)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.DeleteHotlist(hotlist_id)
+
+ return features_pb2.DeleteHotlistResponse()
+
+ # TODO(https://crbug.com/monorail/7515): Replace or delete PredictComponent.
+ @monorail_servicer.PRPCMethod
+ def PredictComponent(self, mc, request):
+ """Predict the component of an issue based on the given text."""
+ with work_env.WorkEnv(mc, self.services) as we:
+ project = we.GetProjectByName(request.project_name)
+ config = we.GetProjectConfig(project.project_id)
+
+ component_ref = None
+ component_id = component_helpers.PredictComponent(request.text, config)
+
+ if component_id:
+ component_ref = converters.ConvertComponentRef(component_id, config)
+
+ result = features_pb2.PredictComponentResponse(component_ref=component_ref)
+ return result
diff --git a/api/issues_servicer.py b/api/issues_servicer.py
new file mode 100644
index 0000000..1cdfeca
--- /dev/null
+++ b/api/issues_servicer.py
@@ -0,0 +1,801 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import copy
+import logging
+
+from google.protobuf import empty_pb2
+
+import settings
+from api import monorail_servicer
+from api import converters
+from api.api_proto import issue_objects_pb2
+from api.api_proto import issues_pb2
+from api.api_proto import issues_prpc_pb2
+from businesslogic import work_env
+from features import filterrules_helpers
+from features import savedqueries_helpers
+from framework import exceptions
+from framework import framework_constants
+from framework import framework_views
+from framework import permissions
+from proto import tracker_pb2
+from search import searchpipeline
+from tracker import field_helpers
+from tracker import tracker_bizobj
+from tracker import tracker_helpers
+
+
+class IssuesServicer(monorail_servicer.MonorailServicer):
+ """Handle API requests related to Issue objects.
+
+ Each API request is implemented with a method as defined in the
+ .proto file that does any request-specific validation, uses work_env
+ to safely operate on business objects, and returns a response proto.
+ """
+
+ DESCRIPTION = issues_prpc_pb2.IssuesServiceDescription
+
+ def _GetProjectIssueAndConfig(
+ self, mc, issue_ref, use_cache=True, issue_required=True,
+ view_deleted=False):
+ """Get three objects that we need for most requests with an issue_ref."""
+ issue = None
+ with work_env.WorkEnv(mc, self.services, phase='getting P, I, C') as we:
+ project = we.GetProjectByName(
+ issue_ref.project_name, use_cache=use_cache)
+ mc.LookupLoggedInUserPerms(project)
+ config = we.GetProjectConfig(project.project_id, use_cache=use_cache)
+ if issue_required or issue_ref.local_id:
+ try:
+ issue = we.GetIssueByLocalID(
+ project.project_id, issue_ref.local_id, use_cache=use_cache,
+ allow_viewing_deleted=view_deleted)
+ except exceptions.NoSuchIssueException as e:
+ issue = None
+ if issue_required:
+ raise e
+ return project, issue, config
+
+ def _GetProjectIssueIDsAndConfig(
+ self, mc, issue_refs, use_cache=True):
+ """Get info from a single project for repeated issue_refs requests."""
+ project_names = set()
+ local_ids = []
+ for issue_ref in issue_refs:
+ if not issue_ref.local_id:
+ raise exceptions.InputException('Param `local_id` required.')
+ local_ids.append(issue_ref.local_id)
+ if issue_ref.project_name:
+ project_names.add(issue_ref.project_name)
+
+ if not project_names:
+ raise exceptions.InputException('Param `project_name` required.')
+ if len(project_names) != 1:
+ raise exceptions.InputException(
+ 'This method does not support cross-project issue_refs.')
+ project_name = project_names.pop()
+ with work_env.WorkEnv(mc, self.services, phase='getting P, I ids, C') as we:
+ project = we.GetProjectByName(project_name, use_cache=use_cache)
+ mc.LookupLoggedInUserPerms(project)
+ config = we.GetProjectConfig(project.project_id, use_cache=use_cache)
+ project_local_id_pairs = [(project.project_id, local_id)
+ for local_id in local_ids]
+ issue_ids, _misses = self.services.issue.LookupIssueIDs(
+ mc.cnxn, project_local_id_pairs)
+ return project, issue_ids, config
+
+ @monorail_servicer.PRPCMethod
+ def CreateIssue(self, _mc, request):
+ response = issue_objects_pb2.Issue()
+ response.CopyFrom(request.issue)
+ return response
+
+ @monorail_servicer.PRPCMethod
+ def GetIssue(self, mc, request):
+ """Return the specified issue in a response proto."""
+ issue_ref = request.issue_ref
+ project, issue, config = self._GetProjectIssueAndConfig(
+ mc, issue_ref, view_deleted=True, issue_required=False)
+
+ # Code for getting where a moved issue was moved to.
+ if issue is None:
+ moved_to_ref = self.services.issue.GetCurrentLocationOfMovedIssue(
+ mc.cnxn, project.project_id, issue_ref.local_id)
+ moved_to_project_id, moved_to_id = moved_to_ref
+ moved_to_project_name = None
+
+ if moved_to_project_id is not None:
+ with work_env.WorkEnv(mc, self.services) as we:
+ moved_to_project = we.GetProject(moved_to_project_id)
+ moved_to_project_name = moved_to_project.project_name
+ return issues_pb2.IssueResponse(moved_to_ref=converters.ConvertIssueRef(
+ (moved_to_project_name, moved_to_id)))
+
+ raise exceptions.NoSuchIssueException()
+
+ if issue.deleted:
+ return issues_pb2.IssueResponse(
+ issue=issue_objects_pb2.Issue(is_deleted=True))
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ related_refs = we.GetRelatedIssueRefs([issue])
+
+ with mc.profiler.Phase('making user views'):
+ users_involved_in_issue = tracker_bizobj.UsersInvolvedInIssues([issue])
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, users_involved_in_issue)
+ framework_views.RevealAllEmailsToMembers(
+ mc.cnxn, self.services, mc.auth, users_by_id, project)
+
+ with mc.profiler.Phase('converting to response objects'):
+ response = issues_pb2.IssueResponse()
+ response.issue.CopyFrom(converters.ConvertIssue(
+ issue, users_by_id, related_refs, config))
+
+ return response
+
+ @monorail_servicer.PRPCMethod
+ def ListIssues(self, mc, request):
+ """Return the list of issues for projects that satisfy the given query."""
+ use_cached_searches = not settings.local_mode
+ can = request.canned_query or 1
+ with work_env.WorkEnv(mc, self.services) as we:
+ start, max_items = converters.IngestPagination(request.pagination)
+ pipeline = we.ListIssues(
+ request.query, request.project_names, mc.auth.user_id, max_items,
+ start, can, request.group_by_spec, request.sort_spec,
+ use_cached_searches)
+ with mc.profiler.Phase('reveal emails to members'):
+ projects = self.services.project.GetProjectsByName(
+ mc.cnxn, request.project_names)
+ for _, p in projects.items():
+ framework_views.RevealAllEmailsToMembers(
+ mc.cnxn, self.services, mc.auth, pipeline.users_by_id, p)
+
+ converted_results = []
+ with work_env.WorkEnv(mc, self.services) as we:
+ for issue in (pipeline.visible_results or []):
+ related_refs = we.GetRelatedIssueRefs([issue])
+ converted_results.append(
+ converters.ConvertIssue(issue, pipeline.users_by_id, related_refs,
+ pipeline.harmonized_config))
+ total_results = 0
+ if hasattr(pipeline.pagination, 'total_count'):
+ total_results = pipeline.pagination.total_count
+ return issues_pb2.ListIssuesResponse(
+ issues=converted_results, total_results=total_results)
+
+
+ @monorail_servicer.PRPCMethod
+ def ListReferencedIssues(self, mc, request):
+ """Return the specified issues in a response proto."""
+ if not request.issue_refs:
+ return issues_pb2.ListReferencedIssuesResponse()
+
+ for issue_ref in request.issue_refs:
+ if not issue_ref.project_name:
+ raise exceptions.InputException('Param `project_name` required.')
+ if not issue_ref.local_id:
+ raise exceptions.InputException('Param `local_id` required.')
+
+ default_project_name = request.issue_refs[0].project_name
+ ref_tuples = [
+ (ref.project_name, ref.local_id) for ref in request.issue_refs]
+ with work_env.WorkEnv(mc, self.services) as we:
+ open_issues, closed_issues = we.ListReferencedIssues(
+ ref_tuples, default_project_name)
+ all_issues = open_issues + closed_issues
+ all_project_ids = [issue.project_id for issue in all_issues]
+ related_refs = we.GetRelatedIssueRefs(all_issues)
+ configs = we.GetProjectConfigs(all_project_ids)
+
+ with mc.profiler.Phase('making user views'):
+ users_involved = tracker_bizobj.UsersInvolvedInIssues(all_issues)
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, users_involved)
+ framework_views.RevealAllEmailsToMembers(
+ mc.cnxn, self.services, mc.auth, users_by_id)
+
+ with mc.profiler.Phase('converting to response objects'):
+ converted_open_issues = [
+ converters.ConvertIssue(
+ issue, users_by_id, related_refs, configs[issue.project_id])
+ for issue in open_issues]
+ converted_closed_issues = [
+ converters.ConvertIssue(
+ issue, users_by_id, related_refs, configs[issue.project_id])
+ for issue in closed_issues]
+ response = issues_pb2.ListReferencedIssuesResponse(
+ open_refs=converted_open_issues, closed_refs=converted_closed_issues)
+
+ return response
+
+ @monorail_servicer.PRPCMethod
+ def ListApplicableFieldDefs(self, mc, request):
+ """Returns specified issues' applicable field refs in a response proto."""
+ if not request.issue_refs:
+ return issues_pb2.ListApplicableFieldDefsResponse()
+
+ _project, issue_ids, config = self._GetProjectIssueIDsAndConfig(
+ mc, request.issue_refs)
+ with work_env.WorkEnv(mc, self.services) as we:
+ issues_dict = we.GetIssuesDict(issue_ids)
+ fds = field_helpers.ListApplicableFieldDefs(issues_dict.values(), config)
+
+ users_by_id = {}
+ with mc.profiler.Phase('converting to response objects'):
+ users_involved = tracker_bizobj.UsersInvolvedInConfig(config)
+ users_by_id.update(framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, users_involved))
+ field_defs = [
+ converters.ConvertFieldDef(fd, [], users_by_id, config, True)
+ for fd in fds]
+
+ return issues_pb2.ListApplicableFieldDefsResponse(field_defs=field_defs)
+
+ @monorail_servicer.PRPCMethod
+ def UpdateIssue(self, mc, request):
+ """Apply a delta and comment to the specified issue, then return it."""
+ project, issue, config = self._GetProjectIssueAndConfig(
+ mc, request.issue_ref, use_cache=False)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ if request.HasField('delta'):
+ delta = converters.IngestIssueDelta(
+ mc.cnxn, self.services, request.delta, config, issue.phases)
+ else:
+ delta = tracker_pb2.IssueDelta() # No changes specified.
+ attachments = converters.IngestAttachmentUploads(request.uploads)
+ we.UpdateIssue(
+ issue, delta, request.comment_content, send_email=request.send_email,
+ attachments=attachments, is_description=request.is_description,
+ kept_attachments=list(request.kept_attachments))
+ related_refs = we.GetRelatedIssueRefs([issue])
+
+ with mc.profiler.Phase('making user views'):
+ users_involved_in_issue = tracker_bizobj.UsersInvolvedInIssues([issue])
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, users_involved_in_issue)
+ framework_views.RevealAllEmailsToMembers(
+ mc.cnxn, self.services, mc.auth, users_by_id, project)
+
+ with mc.profiler.Phase('converting to response objects'):
+ response = issues_pb2.IssueResponse()
+ response.issue.CopyFrom(converters.ConvertIssue(
+ issue, users_by_id, related_refs, config))
+
+ return response
+
+ @monorail_servicer.PRPCMethod
+ def StarIssue(self, mc, request):
+ """Star (or unstar) the specified issue."""
+ _project, issue, _config = self._GetProjectIssueAndConfig(
+ mc, request.issue_ref, use_cache=False)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.StarIssue(issue, request.starred)
+ # Reload the issue to get the new star count.
+ issue = we.GetIssue(issue.issue_id)
+
+ with mc.profiler.Phase('converting to response objects'):
+ response = issues_pb2.StarIssueResponse()
+ response.star_count = issue.star_count
+
+ return response
+
+ @monorail_servicer.PRPCMethod
+ def IsIssueStarred(self, mc, request):
+ """Respond true if the signed-in user has starred the specified issue."""
+ _project, issue, _config = self._GetProjectIssueAndConfig(
+ mc, request.issue_ref, use_cache=False)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ is_starred = we.IsIssueStarred(issue)
+
+ with mc.profiler.Phase('converting to response objects'):
+ response = issues_pb2.IsIssueStarredResponse()
+ response.is_starred = is_starred
+
+ return response
+
+ @monorail_servicer.PRPCMethod
+ def ListStarredIssues(self, mc, _request):
+ """Return a list of issue ids that the signed-in user has starred."""
+ with work_env.WorkEnv(mc, self.services) as we:
+ starred_issues = we.ListStarredIssueIDs()
+ starred_issues_dict = we.GetIssueRefs(starred_issues)
+
+ with mc.profiler.Phase('converting to response objects'):
+ converted_starred_issue_refs = converters.ConvertIssueRefs(
+ starred_issues, starred_issues_dict)
+ response = issues_pb2.ListStarredIssuesResponse(
+ starred_issue_refs=converted_starred_issue_refs)
+
+ return response
+
+ @monorail_servicer.PRPCMethod
+ def ListComments(self, mc, request):
+ """Return comments on the specified issue in a response proto."""
+ project, issue, config = self._GetProjectIssueAndConfig(
+ mc, request.issue_ref)
+ with work_env.WorkEnv(mc, self.services) as we:
+ comments = we.ListIssueComments(issue)
+ _, comment_reporters = we.LookupIssueFlaggers(issue)
+
+ with mc.profiler.Phase('making user views'):
+ users_involved_in_comments = tracker_bizobj.UsersInvolvedInCommentList(
+ comments)
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, users_involved_in_comments)
+ framework_views.RevealAllEmailsToMembers(
+ mc.cnxn, self.services, mc.auth, users_by_id, project)
+
+ with mc.profiler.Phase('converting to response objects'):
+ issue_perms = permissions.UpdateIssuePermissions(
+ mc.perms, project, issue, mc.auth.effective_ids, config=config)
+ converted_comments = converters.ConvertCommentList(
+ issue, comments, config, users_by_id, comment_reporters,
+ mc.auth.user_id, issue_perms)
+ response = issues_pb2.ListCommentsResponse(comments=converted_comments)
+
+ return response
+
+ @monorail_servicer.PRPCMethod
+ def ListActivities(self, mc, request):
+ """Return issue activities by a specified user in a response proto."""
+ converted_user = converters.IngestUserRef(mc.cnxn, request.user_ref,
+ self.services.user)
+ user = self.services.user.GetUser(mc.cnxn, converted_user)
+ comments = self.services.issue.GetIssueActivity(
+ mc.cnxn, user_ids={request.user_ref.user_id})
+ issues = self.services.issue.GetIssues(
+ mc.cnxn, {c.issue_id for c in comments})
+ project_dict = tracker_helpers.GetAllIssueProjects(
+ mc.cnxn, issues, self.services.project)
+ config_dict = self.services.config.GetProjectConfigs(
+ mc.cnxn, list(project_dict.keys()))
+ allowed_issues = tracker_helpers.FilterOutNonViewableIssues(
+ mc.auth.effective_ids, user, project_dict,
+ config_dict, issues)
+ issue_dict = {issue.issue_id: issue for issue in allowed_issues}
+ comments = [
+ c for c in comments if c.issue_id in issue_dict]
+
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, [request.user_ref.user_id],
+ tracker_bizobj.UsersInvolvedInCommentList(comments))
+ for project in project_dict.values():
+ framework_views.RevealAllEmailsToMembers(
+ mc.cnxn, self.services, mc.auth, users_by_id, project)
+
+ issues_by_project = {}
+ for issue in allowed_issues:
+ issues_by_project.setdefault(issue.project_id, []).append(issue)
+
+ # A dictionary {issue_id: perms} of the PermissionSet for the current user
+ # on each of the issues.
+ issue_perms_dict = {}
+ # A dictionary {comment_id: [reporter_id]} of users who have reported the
+ # comment as spam.
+ comment_reporters = {}
+ for project_id, project_issues in issues_by_project.items():
+ mc.LookupLoggedInUserPerms(project_dict[project_id])
+ issue_perms_dict.update({
+ issue.issue_id: permissions.UpdateIssuePermissions(
+ mc.perms, project_dict[issue.project_id], issue,
+ mc.auth.effective_ids, config=config_dict[issue.project_id])
+ for issue in project_issues})
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ project_issue_reporters = we.LookupIssuesFlaggers(project_issues)
+ for _, issue_comment_reporters in project_issue_reporters.values():
+ comment_reporters.update(issue_comment_reporters)
+
+ with mc.profiler.Phase('converting to response objects'):
+ converted_comments = []
+ for c in comments:
+ issue = issue_dict.get(c.issue_id)
+ issue_perms = issue_perms_dict.get(c.issue_id)
+ result = converters.ConvertComment(
+ issue, c,
+ config_dict.get(issue.project_id),
+ users_by_id,
+ comment_reporters.get(c.id, []),
+ {c.id: 1} if c.is_description else {},
+ mc.auth.user_id, issue_perms)
+ converted_comments.append(result)
+ converted_issues = [issue_objects_pb2.IssueSummary(
+ project_name=issue.project_name, local_id=issue.local_id,
+ summary=issue.summary) for issue in allowed_issues]
+ response = issues_pb2.ListActivitiesResponse(
+ comments=converted_comments, issue_summaries=converted_issues)
+
+ return response
+
+ @monorail_servicer.PRPCMethod
+ def DeleteComment(self, mc, request):
+ _project, issue, _config = self._GetProjectIssueAndConfig(
+ mc, request.issue_ref, use_cache=False)
+ with work_env.WorkEnv(mc, self.services) as we:
+ all_comments = we.ListIssueComments(issue)
+ try:
+ comment = all_comments[request.sequence_num]
+ except IndexError:
+ raise exceptions.NoSuchCommentException()
+ we.DeleteComment(issue, comment, request.delete)
+
+ return empty_pb2.Empty()
+
+ @monorail_servicer.PRPCMethod
+ def BulkUpdateApprovals(self, mc, request):
+ """Update multiple issues' approval and return the updated issue_refs."""
+ if not request.issue_refs:
+ raise exceptions.InputException('Param `issue_refs` empty.')
+
+ project, issue_ids, config = self._GetProjectIssueIDsAndConfig(
+ mc, request.issue_refs)
+
+ approval_fd = tracker_bizobj.FindFieldDef(
+ request.field_ref.field_name, config)
+ if not approval_fd:
+ raise exceptions.NoSuchFieldDefException()
+ if request.HasField('approval_delta'):
+ approval_delta = converters.IngestApprovalDelta(
+ mc.cnxn, self.services.user, request.approval_delta,
+ mc.auth.user_id, config)
+ else:
+ approval_delta = tracker_pb2.ApprovalDelta()
+ # No bulk adding approval attachments for now.
+
+ with work_env.WorkEnv(mc, self.services, phase='updating approvals') as we:
+ updated_issue_ids = we.BulkUpdateIssueApprovals(
+ issue_ids, approval_fd.field_id, project, approval_delta,
+ request.comment_content, send_email=request.send_email)
+ with mc.profiler.Phase('converting to response objects'):
+ issue_ref_pairs = we.GetIssueRefs(updated_issue_ids)
+ issue_refs = [converters.ConvertIssueRef(pair)
+ for pair in issue_ref_pairs.values()]
+ response = issues_pb2.BulkUpdateApprovalsResponse(issue_refs=issue_refs)
+
+ return response
+
+ @monorail_servicer.PRPCMethod
+ def UpdateApproval(self, mc, request):
+ """Update and return an approval in a response proto."""
+ project, issue, config = self._GetProjectIssueAndConfig(
+ mc, request.issue_ref, use_cache=False)
+
+ approval_fd = tracker_bizobj.FindFieldDef(
+ request.field_ref.field_name, config)
+ if not approval_fd:
+ raise exceptions.NoSuchFieldDefException()
+ if request.HasField('approval_delta'):
+ approval_delta = converters.IngestApprovalDelta(
+ mc.cnxn, self.services.user, request.approval_delta,
+ mc.auth.user_id, config)
+ else:
+ approval_delta = tracker_pb2.ApprovalDelta()
+ attachments = converters.IngestAttachmentUploads(request.uploads)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ av, _comment, _issue = we.UpdateIssueApproval(
+ issue.issue_id,
+ approval_fd.field_id,
+ approval_delta,
+ request.comment_content,
+ request.is_description,
+ attachments=attachments,
+ send_email=request.send_email,
+ kept_attachments=list(request.kept_attachments))
+
+ with mc.profiler.Phase('converting to response objects'):
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, av.approver_ids, [av.setter_id])
+ framework_views.RevealAllEmailsToMembers(
+ mc.cnxn, self.services, mc.auth, users_by_id, project)
+ response = issues_pb2.UpdateApprovalResponse()
+ response.approval.CopyFrom(converters.ConvertApproval(
+ av, users_by_id, config))
+
+ return response
+
+ @monorail_servicer.PRPCMethod
+ def ConvertIssueApprovalsTemplate(self, mc, request):
+ """Update an issue's existing approvals structure to match the one of the
+ given template."""
+
+ if not request.issue_ref.local_id or not request.issue_ref.project_name:
+ raise exceptions.InputException('Param `issue_ref.local_id` empty')
+ if not request.template_name:
+ raise exceptions.InputException('Param `template_name` empty')
+
+ project, issue, config = self._GetProjectIssueAndConfig(
+ mc, request.issue_ref, use_cache=False)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.ConvertIssueApprovalsTemplate(
+ config, issue, request.template_name, request.comment_content,
+ send_email=request.send_email)
+ related_refs = we.GetRelatedIssueRefs([issue])
+
+ with mc.profiler.Phase('making user views'):
+ users_involved_in_issue = tracker_bizobj.UsersInvolvedInIssues([issue])
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, users_involved_in_issue)
+ framework_views.RevealAllEmailsToMembers(
+ mc.cnxn, self.services, mc.auth, users_by_id, project)
+
+ with mc.profiler.Phase('converting to response objects'):
+ response = issues_pb2.ConvertIssueApprovalsTemplateResponse()
+ response.issue.CopyFrom(converters.ConvertIssue(
+ issue, users_by_id, related_refs, config))
+ return response
+
+ @monorail_servicer.PRPCMethod
+ def IssueSnapshot(self, mc, request):
+ """Fetch IssueSnapshot counts for charting."""
+ warnings = []
+
+ if not request.timestamp:
+ raise exceptions.InputException('Param `timestamp` required.')
+
+ if not request.project_name and not request.hotlist_id:
+ raise exceptions.InputException('Params `project_name` or `hotlist_id` '
+ 'required.')
+
+ if request.group_by == 'label' and not request.label_prefix:
+ raise exceptions.InputException('Param `label_prefix` required.')
+
+ if request.canned_query:
+ canned_query = savedqueries_helpers.SavedQueryIDToCond(
+ mc.cnxn, self.services.features, request.canned_query)
+ # TODO(jrobbins): support linked accounts me_user_ids.
+ canned_query, warnings = searchpipeline.ReplaceKeywordsWithUserIDs(
+ [mc.auth.user_id], canned_query)
+ else:
+ canned_query = None
+
+ if request.query:
+ query, warnings = searchpipeline.ReplaceKeywordsWithUserIDs(
+ [mc.auth.user_id], request.query)
+ else:
+ query = None
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ try:
+ project = we.GetProjectByName(request.project_name)
+ except exceptions.NoSuchProjectException:
+ project = None
+
+ if request.hotlist_id:
+ hotlist = we.GetHotlist(request.hotlist_id)
+ else:
+ hotlist = None
+
+ results, unsupported_fields, limit_reached = we.SnapshotCountsQuery(
+ project, request.timestamp, request.group_by,
+ label_prefix=request.label_prefix, query=query,
+ canned_query=canned_query, hotlist=hotlist)
+ if request.group_by == 'owner':
+ # Map user ids to emails.
+ snapshot_counts = [
+ issues_pb2.IssueSnapshotCount(
+ dimension=self.services.user.GetUser(mc.cnxn, key).email,
+ count=result) for key, result in results.iteritems()
+ ]
+ else:
+ snapshot_counts = [
+ issues_pb2.IssueSnapshotCount(dimension=key, count=result)
+ for key, result in results.items()
+ ]
+ response = issues_pb2.IssueSnapshotResponse()
+ response.snapshot_count.extend(snapshot_counts)
+ response.unsupported_field.extend(unsupported_fields)
+ response.unsupported_field.extend(warnings)
+ response.search_limit_reached = limit_reached
+ return response
+
+ @monorail_servicer.PRPCMethod
+ def PresubmitIssue(self, mc, request):
+ """Provide the UI with warnings and suggestions."""
+ project, issue, config = self._GetProjectIssueAndConfig(
+ mc, request.issue_ref, issue_required=False)
+
+ with mc.profiler.Phase('making user views'):
+ try:
+ proposed_owner_id = converters.IngestUserRef(
+ mc.cnxn, request.issue_delta.owner_ref, self.services.user)
+ except exceptions.NoSuchUserException:
+ proposed_owner_id = 0
+
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, [proposed_owner_id])
+ proposed_owner_view = users_by_id[proposed_owner_id]
+
+ with mc.profiler.Phase('Applying IssueDelta'):
+ if issue:
+ proposed_issue = copy.deepcopy(issue)
+ else:
+ proposed_issue = tracker_pb2.Issue(
+ owner_id=framework_constants.NO_USER_SPECIFIED,
+ project_id=config.project_id)
+ issue_delta = converters.IngestIssueDelta(
+ mc.cnxn, self.services, request.issue_delta, config, None,
+ ignore_missing_objects=True)
+ tracker_bizobj.ApplyIssueDelta(
+ mc.cnxn, self.services.issue, proposed_issue, issue_delta, config)
+
+ with mc.profiler.Phase('applying rules'):
+ _, traces = filterrules_helpers.ApplyFilterRules(
+ mc.cnxn, self.services, proposed_issue, config)
+ logging.info('proposed_issue is now: %r', proposed_issue)
+ logging.info('traces are: %r', traces)
+
+ with mc.profiler.Phase('making derived user views'):
+ derived_users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, [proposed_issue.derived_owner_id],
+ proposed_issue.derived_cc_ids)
+ framework_views.RevealAllEmailsToMembers(
+ mc.cnxn, self.services, mc.auth, derived_users_by_id, project)
+
+ with mc.profiler.Phase('pair derived values with rule explanations'):
+ (derived_labels, derived_owners, derived_ccs, warnings, errors) = (
+ tracker_helpers.PairDerivedValuesWithRuleExplanations(
+ proposed_issue, traces, derived_users_by_id))
+
+ result = issues_pb2.PresubmitIssueResponse(
+ owner_availability=proposed_owner_view.avail_message_short,
+ owner_availability_state=proposed_owner_view.avail_state,
+ derived_labels=converters.ConvertValueAndWhyList(derived_labels),
+ derived_owners=converters.ConvertValueAndWhyList(derived_owners),
+ derived_ccs=converters.ConvertValueAndWhyList(derived_ccs),
+ warnings=converters.ConvertValueAndWhyList(warnings),
+ errors=converters.ConvertValueAndWhyList(errors))
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def RerankBlockedOnIssues(self, mc, request):
+ """Rerank the blocked on issues for the given issue ref."""
+ moved_issue_id, target_issue_id = converters.IngestIssueRefs(
+ mc.cnxn, [request.moved_ref, request.target_ref], self.services)
+ _project, issue, _config = self._GetProjectIssueAndConfig(
+ mc, request.issue_ref)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.RerankBlockedOnIssues(
+ issue, moved_issue_id, target_issue_id, request.split_above)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ issue = we.GetIssue(issue.issue_id)
+ related_refs = we.GetRelatedIssueRefs([issue])
+
+ with mc.profiler.Phase('converting to response objects'):
+ converted_issue_refs = converters.ConvertIssueRefs(
+ issue.blocked_on_iids, related_refs)
+ result = issues_pb2.RerankBlockedOnIssuesResponse(
+ blocked_on_issue_refs=converted_issue_refs)
+
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def DeleteIssue(self, mc, request):
+ """Mark or unmark the given issue as deleted."""
+ _project, issue, _config = self._GetProjectIssueAndConfig(
+ mc, request.issue_ref, view_deleted=True)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.DeleteIssue(issue, request.delete)
+
+ result = issues_pb2.DeleteIssueResponse()
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def DeleteIssueComment(self, mc, request):
+ """Mark or unmark the given comment as deleted."""
+ _project, issue, _config = self._GetProjectIssueAndConfig(
+ mc, request.issue_ref, use_cache=False)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ comments = we.ListIssueComments(issue)
+ if request.sequence_num >= len(comments):
+ raise exceptions.InputException('Invalid sequence number.')
+ we.DeleteComment(issue, comments[request.sequence_num], request.delete)
+
+ result = issues_pb2.DeleteIssueCommentResponse()
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def DeleteAttachment(self, mc, request):
+ """Mark or unmark the given attachment as deleted."""
+ _project, issue, _config = self._GetProjectIssueAndConfig(
+ mc, request.issue_ref, use_cache=False)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ comments = we.ListIssueComments(issue)
+ if request.sequence_num >= len(comments):
+ raise exceptions.InputException('Invalid sequence number.')
+ we.DeleteAttachment(
+ issue, comments[request.sequence_num], request.attachment_id,
+ request.delete)
+
+ result = issues_pb2.DeleteAttachmentResponse()
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def FlagIssues(self, mc, request):
+ """Flag or unflag the given issues as spam."""
+ if not request.issue_refs:
+ raise exceptions.InputException('Param `issue_refs` empty.')
+
+ _project, issue_ids, _config = self._GetProjectIssueIDsAndConfig(
+ mc, request.issue_refs)
+ with work_env.WorkEnv(mc, self.services) as we:
+ issues_by_id = we.GetIssuesDict(issue_ids, use_cache=False)
+ we.FlagIssues(list(issues_by_id.values()), request.flag)
+
+ result = issues_pb2.FlagIssuesResponse()
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def FlagComment(self, mc, request):
+ """Flag or unflag the given comment as spam."""
+ _project, issue, _config = self._GetProjectIssueAndConfig(
+ mc, request.issue_ref, use_cache=False)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ comments = we.ListIssueComments(issue)
+ if request.sequence_num >= len(comments):
+ raise exceptions.InputException('Invalid sequence number.')
+ we.FlagComment(issue, comments[request.sequence_num], request.flag)
+
+ result = issues_pb2.FlagCommentResponse()
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def ListIssuePermissions(self, mc, request):
+ """List the permissions for the current user in the given issue."""
+ project, issue, config = self._GetProjectIssueAndConfig(
+ mc, request.issue_ref, use_cache=False, view_deleted=True)
+
+ perms = permissions.UpdateIssuePermissions(
+ mc.perms, project, issue, mc.auth.effective_ids, config=config)
+
+ return issues_pb2.ListIssuePermissionsResponse(
+ permissions=sorted(perms.perm_names))
+
+ @monorail_servicer.PRPCMethod
+ def MoveIssue(self, mc, request):
+ """Move an issue to another project."""
+ _project, issue, _config = self._GetProjectIssueAndConfig(
+ mc, request.issue_ref, use_cache=False)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ target_project = we.GetProjectByName(request.target_project_name)
+ moved_issue = we.MoveIssue(issue, target_project)
+
+ result = issues_pb2.MoveIssueResponse(
+ new_issue_ref=converters.ConvertIssueRef(
+ (moved_issue.project_name, moved_issue.local_id)))
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def CopyIssue(self, mc, request):
+ """Copy an issue."""
+ _project, issue, _config = self._GetProjectIssueAndConfig(
+ mc, request.issue_ref, use_cache=False)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ target_project = we.GetProjectByName(request.target_project_name)
+ copied_issue = we.CopyIssue(issue, target_project)
+
+ result = issues_pb2.CopyIssueResponse(
+ new_issue_ref=converters.ConvertIssueRef(
+ (copied_issue.project_name, copied_issue.local_id)))
+ return result
diff --git a/api/monorail_servicer.py b/api/monorail_servicer.py
new file mode 100644
index 0000000..8968ec4
--- /dev/null
+++ b/api/monorail_servicer.py
@@ -0,0 +1,383 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import cgi
+import functools
+import logging
+import sys
+import time
+from google.appengine.api import oauth
+
+from google.appengine.api import users
+from google.protobuf import json_format
+from components.prpc import codes
+from components.prpc import server
+
+import settings
+from framework import authdata
+from framework import exceptions
+from framework import framework_bizobj
+from framework import framework_constants
+from framework import monitoring
+from framework import monorailcontext
+from framework import ratelimiter
+from framework import permissions
+from framework import sql
+from framework import xsrf
+from services import client_config_svc
+from services import features_svc
+
+
+# Header for XSRF token to protect cookie-based auth users.
+XSRF_TOKEN_HEADER = 'x-xsrf-token'
+# Header for test account email. Only accepted for local dev server.
+TEST_ACCOUNT_HEADER = 'x-test-account'
+# Optional header to help us understand why certain calls were made.
+REASON_HEADER = 'x-reason'
+# Optional header to help prevent double updates.
+REQUEST_ID_HEADER = 'x-request-id'
+
+
+def ConvertPRPCStatusToHTTPStatus(context):
+ """pRPC uses internal codes 0..16, but we want to report HTTP codes."""
+ return server._PRPC_TO_HTTP_STATUS.get(context._code, 500)
+
+
+def PRPCMethod(func):
+ @functools.wraps(func)
+ def wrapper(self, request, prpc_context, cnxn=None):
+ return self.Run(
+ func, request, prpc_context, cnxn=cnxn)
+
+ wrapper.wrapped = func
+ return wrapper
+
+
+class MonorailServicer(object):
+ """Abstract base class for API servicers.
+ """
+
+ def __init__(self, services, make_rate_limiter=True, xsrf_timeout=None):
+ self.services = services
+ if make_rate_limiter:
+ self.rate_limiter = ratelimiter.ApiRateLimiter()
+ else:
+ self.rate_limiter = None
+ # We allow subclasses to specify a different timeout. This allows the
+ # RefreshToken method to check the token with a longer expiration and
+ # generate a new one.
+ self.xsrf_timeout = xsrf_timeout or xsrf.TOKEN_TIMEOUT_SEC
+
+ def Run(
+ self, handler, request, prpc_context,
+ cnxn=None, perms=None, start_time=None, end_time=None):
+ """Run a Do* method in an API context.
+
+ Args:
+ handler: API handler method to call with MonorailContext and request.
+ request: API Request proto object.
+ prpc_context: pRPC context object with status code.
+ cnxn: Optional connection to SQL database.
+ perms: PermissionSet passed in during testing.
+ start_time: Int timestamp passed in during testing.
+ end_time: Int timestamp passed in during testing.
+
+ Returns:
+ The response proto returned from the handler or None if that
+ method raised an exception that we handle.
+
+ Raises:
+ Only programming errors should be raised as exceptions. All
+ execptions for permission checks and input validation that are
+ raised in the Do* method are converted into pRPC status codes.
+ """
+ start_time = start_time or time.time()
+ cnxn = cnxn or sql.MonorailConnection()
+ if self.services.cache_manager:
+ self.services.cache_manager.DoDistributedInvalidation(cnxn)
+
+ response = None
+ client_id = None # TODO(jrobbins): consider using client ID.
+ requester_auth = None
+ metadata = dict(prpc_context.invocation_metadata())
+ mc = monorailcontext.MonorailContext(self.services, cnxn=cnxn, perms=perms)
+ try:
+ self.AssertBaseChecks(request, metadata)
+ requester_auth = self.GetAndAssertRequesterAuth(
+ cnxn, metadata, self.services)
+ logging.info('request proto is:\n%r\n', request)
+ logging.info('requester is %r', requester_auth.email)
+
+ if self.rate_limiter:
+ self.rate_limiter.CheckStart(
+ client_id, requester_auth.email, start_time)
+ mc.auth = requester_auth
+ if not perms:
+ mc.LookupLoggedInUserPerms(self.GetRequestProject(mc.cnxn, request))
+ response = handler(self, mc, request)
+
+ except Exception as e:
+ if not self.ProcessException(e, prpc_context, mc):
+ raise e.__class__, e, sys.exc_info()[2]
+ finally:
+ if mc:
+ mc.CleanUp()
+ if self.rate_limiter and requester_auth and requester_auth.email:
+ end_time = end_time or time.time()
+ self.rate_limiter.CheckEnd(
+ client_id, requester_auth.email, end_time, start_time)
+ self.RecordMonitoringStats(start_time, request, response, prpc_context)
+
+ return response
+
+ def _GetAllowedEmailDomainAuth(self, cnxn, services):
+ """Checks if the requester's email is found in api_allowed_email_domains
+ and is authorized by the custom monorail scope.
+
+ Args:
+ cnxn: connection to the SQL database.
+ services: connections to backend services.
+
+ Returns:
+ A new AuthData object if the method determines the requester is allowed
+ to access the API, otherwise, None.
+ """
+ try:
+ # Note: get_current_user(scopes) returns the User with the User's email.
+ # So, in addition to requesting any scope listed in 'scopes', it also
+ # always requests the email scope.
+ monorail_scope_user = oauth.get_current_user(
+ framework_constants.MONORAIL_SCOPE)
+ logging.info('monorail scope user %r', monorail_scope_user)
+ # TODO(b/144508063): remove this workaround.
+ authorized_scopes = oauth.get_authorized_scopes(
+ framework_constants.MONORAIL_SCOPE)
+ if framework_constants.MONORAIL_SCOPE not in authorized_scopes:
+ raise oauth.Error('Work around for b/144508063')
+ logging.info(authorized_scopes)
+ if (monorail_scope_user and monorail_scope_user.email().endswith(
+ settings.api_allowed_email_domains)):
+ logging.info('User %r authenticated with Oauth and monorail',
+ monorail_scope_user.email())
+ return authdata.AuthData.FromEmail(
+ cnxn, monorail_scope_user.email(), services)
+ except oauth.Error as ex:
+ logging.info('oauth.Error for monorail scope: %s' % ex)
+ return None
+
+ def GetAndAssertRequesterAuth(self, cnxn, metadata, services):
+ """Gets the requester identity and checks if the user has permission
+ to make the request.
+ Any users successfully authenticated with oauth must be allowlisted or
+ have accounts with the domains in api_allowed_email_domains.
+ Users identified using cookie-based auth must have valid XSRF tokens.
+ Test accounts ending with @example.com are only allowed in the
+ local_mode.
+
+ Args:
+ cnxn: connection to the SQL database.
+ metadata: metadata sent by the client.
+ services: connections to backend services.
+
+ Returns:
+ A new AuthData object representing a signed in or anonymous user.
+
+ Raises:
+ exceptions.NoSuchUserException: If the requester does not exist
+ permissions.BannedUserException: If the user has been banned from the site
+ permissions.PermissionException: If the user is not authorized with the
+ Monorail scope, is not allowlisted, and has an invalid token.
+ """
+ # TODO(monorail:6538): Move different authentication methods into separate
+ # functions.
+ requester_auth = None
+ # When running on localhost, allow request to specify test account.
+ if TEST_ACCOUNT_HEADER in metadata:
+ if not settings.local_mode:
+ raise exceptions.InputException(
+ 'x-test-account only accepted in local_mode')
+ # For local development, we accept any request.
+ # TODO(jrobbins): make this more realistic by requiring a fake XSRF token.
+ test_account = metadata[TEST_ACCOUNT_HEADER]
+ if not test_account.endswith('@example.com'):
+ raise exceptions.InputException(
+ 'test_account must end with @example.com')
+ logging.info('Using test_account: %r' % test_account)
+ requester_auth = authdata.AuthData.FromEmail(cnxn, test_account, services)
+
+ # Oauth for users with email domains in api_allowed_email_domains.
+ if not requester_auth:
+ requester_auth = self._GetAllowedEmailDomainAuth(cnxn, services)
+
+ # Oauth for allowlisted users
+ if not requester_auth:
+ try:
+ client_id = oauth.get_client_id(framework_constants.OAUTH_SCOPE)
+ user = oauth.get_current_user(framework_constants.OAUTH_SCOPE)
+ if user:
+ auth_client_ids, auth_emails = (
+ client_config_svc.GetClientConfigSvc().GetClientIDEmails())
+ logging.info('Oauth requester %s', user.email())
+ # Check if email or client_id is allowlisted
+ if (user.email() in auth_emails) or (client_id in auth_client_ids):
+ logging.info('Client %r is allowlisted', user.email())
+ requester_auth = authdata.AuthData.FromEmail(
+ cnxn, user.email(), services)
+ except oauth.Error as ex:
+ logging.info('Got oauth error: %r', ex)
+
+ # Cookie-based auth for signed in and anonymous users.
+ if not requester_auth:
+ # Check for signed in user
+ user = users.get_current_user()
+ if user:
+ logging.info('Using cookie user: %r', user.email())
+ requester_auth = authdata.AuthData.FromEmail(
+ cnxn, user.email(), services)
+ else:
+ # Create AuthData for anonymous user.
+ requester_auth = authdata.AuthData.FromEmail(cnxn, None, services)
+
+ # Cookie-based auth signed-in and anon users need to have the XSRF
+ # token validate.
+ try:
+ token = metadata.get(XSRF_TOKEN_HEADER)
+ xsrf.ValidateToken(
+ token, requester_auth.user_id, xsrf.XHR_SERVLET_PATH,
+ timeout=self.xsrf_timeout)
+ except xsrf.TokenIncorrect:
+ raise permissions.PermissionException(
+ 'Requester %s does not have permission to make this request.'
+ % requester_auth.email)
+
+ if permissions.IsBanned(requester_auth.user_pb, requester_auth.user_view):
+ raise permissions.BannedUserException(
+ 'The user %s has been banned from using this site' %
+ requester_auth.email)
+
+ return requester_auth
+
+ def AssertBaseChecks(self, request, metadata):
+ """Reject requests that we refuse to serve."""
+ # TODO(jrobbins): Add read_only check as an exception raised in sql.py.
+ if (settings.read_only and
+ not request.__class__.__name__.startswith(('Get', 'List'))):
+ raise permissions.PermissionException(
+ 'This request is not allowed in read-only mode')
+
+ if REASON_HEADER in metadata:
+ logging.info('Request reason: %r', metadata[REASON_HEADER])
+ if REQUEST_ID_HEADER in metadata:
+ # TODO(jrobbins): Ignore requests with duplicate request_ids.
+ logging.info('request_id: %r', metadata[REQUEST_ID_HEADER])
+
+ def GetRequestProject(self, cnxn, request):
+ """Return the Project business object that the user is viewing or None."""
+ if hasattr(request, 'project_name'):
+ project = self.services.project.GetProjectByName(
+ cnxn, request.project_name)
+ if not project:
+ logging.info(
+ 'Request has project_name: %r but it does not exist.',
+ request.project_name)
+ return None
+ return project
+ else:
+ return None
+
+ def ProcessException(self, e, prpc_context, mc):
+ """Return True if we convert an exception to a pRPC status code."""
+ logging.exception(e)
+ logging.info(e.message)
+ exc_type = type(e)
+ if exc_type == exceptions.NoSuchUserException:
+ prpc_context.set_code(codes.StatusCode.NOT_FOUND)
+ prpc_context.set_details('The user does not exist.')
+ elif exc_type == exceptions.NoSuchProjectException:
+ prpc_context.set_code(codes.StatusCode.NOT_FOUND)
+ prpc_context.set_details('The project does not exist.')
+ elif exc_type == exceptions.NoSuchTemplateException:
+ prpc_context.set_code(codes.StatusCode.NOT_FOUND)
+ prpc_context.set_details('The template does not exist.')
+ elif exc_type == exceptions.NoSuchIssueException:
+ prpc_context.set_code(codes.StatusCode.NOT_FOUND)
+ prpc_context.set_details('The issue does not exist.')
+ elif exc_type == exceptions.NoSuchCommentException:
+ prpc_context.set_code(codes.StatusCode.INVALID_ARGUMENT)
+ prpc_context.set_details('No such comment')
+ elif exc_type == exceptions.NoSuchComponentException:
+ prpc_context.set_code(codes.StatusCode.NOT_FOUND)
+ prpc_context.set_details('The component does not exist.')
+ elif exc_type == permissions.BannedUserException:
+ prpc_context.set_code(codes.StatusCode.PERMISSION_DENIED)
+ prpc_context.set_details('The requesting user has been banned.')
+ elif exc_type == permissions.PermissionException:
+ logging.info('perms is %r', mc.perms)
+ prpc_context.set_code(codes.StatusCode.PERMISSION_DENIED)
+ prpc_context.set_details('Permission denied.')
+ elif exc_type == exceptions.GroupExistsException:
+ prpc_context.set_code(codes.StatusCode.ALREADY_EXISTS)
+ prpc_context.set_details('The user group already exists.')
+ elif exc_type == features_svc.HotlistAlreadyExists:
+ prpc_context.set_code(codes.StatusCode.ALREADY_EXISTS)
+ prpc_context.set_details('A hotlist with that name already exists.')
+ elif exc_type == exceptions.FieldDefAlreadyExists:
+ prpc_context.set_code(codes.StatusCode.ALREADY_EXISTS)
+ prpc_context.set_details('A field def with that name already exists.')
+ elif exc_type == exceptions.InvalidComponentNameException:
+ prpc_context.set_code(codes.StatusCode.INVALID_ARGUMENT)
+ prpc_context.set_details('That component name is invalid.')
+ elif exc_type == exceptions.FilterRuleException:
+ prpc_context.set_code(codes.StatusCode.INVALID_ARGUMENT)
+ prpc_context.set_details('Violates filter rule that should error.')
+ elif exc_type == exceptions.InputException:
+ prpc_context.set_code(codes.StatusCode.INVALID_ARGUMENT)
+ prpc_context.set_details(
+ 'Invalid arguments: %s' % cgi.escape(e.message, quote=True))
+ elif exc_type == ratelimiter.ApiRateLimitExceeded:
+ prpc_context.set_code(codes.StatusCode.PERMISSION_DENIED)
+ prpc_context.set_details('The requester has exceeded API quotas limit.')
+ elif exc_type == oauth.InvalidOAuthTokenError:
+ prpc_context.set_code(codes.StatusCode.UNAUTHENTICATED)
+ prpc_context.set_details(
+ 'The oauth token was not valid or must be refreshed.')
+ elif exc_type == xsrf.TokenIncorrect:
+ logging.info('Bad XSRF token: %r', e.message)
+ prpc_context.set_code(codes.StatusCode.INVALID_ARGUMENT)
+ prpc_context.set_details('Bad XSRF token.')
+ else:
+ prpc_context.set_code(codes.StatusCode.INTERNAL)
+ prpc_context.set_details('Potential programming error.')
+ return False # Re-raise any exception from programming errors.
+ return True # It if was one of the cases above, don't reraise.
+
+ def RecordMonitoringStats(
+ self, start_time, request, response, prpc_context, now=None):
+ """Record monitoring info about this request."""
+ now = now or time.time()
+ elapsed_ms = int((now - start_time) * 1000)
+ method_name = request.__class__.__name__
+ if method_name.endswith('Request'):
+ method_name = method_name[:-len('Request')]
+
+ fields = monitoring.GetCommonFields(
+ # pRPC uses its own statuses, but we report HTTP status codes.
+ ConvertPRPCStatusToHTTPStatus(prpc_context),
+ # Use the API name, not the request path, to prevent an explosion in
+ # possible field values.
+ 'monorail.v0.' + method_name)
+
+ monitoring.AddServerDurations(elapsed_ms, fields)
+ monitoring.IncrementServerResponseStatusCount(fields)
+ monitoring.AddServerRequesteBytes(
+ len(json_format.MessageToJson(request)), fields)
+ response_length = 0
+ if response:
+ response_length = len(json_format.MessageToJson(response))
+ monitoring.AddServerResponseBytes(response_length, fields)
diff --git a/api/projects_servicer.py b/api/projects_servicer.py
new file mode 100644
index 0000000..640a433
--- /dev/null
+++ b/api/projects_servicer.py
@@ -0,0 +1,341 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+
+import settings
+
+from api import monorail_servicer
+from api import converters
+from api.api_proto import projects_pb2
+from api.api_proto import project_objects_pb2
+from api.api_proto import projects_prpc_pb2
+from businesslogic import work_env
+from framework import framework_bizobj
+from framework import exceptions
+from framework import framework_views
+from framework import permissions
+from project import project_helpers
+from tracker import tracker_bizobj
+from tracker import tracker_helpers
+
+# TODO(zhangtiff): Remove dependency on tracker_views.
+from tracker import tracker_views
+
+
+class ProjectsServicer(monorail_servicer.MonorailServicer):
+ """Handle API requests related to Project objects.
+
+ Each API request is implemented with a method as defined in the .proto
+ file that does any request-specific validation, uses work_env to
+ safely operate on business objects, and returns a response proto.
+ """
+
+ DESCRIPTION = projects_prpc_pb2.ProjectsServiceDescription
+
+ def _GetProject(self, mc, request, use_cache=True):
+ """Get the project object specified in the request."""
+ with work_env.WorkEnv(mc, self.services, phase='getting project') as we:
+ project = we.GetProjectByName(request.project_name, use_cache=use_cache)
+ # Perms in this project are already looked up in MonorailServicer.
+ return project
+
+ @monorail_servicer.PRPCMethod
+ def ListProjects(self, _mc, _request):
+ return projects_pb2.ListProjectsResponse(
+ projects=[
+ project_objects_pb2.Project(name='One'),
+ project_objects_pb2.Project(name='Two')],
+ next_page_token='next...')
+
+ @monorail_servicer.PRPCMethod
+ def ListProjectTemplates(self, mc, request):
+ """Return the specific project's templates."""
+ if not request.project_name:
+ raise exceptions.InputException('Param `project_name` required.')
+ project = self._GetProject(mc, request)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ config = we.GetProjectConfig(project.project_id)
+ templates = we.ListProjectTemplates(project.project_id)
+
+ with mc.profiler.Phase('converting to response objects'):
+ involved_user_ids = tracker_bizobj.UsersInvolvedInTemplates(templates)
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, involved_user_ids)
+ response = projects_pb2.ListProjectTemplatesResponse(
+ templates=converters.ConvertProjectTemplateDefs(
+ templates, users_by_id, config))
+
+ return response
+
+ @monorail_servicer.PRPCMethod
+ def GetConfig(self, mc, request):
+ """Return the specified project config."""
+ project = self._GetProject(mc, request)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ config = we.GetProjectConfig(project.project_id)
+
+ with mc.profiler.Phase('making user views'):
+ involved_user_ids = tracker_bizobj.UsersInvolvedInConfig(config)
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, involved_user_ids)
+ framework_views.RevealAllEmailsToMembers(
+ mc.cnxn, self.services, mc.auth, users_by_id, project)
+
+ label_ids = tracker_bizobj.LabelIDsInvolvedInConfig(config)
+ labels_by_id = {
+ label_id: self.services.config.LookupLabel(
+ mc.cnxn, config.project_id, label_id)
+ for label_id in label_ids}
+
+ result = converters.ConvertConfig(
+ project, config, users_by_id, labels_by_id)
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def GetPresentationConfig(self, mc, request):
+ """Return the UI centric pieces of the project config."""
+ project = self._GetProject(mc, request)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ config = we.GetProjectConfig(project.project_id)
+
+ project_thumbnail_url = tracker_views.LogoView(project).thumbnail_url
+ project_summary = project.summary
+ custom_issue_entry_url = config.custom_issue_entry_url
+ revision_url_format = (
+ project.revision_url_format or settings.revision_url_format)
+
+ default_query = None
+ saved_queries = None
+
+ # Only show default query or project saved queries for project
+ # members, in case they contain sensitive information.
+ if framework_bizobj.UserIsInProject(
+ project, mc.auth.effective_ids):
+ default_query = config.member_default_query
+
+ saved_queries = self.services.features.GetCannedQueriesByProjectID(
+ mc.cnxn, project.project_id)
+
+ return project_objects_pb2.PresentationConfig(
+ project_thumbnail_url=project_thumbnail_url,
+ project_summary=project_summary,
+ custom_issue_entry_url=custom_issue_entry_url,
+ default_query=default_query,
+ default_col_spec=config.default_col_spec,
+ default_sort_spec=config.default_sort_spec,
+ default_x_attr=config.default_x_attr,
+ default_y_attr=config.default_y_attr,
+ saved_queries=converters.IngestSavedQueries(
+ mc.cnxn, self.services.project, saved_queries),
+ revision_url_format=revision_url_format)
+
+ @monorail_servicer.PRPCMethod
+ def GetCustomPermissions(self, mc, request):
+ """Return the custom permissions for the given project."""
+ project = self._GetProject(mc, request)
+ custom_permissions = permissions.GetCustomPermissions(project)
+
+ result = projects_pb2.GetCustomPermissionsResponse(
+ permissions=custom_permissions)
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def GetVisibleMembers(self, mc, request):
+ """Return the members of the project that the user can see.
+
+ Raises:
+ PermissionException the user is not allowed to view the project members.
+ """
+ project = self._GetProject(mc, request)
+ if not permissions.CanViewContributorList(mc, project):
+ raise permissions.PermissionException(
+ 'User is not allowed to view the project members')
+
+ users_by_id = tracker_helpers.GetVisibleMembers(mc, project, self.services)
+
+ sorted_user_ids = sorted(
+ users_by_id, key=lambda uid: users_by_id[uid].email)
+ user_refs = converters.ConvertUserRefs(
+ sorted_user_ids, [], users_by_id, True)
+ sorted_group_ids = sorted(
+ (uv.user_id for uv in users_by_id.values() if uv.is_group),
+ key=lambda uid: users_by_id[uid].email)
+ group_refs = converters.ConvertUserRefs(
+ sorted_group_ids, [], users_by_id, True)
+
+ result = projects_pb2.GetVisibleMembersResponse(
+ user_refs=user_refs, group_refs=group_refs)
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def GetLabelOptions(self, mc, request):
+ """Return the label options for autocomplete for the given project."""
+ project = self._GetProject(mc, request)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ config = we.GetProjectConfig(project.project_id)
+
+ label_options = tracker_helpers.GetLabelOptions(
+ config, permissions.GetCustomPermissions(project))
+ label_defs = [
+ project_objects_pb2.LabelDef(
+ label=label['name'],
+ docstring=label['doc'])
+ for label in label_options]
+
+ result = projects_pb2.GetLabelOptionsResponse(
+ label_options=label_defs,
+ exclusive_label_prefixes=config.exclusive_label_prefixes)
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def ListStatuses(self, mc, request):
+ """Return all well-known statuses in the specified project."""
+ project = self._GetProject(mc, request)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ config = we.GetProjectConfig(project.project_id)
+
+ status_defs = [
+ converters.ConvertStatusDef(sd)
+ for sd in config.well_known_statuses]
+ statuses_offer_merge = [
+ converters.ConvertStatusRef(sd.status, None, config)
+ for sd in config.well_known_statuses
+ if sd.status in config.statuses_offer_merge]
+
+ result = projects_pb2.ListStatusesResponse(
+ status_defs=status_defs,
+ statuses_offer_merge=statuses_offer_merge,
+ restrict_to_known=config.restrict_to_known)
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def ListComponents(self, mc, request):
+ """Return all component defs in the specified project."""
+ project = self._GetProject(mc, request)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ config = we.GetProjectConfig(project.project_id)
+
+ with mc.profiler.Phase('making user views'):
+ users_by_id = {}
+ if request.include_admin_info:
+ users_involved = tracker_bizobj.UsersInvolvedInConfig(config)
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, users_involved)
+ framework_views.RevealAllEmailsToMembers(
+ mc.cnxn, self.services, mc.auth, users_by_id, project)
+
+ with mc.profiler.Phase('looking up labels'):
+ labels_by_id = {}
+ if request.include_admin_info:
+ label_ids = tracker_bizobj.LabelIDsInvolvedInConfig(config)
+ labels_by_id = {
+ label_id: self.services.config.LookupLabel(
+ mc.cnxn, config.project_id, label_id)
+ for label_id in label_ids}
+
+ component_defs = [
+ converters.ConvertComponentDef(
+ cd, users_by_id, labels_by_id, request.include_admin_info)
+ for cd in config.component_defs]
+
+ result = projects_pb2.ListComponentsResponse(
+ component_defs=component_defs)
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def ListFields(self, mc, request):
+ """List all fields for the specified project."""
+ project = self._GetProject(mc, request)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ config = we.GetProjectConfig(project.project_id)
+
+ users_by_id = {}
+ users_for_perm = {}
+ # Only look for members if user choices are requested and there are user
+ # fields that need permissions.
+ if request.include_user_choices:
+ perms_needed = {
+ fd.needs_perm
+ for fd in config.field_defs
+ if fd.needs_perm and not fd.is_deleted}
+ if perms_needed:
+ users_by_id = tracker_helpers.GetVisibleMembers(
+ mc, project, self.services)
+ effective_ids_by_user = self.services.usergroup.LookupAllMemberships(
+ mc.cnxn, users_by_id)
+ users_for_perm = project_helpers.UsersWithPermsInProject(
+ project, perms_needed, users_by_id, effective_ids_by_user)
+
+ field_defs = [
+ converters.ConvertFieldDef(
+ fd, users_for_perm.get(fd.needs_perm, []), users_by_id, config,
+ request.include_admin_info)
+ for fd in config.field_defs
+ if not fd.is_deleted]
+
+ result = projects_pb2.ListFieldsResponse(field_defs=field_defs)
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def GetProjectStarCount(self, mc, request):
+ """Get the star count for the specified project."""
+ project = self._GetProject(mc, request)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ star_count = we.GetProjectStarCount(project.project_id)
+
+ result = projects_pb2.GetProjectStarCountResponse(star_count=star_count)
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def StarProject(self, mc, request):
+ """Star the specified project."""
+ project = self._GetProject(mc, request)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.StarProject(project.project_id, request.starred)
+ star_count = we.GetProjectStarCount(project.project_id)
+
+ result = projects_pb2.StarProjectResponse(star_count=star_count)
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def CheckProjectName(self, mc, request):
+ """Check that a project name is valid and not already in use."""
+ with work_env.WorkEnv(mc, self.services) as we:
+ error = we.CheckProjectName(request.project_name)
+ result = projects_pb2.CheckProjectNameResponse(error=error)
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def CheckComponentName(self, mc, request):
+ """Check that the component name is valid and not already in use."""
+ project = self._GetProject(mc, request)
+ with work_env.WorkEnv(mc, self.services) as we:
+ error = we.CheckComponentName(
+ project.project_id, request.parent_path, request.component_name)
+ result = projects_pb2.CheckComponentNameResponse(error=error)
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def CheckFieldName(self, mc, request):
+ """Check that a field name is valid and not already in use."""
+ project = self._GetProject(mc, request)
+ with work_env.WorkEnv(mc, self.services) as we:
+ error = we.CheckFieldName(project.project_id, request.field_name)
+ result = projects_pb2.CheckFieldNameResponse(error=error)
+ return result
diff --git a/api/resource_name_converters.py b/api/resource_name_converters.py
new file mode 100644
index 0000000..cb26c9b
--- /dev/null
+++ b/api/resource_name_converters.py
@@ -0,0 +1,1059 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Methods for converting resource names to protorpc objects and back.
+
+IngestFoo methods take resource names and return the IDs of the resources.
+While some Ingest methods need to check for the existence of resources as
+a side-effect of producing their IDs, other layers that call these methods
+should always do their own validity checking.
+
+ConvertFoo methods take object ids
+(and sometimes a MonorailConnection and ServiceManager)
+and return resource names.
+"""
+
+import re
+import logging
+
+from features import features_constants
+from framework import exceptions
+from framework import validate
+from project import project_constants
+from tracker import tracker_constants
+from proto import tracker_pb2
+
+# Constants that hold regex patterns for resource names.
+PROJECT_NAME_PATTERN = (
+ r'projects\/(?P<project_name>%s)' % project_constants.PROJECT_NAME_PATTERN)
+PROJECT_NAME_RE = re.compile(r'%s$' % PROJECT_NAME_PATTERN)
+
+FIELD_DEF_NAME_RE = re.compile(
+ r'%s\/fieldDefs\/(?P<field_def>\d+)$' % (PROJECT_NAME_PATTERN))
+
+APPROVAL_DEF_NAME_PATTERN = (
+ r'%s\/approvalDefs\/(?P<approval_def>\d+)' % PROJECT_NAME_PATTERN)
+APPROVAL_DEF_NAME_RE = re.compile(r'%s$' % APPROVAL_DEF_NAME_PATTERN)
+
+HOTLIST_PATTERN = r'hotlists\/(?P<hotlist_id>\d+)'
+HOTLIST_NAME_RE = re.compile(r'%s$' % HOTLIST_PATTERN)
+HOTLIST_ITEM_NAME_RE = re.compile(
+ r'%s\/items\/(?P<project_name>%s)\.(?P<local_id>\d+)$' % (
+ HOTLIST_PATTERN,
+ project_constants.PROJECT_NAME_PATTERN))
+
+ISSUE_PATTERN = (r'projects\/(?P<project>%s)\/issues\/(?P<local_id>\d+)' %
+ project_constants.PROJECT_NAME_PATTERN)
+ISSUE_NAME_RE = re.compile(r'%s$' % ISSUE_PATTERN)
+
+COMMENT_PATTERN = (r'%s\/comments\/(?P<comment_num>\d+)' % ISSUE_PATTERN)
+COMMENT_NAME_RE = re.compile(r'%s$' % COMMENT_PATTERN)
+
+USER_NAME_RE = re.compile(r'users\/((?P<user_id>\d+)|(?P<potential_email>.+))$')
+APPROVAL_VALUE_RE = re.compile(
+ r'%s\/approvalValues\/(?P<approval_id>\d+)$' % ISSUE_PATTERN)
+
+ISSUE_TEMPLATE_RE = re.compile(
+ r'%s\/templates\/(?P<template_id>\d+)$' % (PROJECT_NAME_PATTERN))
+
+# Constants that hold the template patterns for creating resource names.
+PROJECT_NAME_TMPL = 'projects/{project_name}'
+PROJECT_CONFIG_TMPL = 'projects/{project_name}/config'
+PROJECT_MEMBER_NAME_TMPL = 'projects/{project_name}/members/{user_id}'
+HOTLIST_NAME_TMPL = 'hotlists/{hotlist_id}'
+HOTLIST_ITEM_NAME_TMPL = '%s/items/{project_name}.{local_id}' % (
+ HOTLIST_NAME_TMPL)
+
+ISSUE_NAME_TMPL = 'projects/{project}/issues/{local_id}'
+COMMENT_NAME_TMPL = '%s/comments/{comment_id}' % ISSUE_NAME_TMPL
+APPROVAL_VALUE_NAME_TMPL = '%s/approvalValues/{approval_id}' % ISSUE_NAME_TMPL
+
+USER_NAME_TMPL = 'users/{user_id}'
+PROJECT_STAR_NAME_TMPL = 'users/{user_id}/projectStars/{project_name}'
+PROJECT_SQ_NAME_TMPL = 'projects/{project_name}/savedQueries/{query_name}'
+
+ISSUE_TEMPLATE_TMPL = 'projects/{project_name}/templates/{template_id}'
+STATUS_DEF_TMPL = 'projects/{project_name}/statusDefs/{status}'
+LABEL_DEF_TMPL = 'projects/{project_name}/labelDefs/{label}'
+COMPONENT_DEF_TMPL = 'projects/{project_name}/componentDefs/{component_id}'
+COMPONENT_DEF_RE = re.compile(
+ r'%s\/componentDefs\/((?P<component_id>\d+)|(?P<path>%s))$' %
+ (PROJECT_NAME_PATTERN, tracker_constants.COMPONENT_PATH_PATTERN))
+FIELD_DEF_TMPL = 'projects/{project_name}/fieldDefs/{field_id}'
+APPROVAL_DEF_TMPL = 'projects/{project_name}/approvalDefs/{approval_id}'
+
+
+def _GetResourceNameMatch(name, regex):
+ # type: (str, Pattern[str]) -> Match[str]
+ """Takes a resource name and returns the regex match.
+
+ Args:
+ name: Resource name.
+ regex: Compiled regular expression Pattern object used to match name.
+
+ Raises:
+ InputException if there is not match.
+ """
+ match = regex.match(name)
+ if not match:
+ raise exceptions.InputException(
+ 'Invalid resource name: %s.' % name)
+ return match
+
+
+def _IssueIdsFromLocalIds(cnxn, project_local_id_pairs, services):
+ # type: (MonorailConnection, Sequence[Tuple(str, int)], Services ->
+ # Sequence[int]
+ """Fetches issue IDs using the given project/local ID pairs."""
+ # Fetch Project ids from Project names.
+ project_ids_by_name = services.project.LookupProjectIDs(
+ cnxn, [pair[0] for pair in project_local_id_pairs])
+
+ # Create (project_id, issue_local_id) pairs from project_local_id_pairs.
+ project_id_local_ids = []
+ with exceptions.ErrorAggregator(exceptions.NoSuchProjectException) as err_agg:
+ for project_name, local_id in project_local_id_pairs:
+ try:
+ project_id = project_ids_by_name[project_name]
+ project_id_local_ids.append((project_id, local_id))
+ except KeyError:
+ err_agg.AddErrorMessage('Project %s not found.' % project_name)
+
+ issue_ids, misses = services.issue.LookupIssueIDsFollowMoves(
+ cnxn, project_id_local_ids)
+ if misses:
+ # Raise error with resource names rather than backend IDs.
+ project_names_by_id = {
+ p_id: p_name for p_name, p_id in project_ids_by_name.iteritems()
+ }
+ misses_by_resource_name = [
+ _ConstructIssueName(project_names_by_id[p_id], local_id)
+ for (p_id, local_id) in misses
+ ]
+ raise exceptions.NoSuchIssueException(
+ 'Issue(s) %r not found' % misses_by_resource_name)
+ return issue_ids
+
+# FieldDefs
+
+
+def IngestFieldDefName(cnxn, name, services):
+ # type: (MonorailConnection, str, Services) -> (int, int)
+ """Ingests a FieldDef's resource name.
+
+ Args:
+ cnxn: MonorailConnection to the database.
+ name: Resource name of a FieldDef.
+ services: Services object for connections to backend services.
+
+ Returns:
+ The Project's ID and the FieldDef's ID. FieldDef is not guaranteed to exist.
+ TODO(jessan): This order should be consistent throughout the file.
+
+ Raises:
+ InputException if the given name does not have a valid format.
+ NoSuchProjectException if the given project name does not exist.
+ """
+ match = _GetResourceNameMatch(name, FIELD_DEF_NAME_RE)
+ field_id = int(match.group('field_def'))
+ project_name = match.group('project_name')
+ id_dict = services.project.LookupProjectIDs(cnxn, [project_name])
+ project_id = id_dict.get(project_name)
+ if project_id is None:
+ raise exceptions.NoSuchProjectException(
+ 'Project not found: %s.' % project_name)
+
+ return project_id, field_id
+
+# Hotlists
+
+def IngestHotlistName(name):
+ # type: (str) -> int
+ """Takes a Hotlist resource name and returns the Hotlist ID.
+
+ Args:
+ name: Resource name of a Hotlist.
+
+ Returns:
+ The Hotlist's ID
+
+ Raises:
+ InputException if the given name does not have a valid format.
+ """
+ match = _GetResourceNameMatch(name, HOTLIST_NAME_RE)
+ return int(match.group('hotlist_id'))
+
+
+def IngestHotlistItemNames(cnxn, names, services):
+ # type: (MonorailConnection, Sequence[str], Services -> Sequence[int]
+ """Takes HotlistItem resource names and returns the associated Issues' IDs.
+
+ Args:
+ cnxn: MonorailConnection to the database.
+ names: List of HotlistItem resource names.
+ services: Services object for connections to backend services.
+
+ Returns:
+ List of Issue IDs associated with the given HotlistItems.
+
+ Raises:
+ InputException if a resource name does not have a valid format.
+ NoSuchProjectException if an Issue's Project is not found.
+ NoSuchIssueException if an Issue is not found.
+ """
+ project_local_id_pairs = []
+ for name in names:
+ match = _GetResourceNameMatch(name, HOTLIST_ITEM_NAME_RE)
+ project_local_id_pairs.append(
+ (match.group('project_name'), int(match.group('local_id'))))
+ return _IssueIdsFromLocalIds(cnxn, project_local_id_pairs, services)
+
+
+def ConvertHotlistName(hotlist_id):
+ # type: (int) -> str
+ """Takes a Hotlist and returns the Hotlist's resource name.
+
+ Args:
+ hotlist_id: ID of the Hotlist.
+
+ Returns:
+ The resource name of the Hotlist.
+ """
+ return HOTLIST_NAME_TMPL.format(hotlist_id=hotlist_id)
+
+
+def ConvertHotlistItemNames(cnxn, hotlist_id, issue_ids, services):
+ # type: (MonorailConnection, int, Collection[int], Services) ->
+ # Mapping[int, str]
+ """Takes a Hotlist ID and HotlistItem's issue_ids and returns
+ the Hotlist items' resource names.
+
+ Args:
+ cnxn: MonorailConnection object.
+ hotlist_id: ID of the Hotlist the items belong to.
+ issue_ids: List of Issue IDs that are part of the hotlist's items.
+ services: Services object for connections to backend services.
+
+ Returns:
+ Dict of Issue IDs to HotlistItem resource names for Issues that are found.
+ """
+ # {issue_id: (project_name, local_id),...}
+ issue_refs_dict = services.issue.LookupIssueRefs(cnxn, issue_ids)
+
+ issue_ids_to_names = {}
+ for issue_id in issue_ids:
+ project_name, local_id = issue_refs_dict.get(issue_id, (None, None))
+ if project_name and local_id:
+ issue_ids_to_names[issue_id] = HOTLIST_ITEM_NAME_TMPL.format(
+ hotlist_id=hotlist_id, project_name=project_name, local_id=local_id)
+
+ return issue_ids_to_names
+
+# Issues
+
+
+def IngestCommentName(cnxn, name, services):
+ # type: (MonorailConnection, str, Services) -> Tuple[int, int, int]
+ """Ingests a Comment's resource name.
+
+ Args:
+ cnxn: MonorailConnection to the database.
+ name: Resource name of a Comment.
+ services: Services object for connections to backend services.
+
+ Returns:
+ Tuple containing three items:
+ 1. Global ID of the parent project.
+ 2. Global Issue id of the parent issue.
+ 3. Sequence number of the comment. This is not checked for existence.
+
+ Raises:
+ InputException if the given name does not have a valid format.
+ NoSuchIssueException if the parent Issue does not exist.
+ NoSuchProjectException if the parent Project does not exist.
+ """
+ match = _GetResourceNameMatch(name, COMMENT_NAME_RE)
+
+ # Project
+ project_name = match.group('project')
+ id_dict = services.project.LookupProjectIDs(cnxn, [project_name])
+ project_id = id_dict.get(project_name)
+ if project_id is None:
+ raise exceptions.NoSuchProjectException(
+ 'Project not found: %s.' % project_name)
+ # Issue
+ local_id = int(match.group('local_id'))
+ issue_pair = [(project_name, local_id)]
+ issue_id = _IssueIdsFromLocalIds(cnxn, issue_pair, services)[0]
+
+ return project_id, issue_id, int(match.group('comment_num'))
+
+
+def CreateCommentNames(issue_local_id, issue_project, comment_sequence_nums):
+ # type: (int, str, Sequence[int]) -> Mapping[int, str]
+ """Returns the resource names for the given comments.
+
+ Note: crbug.com/monorail/7507 has important context about guarantees required
+ for comment resource names to be permanent references.
+
+ Args:
+ issue_local_id: local id of the issue for which we're converting comments.
+ issue_project: the project of the issue for which we're converting comments.
+ comment_sequence_nums: sequence numbers of comments on the given issue.
+
+ Returns:
+ A mapping from comment sequence number to comment resource names.
+ """
+ sequence_nums_to_names = {}
+ for comment_sequence_num in comment_sequence_nums:
+ sequence_nums_to_names[comment_sequence_num] = COMMENT_NAME_TMPL.format(
+ project=issue_project,
+ local_id=issue_local_id,
+ comment_id=comment_sequence_num)
+ return sequence_nums_to_names
+
+def IngestApprovalDefName(cnxn, name, services):
+ # type: (MonorailConnection, str, Services) -> int
+ """Ingests an ApprovalDef's resource name.
+
+ Args:
+ cnxn: MonorailConnection to the database.
+ name: Resource name of an ApprovalDef.
+ services: Services object for connections to backend services.
+
+ Returns:
+ The ApprovalDef ID specified in `name`.
+ The ApprovalDef is not guaranteed to exist.
+
+ Raises:
+ InputException if the given name does not have a valid format.
+ NoSuchProjectException if the given project name does not exist.
+ """
+ match = _GetResourceNameMatch(name, APPROVAL_DEF_NAME_RE)
+
+ # Project
+ project_name = match.group('project_name')
+ id_dict = services.project.LookupProjectIDs(cnxn, [project_name])
+ project_id = id_dict.get(project_name)
+ if project_id is None:
+ raise exceptions.NoSuchProjectException(
+ 'Project not found: %s.' % project_name)
+
+ return int(match.group('approval_def'))
+
+def IngestApprovalValueName(cnxn, name, services):
+ # type: (MonorailConnection, str, Services) -> Tuple[int, int, int]
+ """Ingests the three components of an ApprovalValue resource name.
+
+ Args:
+ cnxn: MonorailConnection object.
+ name: Resource name of an ApprovalValue.
+ services: Services object for connections to backend services.
+
+ Returns:
+ Tuple containing three items
+ 1. Global ID of the parent project.
+ 2. Global Issue ID of the parent issue.
+ 3. The approval_id portion of the resource name. This is not checked
+ for existence.
+
+ Raises:
+ InputException if the given name does not have a valid format.
+ NoSuchIssueException if the parent Issue does not exist.
+ NoSuchProjectException if the parent Project does not exist.
+ """
+ match = _GetResourceNameMatch(name, APPROVAL_VALUE_RE)
+
+ # Project
+ project_name = match.group('project')
+ id_dict = services.project.LookupProjectIDs(cnxn, [project_name])
+ project_id = id_dict.get(project_name)
+ if project_id is None:
+ raise exceptions.NoSuchProjectException(
+ 'Project not found: %s.' % project_name)
+ # Issue
+ local_id = int(match.group('local_id'))
+ issue_pair = [(project_name, local_id)]
+ issue_id = _IssueIdsFromLocalIds(cnxn, issue_pair, services)[0]
+
+ return project_id, issue_id, int(match.group('approval_id'))
+
+
+def IngestIssueName(cnxn, name, services):
+ # type: (MonorailConnection, str, Services) -> int
+ """Takes an Issue resource name and returns its global ID.
+
+ Args:
+ cnxn: MonorailConnection object.
+ name: Resource name of an Issue.
+ services: Services object for connections to backend services.
+
+ Returns:
+ The global Issue ID associated with the name.
+
+ Raises:
+ InputException if the given name does not have a valid format.
+ NoSuchIssueException if the Issue does not exist.
+ NoSuchProjectException if an Issue's Project is not found.
+
+ """
+ return IngestIssueNames(cnxn, [name], services)[0]
+
+
+def IngestIssueNames(cnxn, names, services):
+ # type: (MonorailConnection, Sequence[str], Services) -> Sequence[int]
+ """Returns global IDs for the given Issue resource names.
+
+ Args:
+ cnxn: MonorailConnection object.
+ names: Resource names of zero or more issues.
+ services: Services object for connections to backend services.
+
+ Returns:
+ The global IDs for the issues.
+
+ Raises:
+ InputException if a resource name does not have a valid format.
+ NoSuchIssueException if an Issue is not found.
+ NoSuchProjectException if an Issue's Project is not found.
+ """
+ project_local_id_pairs = []
+ with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
+ for name in names:
+ try:
+ match = _GetResourceNameMatch(name, ISSUE_NAME_RE)
+ project_local_id_pairs.append(
+ (match.group('project'), int(match.group('local_id'))))
+ except exceptions.InputException as e:
+ err_agg.AddErrorMessage(e.message)
+ return _IssueIdsFromLocalIds(cnxn, project_local_id_pairs, services)
+
+
+def IngestProjectFromIssue(issue_name):
+ # type: (str) -> str
+ """Takes an issue resource_name and returns its project name.
+
+ TODO(crbug/monorail/7614): This method should only be needed for the
+ workaround for the referenced issue. When the cleanup is completed, this
+ method should be able to be removed.
+
+ Args:
+ issue_name: A resource name for an issue.
+
+ Returns:
+ The project section of the resource name (e.g for 'projects/xyz/issue/1'),
+ the method would return 'xyz'. The associated project is not guaranteed to
+ exist.
+
+ Raises:
+ InputException if 'issue_name' does not have a valid format.
+ """
+ match = _GetResourceNameMatch(issue_name, ISSUE_NAME_RE)
+ return match.group('project')
+
+
+def ConvertIssueName(cnxn, issue_id, services):
+ # type: (MonorailConnection, int, Services) -> str
+ """Takes an Issue ID and returns the corresponding Issue resource name.
+
+ Args:
+ cnxn: MonorailConnection object.
+ issue_id: The ID of the issue.
+ services: Services object.
+
+ Returns:
+ The resource name of the Issue.
+
+ Raises:
+ NoSuchIssueException if the issue is not found.
+ """
+ name = ConvertIssueNames(cnxn, [issue_id], services).get(issue_id)
+ if not name:
+ raise exceptions.NoSuchIssueException()
+ return name
+
+
+def ConvertIssueNames(cnxn, issue_ids, services):
+ # type: (MonorailConnection, Collection[int], Services) -> Mapping[int, str]
+ """Takes Issue IDs and returns the Issue resource names.
+
+ Args:
+ cnxn: MonorailConnection object.
+ issue_ids: List of Issue IDs
+ services: Services object.
+
+ Returns:
+ Dict of Issue IDs to Issue resource names for Issues that are found.
+ """
+ issue_ids_to_names = {}
+ issue_refs_dict = services.issue.LookupIssueRefs(cnxn, issue_ids)
+ for issue_id in issue_ids:
+ project, local_id = issue_refs_dict.get(issue_id, (None, None))
+ if project and local_id:
+ issue_ids_to_names[issue_id] = _ConstructIssueName(project, local_id)
+ return issue_ids_to_names
+
+
+def _ConstructIssueName(project, local_id):
+ # type: (str, int) -> str
+ """Takes project name and issue local id returns the Issue resource name."""
+ return ISSUE_NAME_TMPL.format(project=project, local_id=local_id)
+
+
+def ConvertApprovalValueNames(cnxn, issue_id, services):
+ # type: (MonorailConnection, int, Services)
+ # -> Mapping[int, str]
+ """Takes an Issue ID and returns the resource names of its ApprovalValues.
+
+ Args:
+ cnxn: MonorailConnection object.
+ issue_id: ID of the Issue the approval_values belong to.
+ services: Services object.
+
+ Returns:
+ Dict of ApprovalDef IDs to ApprovalValue resource names for
+ ApprovalDefs that are found.
+
+ Raises:
+ NoSuchIssueException if the Issue is not found.
+ """
+ issue = services.issue.GetIssue(cnxn, issue_id)
+ project = services.project.GetProject(cnxn, issue.project_id)
+ config = services.config.GetProjectConfig(cnxn, issue.project_id)
+
+ ads_by_id = {fd.field_id: fd for fd in config.field_defs
+ if fd.field_type is tracker_pb2.FieldTypes.APPROVAL_TYPE}
+
+ approval_def_ids = [av.approval_id for av in issue.approval_values]
+ approval_ids_to_names = {}
+ for ad_id in approval_def_ids:
+ fd = ads_by_id.get(ad_id)
+ if not fd:
+ logging.info('Approval type field with id %d not found.', ad_id)
+ continue
+ approval_ids_to_names[ad_id] = APPROVAL_VALUE_NAME_TMPL.format(
+ project=project.project_name,
+ local_id=issue.local_id,
+ approval_id=ad_id)
+ return approval_ids_to_names
+
+# Users
+
+
+def IngestUserName(cnxn, name, services, autocreate=False):
+ # type: (MonorailConnection, str, Services) -> int
+ """Takes a User resource name and returns a User ID.
+
+ Args:
+ cnxn: MonorailConnection object.
+ name: The User resource name.
+ services: Services object.
+ autocreate: set to True if new Users should be created for
+ emails in resource names that do not belong to existing
+ Users.
+
+ Returns:
+ The ID of the User.
+
+ Raises:
+ InputException if the resource name does not have a valid format.
+ NoSuchUserException if autocreate is False and the given email
+ was not found.
+ """
+ match = _GetResourceNameMatch(name, USER_NAME_RE)
+ user_id = match.group('user_id')
+ if user_id:
+ return int(user_id)
+ elif validate.IsValidEmail(match.group('potential_email')):
+ return services.user.LookupUserID(
+ cnxn, match.group('potential_email'), autocreate=autocreate)
+ else:
+ raise exceptions.InputException(
+ 'Invalid email format found in User resource name: %s' % name)
+
+
+def IngestUserNames(cnxn, names, services, autocreate=False):
+ # Type: (MonorailConnection, Sequence[str], Services, Optional[bool]) ->
+ # Sequence[int]
+ """Takes User resource names and returns the User IDs.
+
+ Args:
+ cnxn: MonorailConnection object.
+ names: List of User resource names.
+ services: Services object.
+ autocreate: set to True if new Users should be created for
+ emails in resource names that do not belong to existing
+ Users.
+
+ Returns:
+ List of User IDs in the same order as names.
+
+ Raises:
+ InputException if an resource name does not have a valid format.
+ NoSuchUserException if autocreate is False and some users with given
+ emails were not found.
+ """
+ ids = []
+ for name in names:
+ ids.append(IngestUserName(cnxn, name, services, autocreate))
+
+ return ids
+
+
+def ConvertUserName(user_id):
+ # type: (int) -> str
+ """Takes a User ID and returns the User's resource name."""
+ return ConvertUserNames([user_id])[user_id]
+
+
+def ConvertUserNames(user_ids):
+ # type: (Collection[int]) -> Mapping[int, str]
+ """Takes User IDs and returns the Users' resource names.
+
+ Args:
+ user_ids: List of User IDs.
+
+ Returns:
+ Dict of User IDs to User resource names for all given user_ids.
+ """
+ user_ids_to_names = {}
+ for user_id in user_ids:
+ user_ids_to_names[user_id] = USER_NAME_TMPL.format(user_id=user_id)
+
+ return user_ids_to_names
+
+
+def ConvertProjectStarName(cnxn, user_id, project_id, services):
+ # type: (MonorailConnection, int, int, Services) -> str
+ """Takes User ID and Project ID and returns the ProjectStar resource name.
+
+ Args:
+ user_id: User ID associated with the star.
+ project_id: ID of the starred project.
+
+ Returns:
+ The ProjectStar's name.
+
+ Raises:
+ NoSuchProjectException if the project_id is not found.
+ """
+ project_name = services.project.LookupProjectNames(
+ cnxn, [project_id]).get(project_id)
+
+ return PROJECT_STAR_NAME_TMPL.format(
+ user_id=user_id, project_name=project_name)
+
+# Projects
+
+
+def IngestProjectName(cnxn, name, services):
+ # type: (str) -> int
+ """Takes a Project resource name and returns the project id.
+
+ Args:
+ name: Resource name of a Project.
+
+ Returns:
+ The project's id
+
+ Raises:
+ InputException if the given name does not have a valid format.
+ NoSuchProjectException if no project exists with the given name.
+ """
+ match = _GetResourceNameMatch(name, PROJECT_NAME_RE)
+ project_name = match.group('project_name')
+
+ id_dict = services.project.LookupProjectIDs(cnxn, [project_name])
+
+ return id_dict.get(project_name)
+
+
+def ConvertTemplateNames(cnxn, project_id, template_ids, services):
+ # type: (MonorailConnection, int, Collection[int] Services) ->
+ # Mapping[int, str]
+ """Takes Template IDs and returns the Templates' resource names
+
+ Args:
+ cnxn: MonorailConnection object.
+ project_id: Project ID of Project that Templates must belong to.
+ template_ids: Template IDs to convert.
+ services: Services object.
+
+ Returns:
+ Dict of template ID to template resource names for all found template IDs
+ within the given project.
+
+ Raises:
+ NoSuchProjectException if no project exists with given id.
+ """
+ id_to_resource_names = {}
+
+ project_name = services.project.LookupProjectNames(
+ cnxn, [project_id]).get(project_id)
+ project_templates = services.template.GetProjectTemplates(cnxn, project_id)
+ tmpl_by_id = {tmpl.template_id: tmpl for tmpl in project_templates}
+
+ for template_id in template_ids:
+ if template_id not in tmpl_by_id:
+ logging.info(
+ 'Ignoring template referencing a non-existent id: %s, ' \
+ 'or not in project: %s', template_id, project_id)
+ continue
+ id_to_resource_names[template_id] = ISSUE_TEMPLATE_TMPL.format(
+ project_name=project_name,
+ template_id=template_id)
+
+ return id_to_resource_names
+
+
+def IngestTemplateName(cnxn, name, services):
+ # type: (MonorailConnection, str, Services) -> Tuple[int, int]
+ """Ingests an IssueTemplate resource name.
+
+ Args:
+ cnxn: MonorailConnection object.
+ name: Resource name of an IssueTemplate.
+ services: Services object.
+
+ Returns:
+ The IssueTemplate's ID and the Project's ID.
+
+ Raises:
+ InputException if the given name does not have a valid format.
+ NoSuchProjectException if the given project name does not exist.
+ """
+ match = _GetResourceNameMatch(name, ISSUE_TEMPLATE_RE)
+ template_id = int(match.group('template_id'))
+ project_name = match.group('project_name')
+
+ id_dict = services.project.LookupProjectIDs(cnxn, [project_name])
+ project_id = id_dict.get(project_name)
+ if project_id is None:
+ raise exceptions.NoSuchProjectException(
+ 'Project not found: %s.' % project_name)
+ return template_id, project_id
+
+
+def ConvertStatusDefNames(cnxn, statuses, project_id, services):
+ # type: (MonorailConnection, Collection[str], int, Services) ->
+ # Mapping[str, str]
+ """Takes list of status strings and returns StatusDef resource names
+
+ Args:
+ cnxn: MonorailConnection object.
+ statuses: List of status name strings
+ project_id: project id of project this belongs to
+ services: Services object.
+
+ Returns:
+ Mapping of string to resource name for all given `statuses`.
+
+ Raises:
+ NoSuchProjectException if no project exists with given id.
+ """
+ project = services.project.GetProject(cnxn, project_id)
+
+ name_dict = {}
+ for status in statuses:
+ name_dict[status] = STATUS_DEF_TMPL.format(
+ project_name=project.project_name, status=status)
+
+ return name_dict
+
+
+def ConvertLabelDefNames(cnxn, labels, project_id, services):
+ # type: (MonorailConnection, Collection[str], int, Services) ->
+ # Mapping[str, str]
+ """Takes a list of labels and returns LabelDef resource names
+
+ Args:
+ cnxn: MonorailConnection object.
+ labels: List of labels as string
+ project_id: project id of project this belongs to
+ services: Services object.
+
+ Returns:
+ Dict of label string to label's resource name for all given `labels`.
+
+ Raises:
+ NoSuchProjectException if no project exists with given id.
+ """
+ project = services.project.GetProject(cnxn, project_id)
+
+ name_dict = {}
+
+ for label in labels:
+ name_dict[label] = LABEL_DEF_TMPL.format(
+ project_name=project.project_name, label=label)
+
+ return name_dict
+
+
+def ConvertComponentDefNames(cnxn, component_ids, project_id, services):
+ # type: (MonorailConnection, Collection[int], int, Services) ->
+ # Mapping[int, str]
+ """Takes Component IDs and returns ComponentDef resource names
+
+ Args:
+ cnxn: MonorailConnection object.
+ component_ids: List of component ids
+ project_id: project id of project this belongs to
+ services: Services object.
+
+ Returns:
+ Dict of component ID to component's resource name for all given
+ `component_ids`
+
+ Raises:
+ NoSuchProjectException if no project exists with given id.
+ """
+ project = services.project.GetProject(cnxn, project_id)
+
+ id_dict = {}
+
+ for component_id in component_ids:
+ id_dict[component_id] = COMPONENT_DEF_TMPL.format(
+ project_name=project.project_name, component_id=component_id)
+
+ return id_dict
+
+
+def IngestComponentDefNames(cnxn, names, services):
+ # type: (MonorailConnection, Sequence[str], Services)
+ # -> Sequence[Tuple[int, int]]
+ """Takes a list of component resource names and returns their IDs.
+
+ Args:
+ cnxn: MonorailConnection object.
+ names: List of component resource names.
+ services: Services object.
+
+ Returns:
+ List of (project ID, component ID)s in the same order as names.
+
+ Raises:
+ InputException if a resource name does not have a valid format.
+ NoSuchProjectException if no project exists with given id.
+ NoSuchComponentException if a component is not found.
+ """
+ # Parse as many (component id or path, project name) pairs as possible.
+ parsed_comp_projectnames = []
+ with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
+ for name in names:
+ try:
+ match = _GetResourceNameMatch(name, COMPONENT_DEF_RE)
+ project_name = match.group('project_name')
+ component_id = match.group('component_id')
+ if component_id:
+ parsed_comp_projectnames.append((int(component_id), project_name))
+ else:
+ parsed_comp_projectnames.append(
+ (str(match.group('path')), project_name))
+ except exceptions.InputException as e:
+ err_agg.AddErrorMessage(e.message)
+
+ # Validate as many projects as possible.
+ project_names = {project_name for _, project_name in parsed_comp_projectnames}
+ project_ids_by_name = services.project.LookupProjectIDs(cnxn, project_names)
+ with exceptions.ErrorAggregator(exceptions.NoSuchProjectException) as err_agg:
+ for _, project_name in parsed_comp_projectnames:
+ if project_name not in project_ids_by_name:
+ err_agg.AddErrorMessage('Project not found: %s.' % project_name)
+
+ configs_by_pid = services.config.GetProjectConfigs(
+ cnxn, project_ids_by_name.values())
+ compid_by_pid = {}
+ comp_path_by_pid = {}
+ for pid, config in configs_by_pid.items():
+ compid_by_pid[pid] = {comp.component_id for comp in config.component_defs}
+ comp_path_by_pid[pid] = {
+ comp.path.lower(): comp.component_id for comp in config.component_defs
+ }
+
+ # Find as many components as possible
+ pid_cid_pairs = []
+ with exceptions.ErrorAggregator(
+ exceptions.NoSuchComponentException) as err_agg:
+ for comp, pname in parsed_comp_projectnames:
+ pid = project_ids_by_name[pname]
+ if isinstance(comp, int) and comp in compid_by_pid[pid]:
+ pid_cid_pairs.append((pid, comp))
+ elif isinstance(comp, str) and comp.lower() in comp_path_by_pid[pid]:
+ pid_cid_pairs.append((pid, comp_path_by_pid[pid][comp.lower()]))
+ else:
+ err_agg.AddErrorMessage('Component not found: %r.' % comp)
+
+ return pid_cid_pairs
+
+
+def ConvertFieldDefNames(cnxn, field_ids, project_id, services):
+ # type: (MonorailConnection, Collection[int], int, Services) ->
+ # Mapping[int, str]
+ """Takes Field IDs and returns FieldDef resource names.
+
+ Args:
+ cnxn: MonorailConnection object.
+ field_ids: List of Field IDs
+ project_id: project ID that each Field must belong to.
+ services: Services object.
+
+ Returns:
+ Dict of Field ID to FieldDef resource name for FieldDefs that are found.
+
+ Raises:
+ NoSuchProjectException if no project exists with given ID.
+ """
+ project = services.project.GetProject(cnxn, project_id)
+ config = services.config.GetProjectConfig(cnxn, project_id)
+
+ fds_by_id = {fd.field_id: fd for fd in config.field_defs}
+
+ id_dict = {}
+
+ for field_id in field_ids:
+ field_def = fds_by_id.get(field_id)
+ if not field_def:
+ logging.info('Ignoring field referencing a non-existent id: %s', field_id)
+ continue
+ id_dict[field_id] = FIELD_DEF_TMPL.format(
+ project_name=project.project_name, field_id=field_id)
+
+ return id_dict
+
+
+def ConvertApprovalDefNames(cnxn, approval_ids, project_id, services):
+ # type: (MonorailConnection, Collection[int], int, Services) ->
+ # Mapping[int, str]
+ """Takes Approval IDs and returns ApprovalDef resource names.
+
+ Args:
+ cnxn: MonorailConnection object.
+ approval_ids: List of Approval IDs.
+ project_id: Project ID these approvals must belong to.
+ services: Services object.
+
+ Returns:
+ Dict of Approval ID to ApprovalDef resource name for ApprovalDefs
+ that are found.
+
+ Raises:
+ NoSuchProjectException if no project exists with given ID.
+ """
+ project = services.project.GetProject(cnxn, project_id)
+ config = services.config.GetProjectConfig(cnxn, project_id)
+
+ fds_by_id = {fd.field_id: fd for fd in config.field_defs}
+
+ id_dict = {}
+
+ for approval_id in approval_ids:
+ approval_def = fds_by_id.get(approval_id)
+ if not approval_def:
+ logging.info(
+ 'Ignoring approval referencing a non-existent id: %s', approval_id)
+ continue
+ id_dict[approval_id] = APPROVAL_DEF_TMPL.format(
+ project_name=project.project_name, approval_id=approval_id)
+
+ return id_dict
+
+
+def ConvertProjectName(cnxn, project_id, services):
+ # type: (MonorailConnection, int, Services) -> str
+ """Takes a Project ID and returns the Project's resource name.
+
+ Args:
+ cnxn: MonorailConnection object.
+ project_id: ID of the Project.
+ services: Services object.
+
+ Returns:
+ The resource name of the Project.
+
+ Raises:
+ NoSuchProjectException if no project exists with given id.
+ """
+ project_name = services.project.LookupProjectNames(
+ cnxn, [project_id]).get(project_id)
+ return PROJECT_NAME_TMPL.format(project_name=project_name)
+
+
+def ConvertProjectConfigName(cnxn, project_id, services):
+ # type: (MonorailConnection, int, Services) -> str
+ """Takes a Project ID and returns that project's config resource name.
+
+ Args:
+ cnxn: MonorailConnection object.
+ project_id: ID of the Project.
+ services: Services object.
+
+ Returns:
+ The resource name of the ProjectConfig.
+
+ Raises:
+ NoSuchProjectException if no project exists with given id.
+ """
+ project_name = services.project.LookupProjectNames(
+ cnxn, [project_id]).get(project_id)
+ return PROJECT_CONFIG_TMPL.format(project_name=project_name)
+
+
+def ConvertProjectMemberName(cnxn, project_id, user_id, services):
+ # type: (MonorailConnection, int, int, Services) -> str
+ """Takes Project and User ID then returns the ProjectMember resource name.
+
+ Args:
+ cnxn: MonorailConnection object.
+ project_id: ID of the Project.
+ user_id: ID of the User.
+ services: Services object.
+
+ Returns:
+ The resource name of the ProjectMember.
+
+ Raises:
+ NoSuchProjectException if no project exists with given id.
+ """
+ project_name = services.project.LookupProjectNames(
+ cnxn, [project_id]).get(project_id)
+
+ return PROJECT_MEMBER_NAME_TMPL.format(
+ project_name=project_name, user_id=user_id)
+
+
+def ConvertProjectSavedQueryNames(cnxn, query_ids, project_id, services):
+ # type: (MonorailConnection, Collection[int], int, Services) ->
+ # Mapping[int, str]
+ """Takes SavedQuery IDs and returns ProjectSavedQuery resource names.
+
+ Args:
+ cnxn: MonorailConnection object.
+ query_ids: List of SavedQuery ids
+ project_id: project id of project this belongs to
+ services: Services object.
+
+ Returns:
+ Dict of ids to ProjectSavedQuery resource names for all found query ids
+ that belong to given project_id.
+
+ Raises:
+ NoSuchProjectException if no project exists with given id.
+ """
+ project_name = services.project.LookupProjectNames(
+ cnxn, [project_id]).get(project_id)
+ all_project_queries = services.features.GetCannedQueriesByProjectID(
+ cnxn, project_id)
+ query_by_ids = {query.query_id: query for query in all_project_queries}
+ ids_to_names = {}
+ for query_id in query_ids:
+ query = query_by_ids.get(query_id)
+ if not query:
+ logging.info(
+ 'Ignoring saved query referencing a non-existent id: %s '
+ 'or not in project: %s', query_id, project_id)
+ continue
+ ids_to_names[query_id] = PROJECT_SQ_NAME_TMPL.format(
+ project_name=project_name, query_name=query.name)
+ return ids_to_names
diff --git a/api/sitewide_servicer.py b/api/sitewide_servicer.py
new file mode 100644
index 0000000..b008e5d
--- /dev/null
+++ b/api/sitewide_servicer.py
@@ -0,0 +1,57 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+
+import settings
+from api import monorail_servicer
+from api.api_proto import sitewide_pb2
+from api.api_proto import sitewide_prpc_pb2
+from framework import servlet_helpers
+from framework import xsrf
+
+
+class SitewideServicer(monorail_servicer.MonorailServicer):
+ """Handle API requests related to sitewide operations.
+
+ Each API request is implemented with a method as defined in the .proto
+ file that does any request-specific validation, uses work_env to
+ safely operate on business objects, and returns a response proto.
+ """
+
+ DESCRIPTION = sitewide_prpc_pb2.SitewideServiceDescription
+
+ def __init__(self, services, make_rate_limiter=True):
+ # It might be that the token we're asked to refresh is the same one we are
+ # using to authenticate. So we should use a longer timeout
+ # (xsrf.REFRESH_TOKEN_TIMEOUT_SEC) when checking the XSRF
+ super(SitewideServicer, self).__init__(
+ services, make_rate_limiter, xsrf.REFRESH_TOKEN_TIMEOUT_SEC)
+
+ @monorail_servicer.PRPCMethod
+ def RefreshToken(self, mc, request):
+ """Return a new token."""
+ # Validate that the token we're asked to refresh would still be valid with a
+ # longer timeout.
+ xsrf.ValidateToken(
+ request.token, mc.auth.user_id, request.token_path,
+ timeout=xsrf.REFRESH_TOKEN_TIMEOUT_SEC)
+
+ result = sitewide_pb2.RefreshTokenResponse(
+ token=xsrf.GenerateToken(mc.auth.user_id, request.token_path),
+ token_expires_sec=xsrf.TokenExpiresSec())
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def GetServerStatus(self, _mc, _request):
+ result = sitewide_pb2.GetServerStatusResponse(
+ banner_message=settings.banner_message,
+ banner_time=servlet_helpers.GetBannerTime(settings.banner_time),
+ read_only=settings.read_only)
+ return result
diff --git a/api/test/__init__.py b/api/test/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/api/test/__init__.py
diff --git a/api/test/converters_test.py b/api/test/converters_test.py
new file mode 100644
index 0000000..e193423
--- /dev/null
+++ b/api/test/converters_test.py
@@ -0,0 +1,2222 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for converting internal protorpc to external protoc."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from mock import Mock, patch
+import unittest
+
+from google.protobuf import wrappers_pb2
+
+import settings
+from api import converters
+from api.api_proto import common_pb2
+from api.api_proto import features_objects_pb2
+from api.api_proto import issue_objects_pb2
+from api.api_proto import project_objects_pb2
+from api.api_proto import user_objects_pb2
+from framework import exceptions
+from framework import permissions
+from proto import tracker_pb2
+from proto import user_pb2
+from testing import fake
+from testing import testing_helpers
+from tracker import tracker_bizobj
+from services import features_svc
+from services import service_manager
+
+
+class ConverterFunctionsTest(unittest.TestCase):
+
+ NOW = 1234567890
+
+ def setUp(self):
+ self.users_by_id = {
+ 111: testing_helpers.Blank(
+ display_name='one@example.com', email='one@example.com',
+ banned=False),
+ 222: testing_helpers.Blank(
+ display_name='two@example.com', email='two@example.com',
+ banned=False),
+ 333: testing_helpers.Blank(
+ display_name='ban...@example.com', email='banned@example.com',
+ banned=True),
+ }
+
+ self.services = service_manager.Services(
+ issue=fake.IssueService(),
+ project=fake.ProjectService(),
+ user=fake.UserService(),
+ features=fake.FeaturesService())
+ self.cnxn = fake.MonorailConnection()
+ self.project = self.services.project.TestAddProject(
+ 'proj', project_id=789)
+ self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+
+ self.fd_1 = tracker_pb2.FieldDef(
+ field_name='FirstField', field_id=1,
+ field_type=tracker_pb2.FieldTypes.STR_TYPE,
+ applicable_type='')
+ self.fd_2 = tracker_pb2.FieldDef(
+ field_name='SecField', field_id=2,
+ field_type=tracker_pb2.FieldTypes.INT_TYPE,
+ applicable_type='')
+ self.fd_3 = tracker_pb2.FieldDef(
+ field_name='LegalApproval', field_id=3,
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ applicable_type='')
+ self.fd_4 = tracker_pb2.FieldDef(
+ field_name='UserField', field_id=4,
+ field_type=tracker_pb2.FieldTypes.USER_TYPE,
+ applicable_type='')
+ self.fd_5 = tracker_pb2.FieldDef(
+ field_name='Pre', field_id=5,
+ field_type=tracker_pb2.FieldTypes.ENUM_TYPE,
+ applicable_type='')
+ self.fd_6 = tracker_pb2.FieldDef(
+ field_name='PhaseField', field_id=6,
+ field_type=tracker_pb2.FieldTypes.INT_TYPE,
+ applicable_type='', is_phase_field=True)
+ self.fd_7 = tracker_pb2.FieldDef(
+ field_name='ApprovalEnum', field_id=7,
+ field_type=tracker_pb2.FieldTypes.ENUM_TYPE,
+ applicable_type='', approval_id=self.fd_3.field_id)
+
+ self.user_1 = self.services.user.TestAddUser('one@example.com', 111)
+ self.user_2 = self.services.user.TestAddUser('two@example.com', 222)
+ self.user_3 = self.services.user.TestAddUser('banned@example.com', 333)
+ self.issue_1 = fake.MakeTestIssue(
+ 789, 1, 'sum', 'New', 111, project_name='proj')
+ self.issue_2 = fake.MakeTestIssue(
+ 789, 2, 'sum', 'New', 111, project_name='proj')
+ self.services.issue.TestAddIssue(self.issue_1)
+ self.services.issue.TestAddIssue(self.issue_2)
+
+ def testConvertApprovalValues_Empty(self):
+ """We handle the case where an issue has no approval values."""
+ actual = converters.ConvertApprovalValues([], [], {}, self.config)
+ self.assertEqual([], actual)
+
+ def testConvertApprovalValues_Normal(self):
+ """We can convert a list of approval values."""
+ now = 1234567890
+ self.config.field_defs.append(tracker_pb2.FieldDef(
+ field_id=1, project_id=789, field_name='EstDays',
+ field_type=tracker_pb2.FieldTypes.INT_TYPE,
+ applicable_type=''))
+ self.config.field_defs.append(tracker_pb2.FieldDef(
+ field_id=11, project_id=789, field_name='Accessibility',
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ applicable_type='Launch'))
+ self.config.approval_defs.append(tracker_pb2.ApprovalDef(
+ approval_id=11, approver_ids=[111], survey='survey 1'))
+ self.config.approval_defs.append(tracker_pb2.ApprovalDef(
+ approval_id=12, approver_ids=[111], survey='survey 2'))
+ av_11 = tracker_pb2.ApprovalValue(
+ approval_id=11, status=tracker_pb2.ApprovalStatus.NEED_INFO,
+ setter_id=111, set_on=now, approver_ids=[111, 222],
+ phase_id=21)
+ # Note: no approval def, no phase, so it won't be returned.
+ # TODO(ehmaldonado): Figure out support for "foreign" fields.
+ av_12 = tracker_pb2.ApprovalValue(
+ approval_id=12, status=tracker_pb2.ApprovalStatus.NOT_SET,
+ setter_id=111, set_on=now, approver_ids=[111])
+ phase_21 = tracker_pb2.Phase(phase_id=21, name='Stable', rank=1)
+ actual = converters.ConvertApprovalValues(
+ [av_11, av_12], [phase_21], self.users_by_id, self.config)
+
+ expected_av_1 = issue_objects_pb2.Approval(
+ field_ref=common_pb2.FieldRef(
+ field_id=11,
+ field_name='Accessibility',
+ type=common_pb2.APPROVAL_TYPE),
+ approver_refs=[
+ common_pb2.UserRef(user_id=111, display_name='one@example.com'),
+ common_pb2.UserRef(user_id=222, display_name='two@example.com'),
+ ],
+ status=issue_objects_pb2.NEED_INFO,
+ set_on=now,
+ setter_ref=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Stable'))
+
+ self.assertEqual([expected_av_1], actual)
+
+ def testConvertApproval(self):
+ """We can convert ApprovalValues to protoc Approvals."""
+ approval_value = tracker_pb2.ApprovalValue(
+ approval_id=3,
+ status=tracker_pb2.ApprovalStatus.NEED_INFO,
+ setter_id=222,
+ set_on=2345,
+ approver_ids=[111],
+ phase_id=1
+ )
+
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_3]
+
+ phase = tracker_pb2.Phase(phase_id=1, name='Canary')
+
+ actual = converters.ConvertApproval(
+ approval_value, self.users_by_id, self.config, phase=phase)
+ expected = issue_objects_pb2.Approval(
+ field_ref=common_pb2.FieldRef(
+ field_id=3,
+ field_name='LegalApproval',
+ type=common_pb2.APPROVAL_TYPE),
+ approver_refs=[common_pb2.UserRef(
+ user_id=111, display_name='one@example.com', is_derived=False)
+ ],
+ status=5,
+ set_on=2345,
+ setter_ref=common_pb2.UserRef(
+ user_id=222, display_name='two@example.com', is_derived=False
+ ),
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Canary')
+ )
+
+ self.assertEqual(expected, actual)
+
+ def testConvertApproval_NonExistentApproval(self):
+ approval_value = tracker_pb2.ApprovalValue(
+ approval_id=3,
+ status=tracker_pb2.ApprovalStatus.NEED_INFO,
+ setter_id=222,
+ set_on=2345,
+ approver_ids=[111],
+ phase_id=1
+ )
+ phase = tracker_pb2.Phase(phase_id=1, name='Canary')
+ self.assertIsNone(converters.ConvertApproval(
+ approval_value, self.users_by_id, self.config, phase=phase))
+
+
+ def testConvertApprovalStatus(self):
+ """We can convert a protorpc ApprovalStatus to a protoc ApprovalStatus."""
+ actual = converters.ConvertApprovalStatus(
+ tracker_pb2.ApprovalStatus.REVIEW_REQUESTED)
+ self.assertEqual(actual, issue_objects_pb2.REVIEW_REQUESTED)
+
+ actual = converters.ConvertApprovalStatus(
+ tracker_pb2.ApprovalStatus.NOT_SET)
+ self.assertEqual(actual, issue_objects_pb2.NOT_SET)
+
+ def testConvertUserRef(self):
+ """We can convert user IDs to a UserRef."""
+ # No specified user
+ actual = converters.ConvertUserRef(None, None, self.users_by_id)
+ expected = None
+ self.assertEqual(expected, actual)
+
+ # Explicitly specified user
+ actual = converters.ConvertUserRef(111, None, self.users_by_id)
+ expected = common_pb2.UserRef(
+ user_id=111, is_derived=False, display_name='one@example.com')
+ self.assertEqual(expected, actual)
+
+ # Derived user
+ actual = converters.ConvertUserRef(None, 111, self.users_by_id)
+ expected = common_pb2.UserRef(
+ user_id=111, is_derived=True, display_name='one@example.com')
+ self.assertEqual(expected, actual)
+
+ def testConvertUserRefs(self):
+ """We can convert lists of user_ids into UserRefs."""
+ # No specified users
+ actual = converters.ConvertUserRefs(
+ [], [], self.users_by_id, False)
+ expected = []
+ self.assertEqual(expected, actual)
+
+ # A mix of explicit and derived users
+ actual = converters.ConvertUserRefs(
+ [111], [222], self.users_by_id, False)
+ expected = [
+ common_pb2.UserRef(
+ user_id=111, is_derived=False, display_name='one@example.com'),
+ common_pb2.UserRef(
+ user_id=222, is_derived=True, display_name='two@example.com'),
+ ]
+ self.assertEqual(expected, actual)
+
+ # Use display name
+ actual = converters.ConvertUserRefs([333], [], self.users_by_id, False)
+ self.assertEqual(
+ [common_pb2.UserRef(
+ user_id=333, is_derived=False, display_name='ban...@example.com')],
+ actual)
+
+ # Use email
+ actual = converters.ConvertUserRefs([333], [], self.users_by_id, True)
+ self.assertEqual(
+ [common_pb2.UserRef(
+ user_id=333, is_derived=False, display_name='banned@example.com')],
+ actual)
+
+ @patch('time.time')
+ def testConvertUsers(self, mock_time):
+ """We can convert lists of protorpc Users to protoc Users."""
+ mock_time.return_value = self.NOW
+ user1 = user_pb2.User(
+ user_id=1, email='user1@example.com', last_visit_timestamp=self.NOW)
+ user2 = user_pb2.User(
+ user_id=2, email='user2@example.com', is_site_admin=True,
+ last_visit_timestamp=self.NOW)
+ user3 = user_pb2.User(
+ user_id=3, email='user3@example.com',
+ linked_child_ids=[4])
+ user4 = user_pb2.User(
+ user_id=4, email='user4@example.com', last_visit_timestamp=1,
+ linked_parent_id=3)
+ users_by_id = {
+ 3: testing_helpers.Blank(
+ display_name='user3@example.com', email='user3@example.com',
+ banned=False),
+ 4: testing_helpers.Blank(
+ display_name='user4@example.com', email='user4@example.com',
+ banned=False),
+ }
+
+ actual = converters.ConvertUsers(
+ [user1, user2, user3, user4], users_by_id)
+ self.assertItemsEqual(
+ actual,
+ [user_objects_pb2.User(
+ user_id=1,
+ display_name='user1@example.com'),
+ user_objects_pb2.User(
+ user_id=2,
+ display_name='user2@example.com',
+ is_site_admin=True),
+ user_objects_pb2.User(
+ user_id=3,
+ display_name='user3@example.com',
+ availability='User never visited',
+ linked_child_refs=[common_pb2.UserRef(
+ user_id=4, display_name='user4@example.com')]),
+ user_objects_pb2.User(
+ user_id=4,
+ display_name='user4@example.com',
+ availability='Last visit > 30 days ago',
+ linked_parent_ref=common_pb2.UserRef(
+ user_id=3, display_name='user3@example.com')),
+ ])
+
+ def testConvetPrefValues(self):
+ """We can convert a list of UserPrefValues from protorpc to protoc."""
+ self.assertEqual(
+ [],
+ converters.ConvertPrefValues([]))
+
+ userprefvalues = [
+ user_pb2.UserPrefValue(name='foo_1', value='bar_1'),
+ user_pb2.UserPrefValue(name='foo_2', value='bar_2')]
+ actual = converters.ConvertPrefValues(userprefvalues)
+ expected = [
+ user_objects_pb2.UserPrefValue(name='foo_1', value='bar_1'),
+ user_objects_pb2.UserPrefValue(name='foo_2', value='bar_2')]
+ self.assertEqual(expected, actual)
+
+ def testConvertLabels(self):
+ """We can convert labels."""
+ # No labels specified
+ actual = converters.ConvertLabels([], [])
+ self.assertEqual([], actual)
+
+ # A mix of explicit and derived labels
+ actual = converters.ConvertLabels(
+ ['Milestone-66'], ['Restrict-View-CoreTeam'])
+ expected = [
+ common_pb2.LabelRef(label='Milestone-66', is_derived=False),
+ common_pb2.LabelRef(label='Restrict-View-CoreTeam', is_derived=True),
+ ]
+ self.assertEqual(expected, actual)
+
+ def testConvertComponentRef(self):
+ """We can convert a component ref."""
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI'),
+ tracker_pb2.ComponentDef(component_id=2, path='DB')]
+
+ self.assertEqual(
+ common_pb2.ComponentRef(
+ path='UI',
+ is_derived=False),
+ converters.ConvertComponentRef(1, self.config))
+
+ self.assertEqual(
+ common_pb2.ComponentRef(
+ path='DB',
+ is_derived=True),
+ converters.ConvertComponentRef(2, self.config, True))
+
+ self.assertIsNone(
+ converters.ConvertComponentRef(3, self.config, True))
+
+ def testConvertComponents(self):
+ """We can convert a list of components."""
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI'),
+ tracker_pb2.ComponentDef(component_id=2, path='DB'),
+ ]
+
+ # No components specified
+ actual = converters.ConvertComponents([], [], self.config)
+ self.assertEqual([], actual)
+
+ # A mix of explicit, derived, and non-existing components
+ actual = converters.ConvertComponents([1, 4], [2, 3], self.config)
+ expected = [
+ common_pb2.ComponentRef(path='UI', is_derived=False),
+ common_pb2.ComponentRef(path='DB', is_derived=True),
+ ]
+ self.assertEqual(expected, actual)
+
+ def testConvertIssueRef(self):
+ """We can convert a pair (project_name, local_id) to an IssueRef."""
+ actual = converters.ConvertIssueRef(('proj', 1))
+ self.assertEqual(
+ common_pb2.IssueRef(project_name='proj', local_id=1),
+ actual)
+
+ def testConvertIssueRef_ExtIssue(self):
+ """ConvertIssueRef successfully converts an external issue."""
+ actual = converters.ConvertIssueRef(('', 0), ext_id='b/1234567')
+ self.assertEqual(
+ common_pb2.IssueRef(project_name='', local_id=0,
+ ext_identifier='b/1234567'),
+ actual)
+
+ def testConvertIssueRefs(self):
+ """We can convert issue_ids to IssueRefs."""
+ related_refs_dict = {
+ 78901: ('proj', 1),
+ 78902: ('proj', 2),
+ }
+ actual = converters.ConvertIssueRefs([78901, 78902], related_refs_dict)
+ self.assertEqual(
+ [common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='proj', local_id=2)],
+ actual)
+
+ def testConvertFieldType(self):
+ self.assertEqual(
+ common_pb2.STR_TYPE,
+ converters.ConvertFieldType(tracker_pb2.FieldTypes.STR_TYPE))
+
+ self.assertEqual(
+ common_pb2.URL_TYPE,
+ converters.ConvertFieldType(tracker_pb2.FieldTypes.URL_TYPE))
+
+ def testConvertFieldRef(self):
+ actual = converters.ConvertFieldRef(
+ 1, 'SomeName', tracker_pb2.FieldTypes.ENUM_TYPE, None)
+ self.assertEqual(
+ actual,
+ common_pb2.FieldRef(
+ field_id=1,
+ field_name='SomeName',
+ type=common_pb2.ENUM_TYPE))
+
+ def testConvertFieldValue(self):
+ """We can convert one FieldValueView item to a protoc FieldValue."""
+ actual = converters.ConvertFieldValue(
+ 1, 'Size', 123, tracker_pb2.FieldTypes.INT_TYPE, phase_name='Canary')
+ expected = issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=1,
+ field_name='Size',
+ type=common_pb2.INT_TYPE),
+ value='123',
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Canary'))
+ self.assertEqual(expected, actual)
+
+ actual = converters.ConvertFieldValue(
+ 1, 'Size', 123, tracker_pb2.FieldTypes.INT_TYPE, 'Legal', '',
+ is_derived=True)
+ expected = issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=1,
+ field_name='Size',
+ type=common_pb2.INT_TYPE,
+ approval_name='Legal'),
+ value='123',
+ is_derived=True)
+ self.assertEqual(expected, actual)
+
+ def testConvertFieldValue_Unicode(self):
+ """We can convert one FieldValueView unicode item to a protoc FieldValue."""
+ actual = converters.ConvertFieldValue(
+ 1, 'Size', u'\xe2\x9d\xa4\xef\xb8\x8f',
+ tracker_pb2.FieldTypes.STR_TYPE, phase_name='Canary')
+ expected = issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=1,
+ field_name='Size',
+ type=common_pb2.STR_TYPE),
+ value=u'\xe2\x9d\xa4\xef\xb8\x8f',
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Canary'))
+ self.assertEqual(expected, actual)
+
+ def testConvertFieldValues(self):
+ self.fd_2.approval_id = 3
+ self.config.field_defs = [
+ self.fd_1, self.fd_2, self.fd_3, self.fd_4, self.fd_5]
+ fv_1 = tracker_bizobj.MakeFieldValue(
+ 1, None, 'string', None, None, None, False)
+ fv_2 = tracker_bizobj.MakeFieldValue(
+ 2, 34, None, None, None, None, False)
+ fv_3 = tracker_bizobj.MakeFieldValue(
+ 111, None, 'value', None, None, None, False)
+ labels = ['Pre-label', 'not-label-enum', 'prenot-label']
+ der_labels = ['Pre-label2']
+ phases = [tracker_pb2.Phase(name='Canary', phase_id=17)]
+ fv_1.phase_id=17
+
+ actual = converters.ConvertFieldValues(
+ self.config, labels, der_labels, [fv_1, fv_2, fv_3], {}, phases=phases)
+
+ self.maxDiff = None
+ expected = [
+ issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=1,
+ field_name='FirstField',
+ type=common_pb2.STR_TYPE),
+ value='string',
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Canary')),
+ issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=2,
+ field_name='SecField',
+ type=common_pb2.INT_TYPE,
+ approval_name='LegalApproval'),
+ value='34'),
+ issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=5, field_name='Pre', type=common_pb2.ENUM_TYPE),
+ value='label'),
+ issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=5, field_name='Pre', type=common_pb2.ENUM_TYPE),
+ value='label2', is_derived=True),
+ ]
+ self.assertItemsEqual(expected, actual)
+
+ def testConvertIssue(self):
+ """We can convert a protorpc Issue to a protoc Issue."""
+ related_refs_dict = {
+ 78901: ('proj', 1),
+ 78902: ('proj', 2),
+ }
+ now = 12345678
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI'),
+ tracker_pb2.ComponentDef(component_id=2, path='DB'),
+ ]
+ issue = fake.MakeTestIssue(
+ 789, 3, 'sum', 'New', 111, labels=['Hot'],
+ derived_labels=['Scalability'], star_count=12, reporter_id=222,
+ opened_timestamp=now, component_ids=[1], project_name='proj',
+ cc_ids=[111], derived_cc_ids=[222])
+ issue.phases = [
+ tracker_pb2.Phase(phase_id=1, name='Dev', rank=1),
+ tracker_pb2.Phase(phase_id=2, name='Beta', rank=2),
+ ]
+ issue.dangling_blocked_on_refs = [
+ tracker_pb2.DanglingIssueRef(project='dangling_proj', issue_id=1234)]
+ issue.dangling_blocking_refs = [
+ tracker_pb2.DanglingIssueRef(project='dangling_proj', issue_id=5678)]
+
+ actual = converters.ConvertIssue(
+ issue, self.users_by_id, related_refs_dict, self.config)
+
+ expected = issue_objects_pb2.Issue(
+ project_name='proj',
+ local_id=3,
+ summary='sum',
+ status_ref=common_pb2.StatusRef(
+ status='New',
+ is_derived=False,
+ means_open=True),
+ owner_ref=common_pb2.UserRef(
+ user_id=111,
+ display_name='one@example.com',
+ is_derived=False),
+ cc_refs=[
+ common_pb2.UserRef(
+ user_id=111,
+ display_name='one@example.com',
+ is_derived=False),
+ common_pb2.UserRef(
+ user_id=222,
+ display_name='two@example.com',
+ is_derived=True)],
+ label_refs=[
+ common_pb2.LabelRef(label='Hot', is_derived=False),
+ common_pb2.LabelRef(label='Scalability', is_derived=True)],
+ component_refs=[common_pb2.ComponentRef(path='UI', is_derived=False)],
+ is_deleted=False,
+ reporter_ref=common_pb2.UserRef(
+ user_id=222, display_name='two@example.com', is_derived=False),
+ opened_timestamp=now,
+ component_modified_timestamp=now,
+ status_modified_timestamp=now,
+ owner_modified_timestamp=now,
+ star_count=12,
+ is_spam=False,
+ attachment_count=0,
+ dangling_blocked_on_refs=[
+ common_pb2.IssueRef(project_name='dangling_proj', local_id=1234)],
+ dangling_blocking_refs=[
+ common_pb2.IssueRef(project_name='dangling_proj', local_id=5678)],
+ phases=[
+ issue_objects_pb2.PhaseDef(
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Dev'),
+ rank=1),
+ issue_objects_pb2.PhaseDef(
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Beta'),
+ rank=2)])
+ self.assertEqual(expected, actual)
+
+ def testConvertIssue_NegativeAttachmentCount(self):
+ """We can convert a protorpc Issue to a protoc Issue."""
+ related_refs_dict = {
+ 78901: ('proj', 1),
+ 78902: ('proj', 2),
+ }
+ now = 12345678
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI'),
+ tracker_pb2.ComponentDef(component_id=2, path='DB'),
+ ]
+ issue = fake.MakeTestIssue(
+ 789, 3, 'sum', 'New', 111, labels=['Hot'],
+ derived_labels=['Scalability'], star_count=12, reporter_id=222,
+ opened_timestamp=now, component_ids=[1], project_name='proj',
+ cc_ids=[111], derived_cc_ids=[222], attachment_count=-10)
+ issue.phases = [
+ tracker_pb2.Phase(phase_id=1, name='Dev', rank=1),
+ tracker_pb2.Phase(phase_id=2, name='Beta', rank=2),
+ ]
+ issue.dangling_blocked_on_refs = [
+ tracker_pb2.DanglingIssueRef(project='dangling_proj', issue_id=1234)]
+ issue.dangling_blocking_refs = [
+ tracker_pb2.DanglingIssueRef(project='dangling_proj', issue_id=5678)]
+
+ actual = converters.ConvertIssue(
+ issue, self.users_by_id, related_refs_dict, self.config)
+
+ expected = issue_objects_pb2.Issue(
+ project_name='proj',
+ local_id=3,
+ summary='sum',
+ status_ref=common_pb2.StatusRef(
+ status='New',
+ is_derived=False,
+ means_open=True),
+ owner_ref=common_pb2.UserRef(
+ user_id=111,
+ display_name='one@example.com',
+ is_derived=False),
+ cc_refs=[
+ common_pb2.UserRef(
+ user_id=111,
+ display_name='one@example.com',
+ is_derived=False),
+ common_pb2.UserRef(
+ user_id=222,
+ display_name='two@example.com',
+ is_derived=True)],
+ label_refs=[
+ common_pb2.LabelRef(label='Hot', is_derived=False),
+ common_pb2.LabelRef(label='Scalability', is_derived=True)],
+ component_refs=[common_pb2.ComponentRef(path='UI', is_derived=False)],
+ is_deleted=False,
+ reporter_ref=common_pb2.UserRef(
+ user_id=222, display_name='two@example.com', is_derived=False),
+ opened_timestamp=now,
+ component_modified_timestamp=now,
+ status_modified_timestamp=now,
+ owner_modified_timestamp=now,
+ star_count=12,
+ is_spam=False,
+ dangling_blocked_on_refs=[
+ common_pb2.IssueRef(project_name='dangling_proj', local_id=1234)],
+ dangling_blocking_refs=[
+ common_pb2.IssueRef(project_name='dangling_proj', local_id=5678)],
+ phases=[
+ issue_objects_pb2.PhaseDef(
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Dev'),
+ rank=1),
+ issue_objects_pb2.PhaseDef(
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Beta'),
+ rank=2)])
+ self.assertEqual(expected, actual)
+
+ def testConvertIssue_ExternalMergedInto(self):
+ """ConvertIssue works on issues with external mergedinto values."""
+ issue = fake.MakeTestIssue(789, 3, 'sum', 'New', 111, project_name='proj',
+ merged_into_external='b/5678')
+ actual = converters.ConvertIssue(issue, self.users_by_id, {}, self.config)
+ expected = issue_objects_pb2.Issue(
+ project_name='proj',
+ local_id=3,
+ summary='sum',
+ merged_into_issue_ref=common_pb2.IssueRef(ext_identifier='b/5678'),
+ status_ref=common_pb2.StatusRef(
+ status='New',
+ is_derived=False,
+ means_open=True),
+ owner_ref=common_pb2.UserRef(
+ user_id=111,
+ display_name='one@example.com',
+ is_derived=False),
+ reporter_ref=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com', is_derived=False))
+
+ self.assertEqual(expected, actual)
+
+ def testConvertPhaseDef(self):
+ """We can convert a prototpc Phase to a protoc PhaseDef. """
+ phase = tracker_pb2.Phase(phase_id=1, name='phase', rank=2)
+ actual = converters.ConvertPhaseDef(phase)
+ expected = issue_objects_pb2.PhaseDef(
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='phase'),
+ rank=2
+ )
+ self.assertEqual(expected, actual)
+
+ def testConvertAmendment(self):
+ """We can convert various kinds of Amendments."""
+ amend = tracker_pb2.Amendment(
+ field=tracker_pb2.FieldID.SUMMARY, newvalue='new', oldvalue='old')
+ actual = converters.ConvertAmendment(amend, self.users_by_id)
+ self.assertEqual('Summary', actual.field_name)
+ self.assertEqual('new', actual.new_or_delta_value)
+ self.assertEqual('old', actual.old_value)
+
+ amend = tracker_pb2.Amendment(
+ field=tracker_pb2.FieldID.OWNER, added_user_ids=[111])
+ actual = converters.ConvertAmendment(amend, self.users_by_id)
+ self.assertEqual('Owner', actual.field_name)
+ self.assertEqual('one@example.com', actual.new_or_delta_value)
+ self.assertEqual('', actual.old_value)
+
+ amend = tracker_pb2.Amendment(
+ field=tracker_pb2.FieldID.CC,
+ added_user_ids=[111], removed_user_ids=[222])
+ actual = converters.ConvertAmendment(amend, self.users_by_id)
+ self.assertEqual('Cc', actual.field_name)
+ self.assertEqual(
+ '-two@example.com one@example.com', actual.new_or_delta_value)
+ self.assertEqual('', actual.old_value)
+
+ amend = tracker_pb2.Amendment(
+ field=tracker_pb2.FieldID.CUSTOM, custom_field_name='EstDays',
+ newvalue='12')
+ actual = converters.ConvertAmendment(amend, self.users_by_id)
+ self.assertEqual('EstDays', actual.field_name)
+ self.assertEqual('12', actual.new_or_delta_value)
+ self.assertEqual('', actual.old_value)
+
+ @patch('tracker.attachment_helpers.SignAttachmentID')
+ def testConvertAttachment(self, mock_SignAttachmentID):
+ mock_SignAttachmentID.return_value = 2
+ attach = tracker_pb2.Attachment(
+ attachment_id=1, mimetype='image/png', filename='example.png',
+ filesize=12345)
+
+ actual = converters.ConvertAttachment(attach, 'proj')
+
+ expected = issue_objects_pb2.Attachment(
+ attachment_id=1, filename='example.png',
+ size=12345, content_type='image/png',
+ thumbnail_url='attachment?aid=1&signed_aid=2&inline=1&thumb=1',
+ view_url='attachment?aid=1&signed_aid=2&inline=1',
+ download_url='attachment?aid=1&signed_aid=2')
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_Normal(self):
+ """We can convert a protorpc IssueComment to a protoc Comment."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12)
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 111,
+ permissions.PermissionSet([]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', is_spam=False)
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_CanReportComment(self):
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12)
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 111,
+ permissions.PermissionSet([permissions.FLAG_SPAM]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', can_flag=True)
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_CanUnReportComment(self):
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12)
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [111], {}, 111,
+ permissions.PermissionSet([permissions.FLAG_SPAM]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', is_spam=True, is_deleted=True,
+ can_flag=True)
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_CantUnFlagCommentWithoutVerdictSpam(self):
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12, is_spam=True)
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [111], {}, 111,
+ permissions.PermissionSet([permissions.FLAG_SPAM]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12,
+ timestamp=now, is_spam=True, is_deleted=True)
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_CanFlagSpamComment(self):
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12)
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 111,
+ permissions.PermissionSet([permissions.VERDICT_SPAM]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', can_flag=True)
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_CanUnFlagSpamComment(self):
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12, is_spam=True)
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [222], {}, 111,
+ permissions.PermissionSet([permissions.VERDICT_SPAM]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', is_spam=True, is_deleted=True,
+ can_flag=True)
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_DeletedComment(self):
+ """We can convert a protorpc IssueComment to a protoc Comment."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12, deleted_by=111)
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 111,
+ permissions.PermissionSet([permissions.DELETE_OWN]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12, is_deleted=True,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', can_delete=True)
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_DeletedCommentCantView(self):
+ """We can convert a protorpc IssueComment to a protoc Comment."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12, deleted_by=111)
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 111,
+ permissions.PermissionSet([]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12, is_deleted=True,
+ timestamp=now)
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_CommentByBannedUser(self):
+ """We can convert a protorpc IssueComment to a protoc Comment."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=333, timestamp=now,
+ content='a comment', sequence=12)
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 111,
+ permissions.PermissionSet([]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12, is_deleted=True,
+ timestamp=now)
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_Description(self):
+ """We can convert a protorpc IssueComment to a protoc Comment."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12, is_description=True)
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {101: 1}, 111,
+ permissions.PermissionSet([]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', is_spam=False, description_num=1)
+ self.assertEqual(expected, actual)
+ comment.is_description = False
+
+ def testConvertComment_Approval(self):
+ """We can convert a protorpc IssueComment to a protoc Comment."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12, approval_id=11)
+ # Comment on an approval.
+ self.config.field_defs.append(tracker_pb2.FieldDef(
+ field_id=11, project_id=789, field_name='Accessibility',
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ applicable_type='Launch'))
+ self.config.approval_defs.append(tracker_pb2.ApprovalDef(
+ approval_id=11, approver_ids=[111], survey='survey 1'))
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 111,
+ permissions.PermissionSet([]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', is_spam=False,
+ approval_ref=common_pb2.FieldRef(field_name='Accessibility'))
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_ViewOwnInboundMessage(self):
+ """Users can view their own inbound messages."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12, inbound_message='inbound message')
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 111,
+ permissions.PermissionSet([]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', inbound_message='inbound message')
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_ViewInboundMessageWithPermission(self):
+ """Users can view their own inbound messages."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12, inbound_message='inbound message')
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 222,
+ permissions.PermissionSet([permissions.VIEW_INBOUND_MESSAGES]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment', inbound_message='inbound message')
+ self.assertEqual(expected, actual)
+
+ def testConvertComment_NotAllowedToViewInboundMessage(self):
+ """Users can view their own inbound messages."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=111, timestamp=now,
+ content='a comment', sequence=12, inbound_message='inbound message')
+
+ actual = converters.ConvertComment(
+ issue, comment, self.config, self.users_by_id, [], {}, 222,
+ permissions.PermissionSet([]))
+ expected = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=12, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a comment')
+ self.assertEqual(expected, actual)
+
+ def testConvertCommentList(self):
+ """We can convert a list of comments."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment_0 = tracker_pb2.IssueComment(
+ id=100, project_id=789, user_id=111, timestamp=now,
+ content='a description', sequence=0, is_description=True)
+ comment_1 = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=222, timestamp=now,
+ content='a comment', sequence=1)
+ comment_2 = tracker_pb2.IssueComment(
+ id=102, project_id=789, user_id=222, timestamp=now,
+ content='deleted comment', sequence=2, deleted_by=111)
+ comment_3 = tracker_pb2.IssueComment(
+ id=103, project_id=789, user_id=111, timestamp=now,
+ content='another desc', sequence=3, is_description=True)
+
+ actual = converters.ConvertCommentList(
+ issue, [comment_0, comment_1, comment_2, comment_3], self.config,
+ self.users_by_id, {}, 222,
+ permissions.PermissionSet([permissions.DELETE_OWN]))
+
+ expected_0 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=0, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a description', is_spam=False,
+ description_num=1)
+ expected_1 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=1, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=222, display_name='two@example.com'),
+ timestamp=now, content='a comment', is_spam=False, can_delete=True)
+ expected_2 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=2, is_deleted=True,
+ timestamp=now)
+ expected_3 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=3, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='another desc', is_spam=False,
+ description_num=2)
+ self.assertEqual(expected_0, actual[0])
+ self.assertEqual(expected_1, actual[1])
+ self.assertEqual(expected_2, actual[2])
+ self.assertEqual(expected_3, actual[3])
+
+ def testConvertCommentList_DontUseDeletedOrSpamDescriptions(self):
+ """When converting comments, deleted or spam are not descriptions."""
+ now = 1234567890
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj')
+ comment_0 = tracker_pb2.IssueComment(
+ id=100, project_id=789, user_id=111, timestamp=now,
+ content='a description', sequence=0, is_description=True)
+ comment_1 = tracker_pb2.IssueComment(
+ id=101, project_id=789, user_id=222, timestamp=now,
+ content='a spam description', sequence=1, is_description=True,
+ is_spam=True)
+ comment_2 = tracker_pb2.IssueComment(
+ id=102, project_id=789, user_id=222, timestamp=now,
+ content='a deleted description', sequence=2, is_description=True,
+ deleted_by=111)
+ comment_3 = tracker_pb2.IssueComment(
+ id=103, project_id=789, user_id=111, timestamp=now,
+ content='another good desc', sequence=3, is_description=True)
+ comment_4 = tracker_pb2.IssueComment(
+ id=104, project_id=789, user_id=333, timestamp=now,
+ content='desc from banned', sequence=4, is_description=True)
+
+ actual = converters.ConvertCommentList(
+ issue, [comment_0, comment_1, comment_2, comment_3, comment_4],
+ self.config, self.users_by_id, {}, 222,
+ permissions.PermissionSet([permissions.DELETE_OWN]))
+
+ expected_0 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=0, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='a description', is_spam=False,
+ description_num=1)
+ expected_1 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=1, is_deleted=True,
+ timestamp=now, is_spam=True, can_delete=False)
+ expected_2 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=2, is_deleted=True,
+ timestamp=now)
+ expected_3 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=3, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='one@example.com'),
+ timestamp=now, content='another good desc', is_spam=False,
+ description_num=2)
+ expected_4 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=4, is_deleted=True,
+ timestamp=now, is_spam=False)
+ self.assertEqual(expected_0, actual[0])
+ self.assertEqual(expected_1, actual[1])
+ self.assertEqual(expected_2, actual[2])
+ self.assertEqual(expected_3, actual[3])
+ self.assertEqual(expected_4, actual[4])
+
+ def testIngestUserRef(self):
+ """We can look up a single user ID for a protoc UserRef."""
+ self.services.user.TestAddUser('user1@example.com', 111)
+ ref = common_pb2.UserRef(display_name='user1@example.com')
+ actual = converters.IngestUserRef(self.cnxn, ref, self.services.user)
+ self.assertEqual(111, actual)
+
+ def testIngestUserRef_NoSuchUser(self):
+ """We reject a malformed UserRef.display_name."""
+ ref = common_pb2.UserRef(display_name='Bob@gmail.com')
+ with self.assertRaises(exceptions.NoSuchUserException):
+ converters.IngestUserRef(self.cnxn, ref, self.services.user)
+
+ def testIngestUserRefs_ClearTheOwnerField(self):
+ """We can look up user IDs for protoc UserRefs."""
+ ref = common_pb2.UserRef(user_id=0)
+ actual = converters.IngestUserRefs(self.cnxn, [ref], self.services.user)
+ self.assertEqual([0], actual)
+
+ def testIngestUserRefs_ByExistingID(self):
+ """Users can be specified by user_id."""
+ self.services.user.TestAddUser('user1@example.com', 111)
+ ref = common_pb2.UserRef(user_id=111)
+ actual = converters.IngestUserRefs(self.cnxn, [ref], self.services.user)
+ self.assertEqual([111], actual)
+
+ def testIngestUserRefs_ByNonExistingID(self):
+ """We reject references to non-existing user IDs."""
+ ref = common_pb2.UserRef(user_id=999)
+ with self.assertRaises(exceptions.NoSuchUserException):
+ converters.IngestUserRefs(self.cnxn, [ref], self.services.user)
+
+ def testIngestUserRefs_ByExistingEmail(self):
+ """Existing users can be specified by email address."""
+ self.services.user.TestAddUser('user1@example.com', 111)
+ ref = common_pb2.UserRef(display_name='user1@example.com')
+ actual = converters.IngestUserRefs(self.cnxn, [ref], self.services.user)
+ self.assertEqual([111], actual)
+
+ def testIngestUserRefs_ByNonExistingEmail(self):
+ """New users can be specified by email address."""
+ # Case where autocreate=False
+ ref = common_pb2.UserRef(display_name='new@example.com')
+ with self.assertRaises(exceptions.NoSuchUserException):
+ converters.IngestUserRefs(
+ self.cnxn, [ref], self.services.user, autocreate=False)
+
+ # Case where autocreate=True
+ actual = converters.IngestUserRefs(
+ self.cnxn, [ref], self.services.user, autocreate=True)
+ user_id = self.services.user.LookupUserID(self.cnxn, 'new@example.com')
+ self.assertEqual([user_id], actual)
+
+ def testIngestUserRefs_ByMalformedEmail(self):
+ """We ignore malformed user emails."""
+ self.services.user.TestAddUser('user1@example.com', 111)
+ self.services.user.TestAddUser('user3@example.com', 333)
+ refs = [
+ common_pb2.UserRef(user_id=0),
+ common_pb2.UserRef(display_name='not-a-valid-email'),
+ common_pb2.UserRef(user_id=333),
+ common_pb2.UserRef(display_name='user1@example.com')
+ ]
+ actual = converters.IngestUserRefs(
+ self.cnxn, refs, self.services.user, autocreate=True)
+ self.assertEqual(actual, [0, 333, 111])
+
+ def testIngestUserRefs_MixOfIDAndEmail(self):
+ """Requests can specify some users by ID and others by email."""
+ self.services.user.TestAddUser('user1@example.com', 111)
+ self.services.user.TestAddUser('user2@example.com', 222)
+ self.services.user.TestAddUser('user3@example.com', 333)
+ ref1 = common_pb2.UserRef(display_name='user1@example.com')
+ ref2 = common_pb2.UserRef(display_name='user2@example.com')
+ ref3 = common_pb2.UserRef(user_id=333)
+ actual = converters.IngestUserRefs(
+ self.cnxn, [ref1, ref2, ref3], self.services.user)
+ self.assertEqual([111, 222, 333], actual)
+
+ def testIngestUserRefs_UppercaseEmail(self):
+ """Request can include uppercase letters in email"""
+ self.services.user.TestAddUser('user1@example.com', 111)
+ ref = common_pb2.UserRef(display_name='USER1@example.com')
+ actual = converters.IngestUserRefs(self.cnxn, [ref], self.services.user)
+ self.assertEqual([111], actual)
+
+ def testIngestPrefValues(self):
+ """We can convert a list of UserPrefValues from protoc to protorpc."""
+ self.assertEqual(
+ [],
+ converters.IngestPrefValues([]))
+
+ userprefvalues = [
+ user_objects_pb2.UserPrefValue(name='foo_1', value='bar_1'),
+ user_objects_pb2.UserPrefValue(name='foo_2', value='bar_2')]
+ actual = converters.IngestPrefValues(userprefvalues)
+ expected = [
+ user_pb2.UserPrefValue(name='foo_1', value='bar_1'),
+ user_pb2.UserPrefValue(name='foo_2', value='bar_2')]
+ self.assertEqual(expected, actual)
+
+ def testIngestComponentRefs(self):
+ """We can look up component IDs for a list of protoc UserRefs."""
+ self.assertEqual([], converters.IngestComponentRefs([], self.config))
+
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI'),
+ tracker_pb2.ComponentDef(component_id=2, path='DB')]
+ refs = [common_pb2.ComponentRef(path='UI'),
+ common_pb2.ComponentRef(path='DB')]
+ self.assertEqual(
+ [1, 2], converters.IngestComponentRefs(refs, self.config))
+
+ def testIngestIssueRefs_ValidatesExternalRefs(self):
+ """IngestIssueRefs requires external refs have at least one slash."""
+ ref = common_pb2.IssueRef(ext_identifier='b123456')
+ with self.assertRaises(exceptions.InvalidExternalIssueReference):
+ converters.IngestIssueRefs(self.cnxn, [ref], self.services)
+
+ def testIngestIssueRefs_SkipsExternalRefs(self):
+ """IngestIssueRefs skips external refs."""
+ ref = common_pb2.IssueRef(ext_identifier='b/123456')
+ actual = converters.IngestIssueRefs(
+ self.cnxn, [ref], self.services)
+ self.assertEqual([], actual)
+
+ def testIngestExtIssueRefs_Normal(self):
+ """IngestExtIssueRefs returns all valid external refs."""
+ refs = [
+ common_pb2.IssueRef(project_name='rutabaga', local_id=1234),
+ common_pb2.IssueRef(ext_identifier='b123456'),
+ common_pb2.IssueRef(ext_identifier='b/123456'), # <- Valid ref 1.
+ common_pb2.IssueRef(ext_identifier='rutabaga/123456'),
+ common_pb2.IssueRef(ext_identifier='123456'),
+ common_pb2.IssueRef(ext_identifier='b/56789'), # <- Valid ref 2.
+ common_pb2.IssueRef(ext_identifier='b//123456')]
+
+ actual = converters.IngestExtIssueRefs(refs)
+ self.assertEqual(['b/123456', 'b/56789'], actual)
+
+ def testIngestIssueDelta_Empty(self):
+ """An empty protorpc IssueDelta makes an empty protoc IssueDelta."""
+ delta = issue_objects_pb2.IssueDelta()
+ actual = converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+ expected = tracker_pb2.IssueDelta()
+ self.assertEqual(expected, actual)
+
+ def testIngestIssueDelta_BuiltInFields(self):
+ """We can create a protorpc IssueDelta from a protoc IssueDelta."""
+ self.services.user.TestAddUser('user1@example.com', 111)
+ self.services.user.TestAddUser('user2@example.com', 222)
+ self.services.user.TestAddUser('user3@example.com', 333)
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI')]
+ delta = issue_objects_pb2.IssueDelta(
+ status=wrappers_pb2.StringValue(value='Fixed'),
+ owner_ref=common_pb2.UserRef(user_id=222),
+ summary=wrappers_pb2.StringValue(value='New summary'),
+ cc_refs_add=[common_pb2.UserRef(user_id=333)],
+ comp_refs_add=[common_pb2.ComponentRef(path='UI')],
+ label_refs_add=[common_pb2.LabelRef(label='Hot')])
+ actual = converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+ expected = tracker_pb2.IssueDelta(
+ status='Fixed', owner_id=222, summary='New summary',
+ cc_ids_add=[333], comp_ids_add=[1],
+ labels_add=['Hot'])
+ self.assertEqual(expected, actual)
+
+ def testIngestIssueDelta_ClearMergedInto(self):
+ """We can clear merged into from the current issue."""
+ delta = issue_objects_pb2.IssueDelta(merged_into_ref=common_pb2.IssueRef())
+ actual = converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+ expected = tracker_pb2.IssueDelta(merged_into=0)
+ self.assertEqual(expected, actual)
+
+ def testIngestIssueDelta_BadOwner(self):
+ """We reject a specified owner that does not exist."""
+ delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(display_name='user@exa'))
+ with self.assertRaises(exceptions.NoSuchUserException):
+ converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+
+ def testIngestIssueDelta_BadOwnerIgnored(self):
+ """We can ignore an incomplete owner email for presubmit."""
+ delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(display_name='user@exa'))
+ actual = converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [],
+ ignore_missing_objects=True)
+ expected = tracker_pb2.IssueDelta()
+ self.assertEqual(expected, actual)
+
+ def testIngestIssueDelta_InvalidComponent(self):
+ """We reject a protorpc IssueDelta that has an invalid component."""
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI')]
+ delta = issue_objects_pb2.IssueDelta(
+ comp_refs_add=[common_pb2.ComponentRef(path='XYZ')])
+ with self.assertRaises(exceptions.NoSuchComponentException):
+ converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+
+ def testIngestIssueDelta_InvalidComponentIgnored(self):
+ """We can ignore invalid components for presubmits."""
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI')]
+ delta = issue_objects_pb2.IssueDelta(
+ comp_refs_add=[common_pb2.ComponentRef(path='UI'),
+ common_pb2.ComponentRef(path='XYZ')])
+ actual = converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [],
+ ignore_missing_objects=True)
+ self.assertEqual([1], actual.comp_ids_add)
+
+ def testIngestIssueDelta_CustomFields(self):
+ """We can create a protorpc IssueDelta from a protoc IssueDelta."""
+ self.config.field_defs = [
+ self.fd_1, self.fd_2, self.fd_3, self.fd_4, self.fd_6]
+ phases = [tracker_pb2.Phase(phase_id=1, name="Beta")]
+ delta = issue_objects_pb2.IssueDelta(
+ field_vals_add=[
+ issue_objects_pb2.FieldValue(
+ value='string',
+ field_ref=common_pb2.FieldRef(field_name='FirstField')
+ ),
+ issue_objects_pb2.FieldValue(
+ value='1',
+ field_ref=common_pb2.FieldRef(field_name='PhaseField'),
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Beta')
+ )],
+ field_vals_remove=[
+ issue_objects_pb2.FieldValue(
+ value='34', field_ref=common_pb2.FieldRef(
+ field_name='SecField'))],
+ fields_clear=[common_pb2.FieldRef(field_name='FirstField')])
+ actual = converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, phases)
+ self.assertEqual(actual.field_vals_add,
+ [tracker_pb2.FieldValue(
+ str_value='string', field_id=1, derived=False),
+ tracker_pb2.FieldValue(
+ int_value=1, field_id=6, phase_id=1, derived=False)
+ ])
+ self.assertEqual(actual.field_vals_remove, [tracker_pb2.FieldValue(
+ int_value=34, field_id=2, derived=False)])
+ self.assertEqual(actual.fields_clear, [1])
+
+ def testIngestIssueDelta_InvalidCustomFields(self):
+ """We can create a protorpc IssueDelta from a protoc IssueDelta."""
+ # TODO(jrobbins): add and remove.
+ delta = issue_objects_pb2.IssueDelta(
+ fields_clear=[common_pb2.FieldRef(field_name='FirstField')])
+ with self.assertRaises(exceptions.NoSuchFieldDefException):
+ converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+
+ def testIngestIssueDelta_ShiftFieldsIntoLabels(self):
+ """Test that enum fields are shifted into labels."""
+ self.config.field_defs = [self.fd_5]
+ delta = issue_objects_pb2.IssueDelta(
+ field_vals_add=[
+ issue_objects_pb2.FieldValue(
+ value='Foo',
+ field_ref=common_pb2.FieldRef(field_name='Pre', field_id=5)
+ )],
+ field_vals_remove=[
+ issue_objects_pb2.FieldValue(
+ value='Bar',
+ field_ref=common_pb2.FieldRef(field_name='Pre', field_id=5),
+ )])
+ actual = converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+ self.assertEqual(actual.field_vals_add, [])
+ self.assertEqual(actual.field_vals_remove, [])
+ self.assertEqual(actual.labels_add, ['Pre-Foo'])
+ self.assertEqual(actual.labels_remove, ['Pre-Bar'])
+
+ def testIngestIssueDelta_RelatedIssues(self):
+ """We can create a protorpc IssueDelta that references related issues."""
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111)
+ self.services.issue.TestAddIssue(issue)
+ delta = issue_objects_pb2.IssueDelta(
+ blocked_on_refs_add=[common_pb2.IssueRef(
+ project_name='proj', local_id=issue.local_id)],
+ merged_into_ref=common_pb2.IssueRef(
+ project_name='proj', local_id=issue.local_id))
+ actual = converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+ self.assertEqual([issue.issue_id], actual.blocked_on_add)
+ self.assertEqual([], actual.blocking_add)
+ self.assertEqual(issue.issue_id, actual.merged_into)
+
+ def testIngestIssueDelta_InvalidRelatedIssues(self):
+ """We reject references to related issues that do not exist."""
+ delta = issue_objects_pb2.IssueDelta(
+ merged_into_ref=common_pb2.IssueRef(
+ project_name='not-a-proj', local_id=8))
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+
+ delta = issue_objects_pb2.IssueDelta(
+ merged_into_ref=common_pb2.IssueRef(
+ project_name='proj', local_id=999))
+ with self.assertRaises(exceptions.NoSuchIssueException):
+ converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+
+ def testIngestIssueDelta_ExternalMergedInto(self):
+ """IngestIssueDelta properly handles external mergedinto refs."""
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111)
+ self.services.issue.TestAddIssue(issue)
+ delta = issue_objects_pb2.IssueDelta(
+ merged_into_ref=common_pb2.IssueRef(ext_identifier='b/5678'))
+ actual = converters.IngestIssueDelta(
+ self.cnxn, self.services, delta, self.config, [])
+
+ self.assertIsNone(actual.merged_into)
+ self.assertEqual('b/5678', actual.merged_into_external)
+
+ def testIngestAttachmentUploads_Empty(self):
+ """Uploading zero files results in an empty list of attachments."""
+ self.assertEqual([], converters.IngestAttachmentUploads([]))
+
+ def testIngestAttachmentUploads_Normal(self):
+ """Uploading files results in a list of attachments."""
+ uploads = [
+ issue_objects_pb2.AttachmentUpload(
+ filename='hello.c', content='int main() {}'),
+ issue_objects_pb2.AttachmentUpload(
+ filename='README.md', content='readme content'),
+ ]
+ actual = converters.IngestAttachmentUploads(uploads)
+ self.assertEqual(
+ [('hello.c', 'int main() {}', 'text/plain'),
+ ('README.md', 'readme content', 'text/plain')],
+ actual)
+
+ def testIngestAttachmentUploads_Invalid(self):
+ """We reject uploaded files that lack a name or content."""
+ with self.assertRaises(exceptions.InputException):
+ converters.IngestAttachmentUploads([
+ issue_objects_pb2.AttachmentUpload(content='name is mssing')])
+
+ with self.assertRaises(exceptions.InputException):
+ converters.IngestAttachmentUploads([
+ issue_objects_pb2.AttachmentUpload(filename='content is mssing')])
+
+ def testIngestApprovalDelta(self):
+ self.services.user.TestAddUser('user1@example.com', 111)
+ self.services.user.TestAddUser('user2@example.com', 222)
+
+ self.config.field_defs = [
+ self.fd_1, self.fd_2, self.fd_3, self.fd_4, self.fd_7]
+
+ approval_delta = issue_objects_pb2.ApprovalDelta(
+ status=issue_objects_pb2.APPROVED,
+ approver_refs_add=[common_pb2.UserRef(user_id=111)],
+ approver_refs_remove=[common_pb2.UserRef(user_id=222)],
+ field_vals_add=[
+ issue_objects_pb2.FieldValue(
+ value='string', field_ref=common_pb2.FieldRef(
+ field_id=1, field_name='FirstField')),
+ issue_objects_pb2.FieldValue(
+ value='choice1', field_ref=common_pb2.FieldRef(
+ field_id=7, field_name='ApprovalEnum')),
+ ],
+ field_vals_remove=[
+ issue_objects_pb2.FieldValue(
+ value='34', field_ref=common_pb2.FieldRef(
+ field_id=2, field_name='SecField')),
+ issue_objects_pb2.FieldValue(
+ value='choice2', field_ref=common_pb2.FieldRef(
+ field_id=7, field_name='ApprovalEnum')),
+ ],
+ fields_clear=[common_pb2.FieldRef(field_name='FirstField')])
+
+ actual = converters.IngestApprovalDelta(
+ self.cnxn, self.services.user, approval_delta, 333, self.config)
+ self.assertEqual(
+ actual.status, tracker_pb2.ApprovalStatus.APPROVED,)
+ self.assertEqual(actual.setter_id, 333)
+ self.assertEqual(actual.approver_ids_add, [111])
+ self.assertEqual(actual.approver_ids_remove, [222])
+ self.assertEqual(actual.subfield_vals_add, [tracker_pb2.FieldValue(
+ str_value='string', field_id=1, derived=False)])
+ self.assertEqual(actual.subfield_vals_remove, [tracker_pb2.FieldValue(
+ int_value=34, field_id=2, derived=False)])
+ self.assertEqual(actual.subfields_clear, [1])
+ self.assertEqual(actual.labels_add, ['ApprovalEnum-choice1'])
+ self.assertEqual(actual.labels_remove, ['ApprovalEnum-choice2'])
+
+ # test a NOT_SET status is registered as None.
+ approval_delta.status = issue_objects_pb2.NOT_SET
+ actual = converters.IngestApprovalDelta(
+ self.cnxn, self.services.user, approval_delta, 333, self.config)
+ self.assertIsNone(actual.status)
+
+ def testIngestApprovalStatus(self):
+ actual = converters.IngestApprovalStatus(issue_objects_pb2.NOT_SET)
+ self.assertEqual(actual, tracker_pb2.ApprovalStatus.NOT_SET)
+
+ actual = converters.IngestApprovalStatus(issue_objects_pb2.NOT_APPROVED)
+ self.assertEqual(actual, tracker_pb2.ApprovalStatus.NOT_APPROVED)
+
+ def testIngestFieldValues(self):
+ self.services.user.TestAddUser('user1@example.com', 111)
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_4, self.fd_6]
+ phases = [
+ tracker_pb2.Phase(phase_id=3, name="Dev"),
+ tracker_pb2.Phase(phase_id=1, name="Beta")
+ ]
+
+ field_values = [
+ issue_objects_pb2.FieldValue(
+ value='string',
+ field_ref=common_pb2.FieldRef(field_name='FirstField')
+ ),
+ issue_objects_pb2.FieldValue(
+ value='34',
+ field_ref=common_pb2.FieldRef(field_name='SecField')
+ ),
+ issue_objects_pb2.FieldValue(
+ value='user1@example.com',
+ field_ref=common_pb2.FieldRef(field_name='UserField'),
+ # phase_ref for non-phase fields should be ignored.
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Dev')
+ ),
+ issue_objects_pb2.FieldValue(
+ value='2',
+ field_ref=common_pb2.FieldRef(field_name='PhaseField'),
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='Beta'))
+ ]
+
+ actual = converters.IngestFieldValues(
+ self.cnxn, self.services.user, field_values, self.config, phases)
+ self.assertEqual(
+ actual,
+ [
+ tracker_pb2.FieldValue(
+ str_value='string', field_id=1, derived=False),
+ tracker_pb2.FieldValue(int_value=34, field_id=2, derived=False),
+ tracker_pb2.FieldValue(user_id=111, field_id=4, derived=False),
+ tracker_pb2.FieldValue(
+ int_value=2, field_id=6, phase_id=1, derived=False)
+ ]
+ )
+
+ def testIngestFieldValues_EmptyUser(self):
+ """We ignore empty user email strings."""
+ self.services.user.TestAddUser('user1@example.com', 111)
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_4, self.fd_6]
+ field_values = [
+ issue_objects_pb2.FieldValue(
+ value='user1@example.com',
+ field_ref=common_pb2.FieldRef(field_name='UserField')),
+ issue_objects_pb2.FieldValue(
+ value='',
+ field_ref=common_pb2.FieldRef(field_name='UserField'))
+ ]
+
+ actual = converters.IngestFieldValues(
+ self.cnxn, self.services.user, field_values, self.config, [])
+ self.assertEqual(
+ actual,
+ [tracker_pb2.FieldValue(user_id=111, field_id=4, derived=False)])
+
+ def testIngestFieldValues_Unicode(self):
+ """We can ingest unicode strings."""
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_4, self.fd_6]
+ field_values = [
+ issue_objects_pb2.FieldValue(
+ value=u'\xe2\x9d\xa4\xef\xb8\x8f',
+ field_ref=common_pb2.FieldRef(field_name='FirstField')
+ ),
+ ]
+
+ actual = converters.IngestFieldValues(
+ self.cnxn, self.services.user, field_values, self.config, [])
+ self.assertEqual(
+ actual,
+ [
+ tracker_pb2.FieldValue(
+ str_value=u'\xe2\x9d\xa4\xef\xb8\x8f', field_id=1,
+ derived=False),
+ ]
+ )
+
+ def testIngestFieldValues_InvalidUser(self):
+ """We reject invalid user email strings."""
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_4, self.fd_6]
+ field_values = [
+ issue_objects_pb2.FieldValue(
+ value='bad value',
+ field_ref=common_pb2.FieldRef(field_name='UserField'))]
+
+ with self.assertRaises(exceptions.NoSuchUserException):
+ converters.IngestFieldValues(
+ self.cnxn, self.services.user, field_values, self.config, [])
+
+ def testIngestFieldValues_InvalidInt(self):
+ """We reject invalid int-field strings."""
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_4, self.fd_6]
+ field_values = [
+ issue_objects_pb2.FieldValue(
+ value='Not a number',
+ field_ref=common_pb2.FieldRef(field_name='SecField'))]
+
+ with self.assertRaises(exceptions.InputException) as cm:
+ converters.IngestFieldValues(
+ self.cnxn, self.services.user, field_values, self.config, [])
+
+ self.assertEqual(
+ 'Unparsable value for field SecField',
+ cm.exception.message)
+
+ def testIngestSavedQueries(self):
+ self.services.project.TestAddProject('chromium', project_id=1)
+ self.services.project.TestAddProject('fakeproject', project_id=2)
+
+ saved_queries = [
+ tracker_pb2.SavedQuery(
+ query_id=101,
+ name='test query',
+ query='owner:me',
+ executes_in_project_ids=[1, 2]),
+ tracker_pb2.SavedQuery(
+ query_id=202,
+ name='another query',
+ query='-component:Test',
+ executes_in_project_ids=[1])
+ ]
+
+ converted_queries = converters.IngestSavedQueries(self.cnxn,
+ self.services.project, saved_queries)
+
+ self.assertEqual(converted_queries[0].query_id, 101)
+ self.assertEqual(converted_queries[0].name, 'test query')
+ self.assertEqual(converted_queries[0].query, 'owner:me')
+ self.assertEqual(converted_queries[0].project_names,
+ ['chromium', 'fakeproject'])
+
+ self.assertEqual(converted_queries[1].query_id, 202)
+ self.assertEqual(converted_queries[1].name, 'another query')
+ self.assertEqual(converted_queries[1].query, '-component:Test')
+ self.assertEqual(converted_queries[1].project_names, ['chromium'])
+
+
+ def testIngestHotlistRef(self):
+ self.services.user.TestAddUser('user1@example.com', 111)
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ owner_ref = common_pb2.UserRef(user_id=111)
+ hotlist_ref = common_pb2.HotlistRef(name='Fake-Hotlist', owner=owner_ref)
+
+ actual_hotlist_id = converters.IngestHotlistRef(
+ self.cnxn, self.services.user, self.services.features, hotlist_ref)
+ self.assertEqual(actual_hotlist_id, hotlist.hotlist_id)
+
+ def testIngestHotlistRef_HotlistID(self):
+ self.services.user.TestAddUser('user1@example.com', 111)
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ hotlist_ref = common_pb2.HotlistRef(hotlist_id=hotlist.hotlist_id)
+
+ actual_hotlist_id = converters.IngestHotlistRef(
+ self.cnxn, self.services.user, self.services.features, hotlist_ref)
+ self.assertEqual(actual_hotlist_id, hotlist.hotlist_id)
+
+ def testIngestHotlistRef_NotEnoughInformation(self):
+ hotlist_ref = common_pb2.HotlistRef(name='Some-Hotlist')
+ with self.assertRaises(features_svc.NoSuchHotlistException):
+ converters.IngestHotlistRef(
+ self.cnxn, self.services.user, self.services.features, hotlist_ref)
+
+ def testIngestHotlistRef_InconsistentRequest(self):
+ self.services.user.TestAddUser('user1@example.com', 111)
+ hotlist1 = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist-2', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ hotlist_ref = common_pb2.HotlistRef(
+ hotlist_id=hotlist1.hotlist_id,
+ name='Fake-Hotlist-2',
+ owner=common_pb2.UserRef(user_id=111))
+ with self.assertRaises(features_svc.NoSuchHotlistException):
+ converters.IngestHotlistRef(
+ self.cnxn, self.services.user, self.services.features, hotlist_ref)
+
+ def testIngestHotlistRef_NonExistentHotlistID(self):
+ hotlist_ref = common_pb2.HotlistRef(hotlist_id=1234)
+ with self.assertRaises(features_svc.NoSuchHotlistException):
+ converters.IngestHotlistRef(
+ self.cnxn, self.services.user, self.services.features, hotlist_ref)
+
+ def testIngestHotlistRef_NoSuchHotlist(self):
+ self.services.user.TestAddUser('user1@example.com', 111)
+
+ owner_ref = common_pb2.UserRef(user_id=111)
+ hotlist_ref = common_pb2.HotlistRef(name='Fake-Hotlist', owner=owner_ref)
+
+ with self.assertRaises(features_svc.NoSuchHotlistException):
+ converters.IngestHotlistRef(
+ self.cnxn, self.services.user, self.services.features, hotlist_ref)
+
+ def testIngestHotlistRefs(self):
+ self.services.user.TestAddUser('user1@example.com', 111)
+ hotlist_1 = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+ hotlist_2 = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist-2', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ owner_ref = common_pb2.UserRef(user_id=111)
+ hotlist_refs = [
+ common_pb2.HotlistRef(name='Fake-Hotlist', owner=owner_ref),
+ common_pb2.HotlistRef(hotlist_id=hotlist_2.hotlist_id)]
+
+ actual_hotlist_ids = converters.IngestHotlistRefs(
+ self.cnxn, self.services.user, self.services.features, hotlist_refs)
+ self.assertEqual(
+ actual_hotlist_ids, [hotlist_1.hotlist_id, hotlist_2.hotlist_id])
+
+ def testIngestPagination(self):
+ # Use settings.max_project_search_results_per_page if max_items is not
+ # present.
+ pagination = common_pb2.Pagination(start=1234)
+ self.assertEqual(
+ (1234, settings.max_artifact_search_results_per_page),
+ converters.IngestPagination(pagination))
+ # Otherwise, use the minimum between what was requested and
+ # settings.max_project_search_results_per_page
+ pagination = common_pb2.Pagination(start=1234, max_items=56)
+ self.assertEqual(
+ (1234, 56),
+ converters.IngestPagination(pagination))
+ pagination = common_pb2.Pagination(start=1234, max_items=5678)
+ self.assertEqual(
+ (1234, settings.max_artifact_search_results_per_page),
+ converters.IngestPagination(pagination))
+
+ # TODO(jojwang): add testConvertStatusRef
+
+ def testConvertStatusDef(self):
+ """We can convert a status definition to protoc."""
+ status_def = tracker_pb2.StatusDef(status='Started')
+ actual = converters.ConvertStatusDef(status_def)
+ self.assertEqual('Started', actual.status)
+ self.assertFalse(actual.means_open)
+ self.assertEqual('', actual.docstring)
+ self.assertFalse(actual.deprecated)
+ # rank is not set on output, only used when setting a new rank.
+ self.assertEqual(0, actual.rank)
+
+ status_def = tracker_pb2.StatusDef(
+ status='New', means_open=True, status_docstring='doc', deprecated=True)
+ actual = converters.ConvertStatusDef(status_def)
+ self.assertEqual('New', actual.status)
+ self.assertTrue(actual.means_open)
+ self.assertEqual('doc', actual.docstring)
+ self.assertTrue(actual.deprecated)
+ self.assertEqual(0, actual.rank)
+
+ def testConvertLabelDef(self):
+ """We can convert a label definition to protoc."""
+ label_def = tracker_pb2.LabelDef(label='Security')
+ actual = converters.ConvertLabelDef(label_def)
+ self.assertEqual('Security', actual.label)
+ self.assertEqual('', actual.docstring)
+ self.assertFalse(actual.deprecated)
+
+ label_def = tracker_pb2.LabelDef(
+ label='UI', label_docstring='doc', deprecated=True)
+ actual = converters.ConvertLabelDef(label_def)
+ self.assertEqual('UI', actual.label)
+ self.assertEqual('doc', actual.docstring)
+ self.assertTrue(actual.deprecated)
+
+ def testConvertComponentDef_Simple(self):
+ """We can convert a minimal component definition to protoc."""
+ now = 1234567890
+ component_def = tracker_pb2.ComponentDef(
+ path='Frontend', docstring='doc', created=now, creator_id=111,
+ modified=now + 1, modifier_id=111)
+ actual = converters.ConvertComponentDef(
+ component_def, self.users_by_id, {}, True)
+ self.assertEqual('Frontend', actual.path)
+ self.assertEqual('doc', actual.docstring)
+ self.assertFalse(actual.deprecated)
+ self.assertEqual(now, actual.created)
+ self.assertEqual(111, actual.creator_ref.user_id)
+ self.assertEqual(now + 1, actual.modified)
+ self.assertEqual(111, actual.modifier_ref.user_id)
+ self.assertEqual('one@example.com', actual.creator_ref.display_name)
+
+ def testConvertComponentDef_Normal(self):
+ """We can convert a component def that has CC'd users and adds labels."""
+ labels_by_id = {1: 'Security', 2: 'Usability'}
+ component_def = tracker_pb2.ComponentDef(
+ path='Frontend', admin_ids=[111], cc_ids=[222], label_ids=[1, 2],
+ docstring='doc')
+ actual = converters.ConvertComponentDef(
+ component_def, self.users_by_id, labels_by_id, True)
+ self.assertEqual('Frontend', actual.path)
+ self.assertEqual('doc', actual.docstring)
+ self.assertEqual(1, len(actual.admin_refs))
+ self.assertEqual(111, actual.admin_refs[0].user_id)
+ self.assertEqual(1, len(actual.cc_refs))
+ self.assertFalse(actual.deprecated)
+ self.assertEqual(222, actual.cc_refs[0].user_id)
+ self.assertEqual(2, len(actual.label_refs))
+ self.assertEqual('Security', actual.label_refs[0].label)
+ self.assertEqual('Usability', actual.label_refs[1].label)
+
+ # Without include_admin_info, some fields are not set.
+ actual = converters.ConvertComponentDef(
+ component_def, self.users_by_id, labels_by_id, False)
+ self.assertEqual('Frontend', actual.path)
+ self.assertEqual('doc', actual.docstring)
+ self.assertEqual(0, len(actual.admin_refs))
+ self.assertEqual(0, len(actual.cc_refs))
+ self.assertFalse(actual.deprecated)
+ self.assertEqual(0, len(actual.label_refs))
+
+ def testConvertFieldDef_Simple(self):
+ """We can convert a minimal field definition to protoc."""
+ field_def = tracker_pb2.FieldDef(
+ field_name='EstDays', field_type=tracker_pb2.FieldTypes.INT_TYPE)
+ actual = converters.ConvertFieldDef(
+ field_def, [], self.users_by_id, self.config, True)
+ self.assertEqual('EstDays', actual.field_ref.field_name)
+ self.assertEqual(common_pb2.INT_TYPE, actual.field_ref.type)
+ self.assertEqual('', actual.field_ref.approval_name)
+ self.assertEqual('', actual.applicable_type)
+ self.assertEqual('', actual.docstring)
+ self.assertEqual(0, len(actual.admin_refs))
+ self.assertFalse(actual.is_required)
+ self.assertFalse(actual.is_niche)
+ self.assertFalse(actual.is_multivalued)
+ self.assertFalse(actual.is_phase_field)
+
+ field_def = tracker_pb2.FieldDef(
+ field_name='DesignDocs', field_type=tracker_pb2.FieldTypes.URL_TYPE,
+ applicable_type='Enhancement', is_required=True, is_niche=True,
+ is_multivalued=True, docstring='doc', admin_ids=[111],
+ is_phase_field=True)
+ actual = converters.ConvertFieldDef(
+ field_def, [], self.users_by_id, self.config, True)
+ self.assertEqual('DesignDocs', actual.field_ref.field_name)
+ self.assertEqual(common_pb2.URL_TYPE, actual.field_ref.type)
+ self.assertEqual('', actual.field_ref.approval_name)
+ self.assertEqual('Enhancement', actual.applicable_type)
+ self.assertEqual('doc', actual.docstring)
+ self.assertEqual(1, len(actual.admin_refs))
+ self.assertEqual(111, actual.admin_refs[0].user_id)
+ self.assertTrue(actual.is_required)
+ self.assertTrue(actual.is_niche)
+ self.assertTrue(actual.is_multivalued)
+ self.assertTrue(actual.is_phase_field)
+
+ # Without include_admin_info, some fields are not set.
+ actual = converters.ConvertFieldDef(
+ field_def, [], self.users_by_id, self.config, False)
+ self.assertEqual('DesignDocs', actual.field_ref.field_name)
+ self.assertEqual(common_pb2.URL_TYPE, actual.field_ref.type)
+ self.assertEqual('', actual.field_ref.approval_name)
+ self.assertEqual('', actual.applicable_type)
+ self.assertEqual('doc', actual.docstring)
+ self.assertEqual(0, len(actual.admin_refs))
+ self.assertFalse(actual.is_required)
+ self.assertFalse(actual.is_niche)
+ self.assertFalse(actual.is_multivalued)
+ self.assertFalse(actual.is_phase_field)
+
+ def testConvertFieldDef_FieldOfAnApproval(self):
+ """We can convert a field that is part of an approval."""
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_3]
+ field_def = tracker_pb2.FieldDef(
+ field_name='Waiver', field_type=tracker_pb2.FieldTypes.URL_TYPE,
+ approval_id=self.fd_3.field_id)
+ actual = converters.ConvertFieldDef(
+ field_def, [], self.users_by_id, self.config, True)
+ self.assertEqual('Waiver', actual.field_ref.field_name)
+ self.assertEqual('LegalApproval', actual.field_ref.approval_name)
+
+ def testConvertFieldDef_UserChoices(self):
+ """We can convert an user type field that need special permissions."""
+ field_def = tracker_pb2.FieldDef(
+ field_name='PM', field_type=tracker_pb2.FieldTypes.USER_TYPE)
+ actual = converters.ConvertFieldDef(
+ field_def, [111, 333], self.users_by_id, self.config, False)
+ self.assertEqual('PM', actual.field_ref.field_name)
+ self.assertEqual(
+ [111, 333],
+ [user_ref.user_id for user_ref in actual.user_choices])
+ self.assertEqual(
+ ['one@example.com', 'banned@example.com'],
+ [user_ref.display_name for user_ref in actual.user_choices])
+
+ def testConvertFieldDef_EnumChoices(self):
+ """We can convert an enum type field."""
+ field_def = tracker_pb2.FieldDef(
+ field_name='Type', field_type=tracker_pb2.FieldTypes.ENUM_TYPE)
+ actual = converters.ConvertFieldDef(
+ field_def, [], self.users_by_id, self.config, False)
+ self.assertEqual('Type', actual.field_ref.field_name)
+ self.assertEqual(
+ ['Defect', 'Enhancement', 'Task', 'Other'],
+ [label_def.label for label_def in actual.enum_choices])
+
+ def testConvertApprovalDef(self):
+ """We can convert an ApprovalDef to protoc."""
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_3]
+ approval_def = tracker_pb2.ApprovalDef(approval_id=3)
+ actual = converters.ConvertApprovalDef(
+ approval_def, self.users_by_id, self.config, True)
+ self.assertEqual('LegalApproval', actual.field_ref.field_name)
+ self.assertEqual(common_pb2.APPROVAL_TYPE, actual.field_ref.type)
+ self.assertEqual(0, len(actual.approver_refs))
+ self.assertEqual('', actual.survey)
+
+ approval_def = tracker_pb2.ApprovalDef(
+ approval_id=3, approver_ids=[111], survey='What?')
+ actual = converters.ConvertApprovalDef(
+ approval_def, self.users_by_id, self.config, True)
+ self.assertEqual('LegalApproval', actual.field_ref.field_name)
+ self.assertEqual(common_pb2.APPROVAL_TYPE, actual.field_ref.type)
+ self.assertEqual(1, len(actual.approver_refs))
+ self.assertEqual(111, actual.approver_refs[0].user_id)
+ self.assertEqual('What?', actual.survey)
+
+ # Without include_admin_info, some fields are not set.
+ actual = converters.ConvertApprovalDef(
+ approval_def, self.users_by_id, self.config, False)
+ self.assertEqual('LegalApproval', actual.field_ref.field_name)
+ self.assertEqual(common_pb2.APPROVAL_TYPE, actual.field_ref.type)
+ self.assertEqual(0, len(actual.approver_refs))
+ self.assertEqual('', actual.survey)
+
+ def testConvertConfig_Simple(self):
+ """We can convert a simple config to protoc."""
+ actual = converters.ConvertConfig(
+ self.project, self.config, self.users_by_id, {})
+ self.assertEqual('proj', actual.project_name)
+ self.assertEqual(9, len(actual.status_defs))
+ self.assertEqual('New', actual.status_defs[0].status)
+ self.assertEqual(17, len(actual.label_defs))
+ self.assertEqual('Type-Defect', actual.label_defs[0].label)
+ self.assertEqual(
+ ['Type', 'Priority', 'Milestone'], actual.exclusive_label_prefixes)
+ self.assertEqual(0, len(actual.component_defs))
+ self.assertEqual(0, len(actual.field_defs))
+ self.assertEqual(0, len(actual.approval_defs))
+ self.assertEqual(False, actual.restrict_to_known)
+ self.assertEqual(
+ ['Duplicate'], [s.status for s in actual.statuses_offer_merge])
+
+ def testConvertConfig_Normal(self):
+ """We can convert a config with fields and components to protoc."""
+ labels_by_id = {1: 'Security', 2: 'Usability'}
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_3]
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path='UI', label_ids=[2])]
+ self.config.approval_defs.append(tracker_pb2.ApprovalDef(
+ approval_id=3, approver_ids=[111], survey='What?'))
+ self.config.restrict_to_known = True
+ self.config.statuses_offer_merge = ['Duplicate', 'New']
+ actual = converters.ConvertConfig(
+ self.project, self.config, self.users_by_id, labels_by_id)
+ self.assertEqual(1, len(actual.component_defs))
+ self.assertEqual(3, len(actual.field_defs))
+ self.assertEqual(1, len(actual.approval_defs))
+ self.assertEqual('proj', actual.project_name)
+ self.assertEqual(True, actual.restrict_to_known)
+ self.assertEqual(
+ ['Duplicate', 'New'],
+ sorted(s.status for s in actual.statuses_offer_merge))
+
+ def testConvertConfig_FiltersDeletedFieldDefs(self):
+ """Deleted fieldDefs don't make it into the config response."""
+ labels_by_id = {1: 'Security', 2: 'Usability'}
+ deleted_fd1 = tracker_pb2.FieldDef(
+ field_name='DeletedField', field_id=100,
+ field_type=tracker_pb2.FieldTypes.STR_TYPE,
+ applicable_type='',
+ is_deleted=True)
+ deleted_fd2 = tracker_pb2.FieldDef(
+ field_name='RemovedField', field_id=101,
+ field_type=tracker_pb2.FieldTypes.ENUM_TYPE,
+ applicable_type='',
+ is_deleted=True)
+ self.config.field_defs = [self.fd_1, self.fd_2, self.fd_3, deleted_fd1,
+ deleted_fd2]
+ actual = converters.ConvertConfig(
+ self.project, self.config, self.users_by_id, labels_by_id)
+ self.assertEqual(3, len(actual.field_defs))
+
+ def testConvertProjectTemplateDefs_Normal(self):
+ """We can convert protoc TemplateDefs."""
+ self.config.component_defs = [
+ tracker_pb2.ComponentDef(component_id=1, path="dude"),
+ ]
+ status_def_1 = tracker_pb2.StatusDef(status='New', means_open=True)
+ status_def_2 = tracker_pb2.StatusDef(status='Old', means_open=False)
+ self.config.well_known_statuses.extend([status_def_1, status_def_2])
+ owner = self.services.user.TestAddUser('owner@example.com', 111)
+ admin1 = self.services.user.TestAddUser('admin1@example.com', 222)
+ admin2 = self.services.user.TestAddUser('admin2@example.com', 333)
+ appr1 = self.services.user.TestAddUser('approver1@example.com', 444)
+ self.config.field_defs = [
+ self.fd_1, # STR_TYPE
+ self.fd_3, # APPROVAl_TYPE
+ self.fd_5, # ENUM_TYPE
+ self.fd_6, # INT_TYPE PHASE
+ self.fd_7, # ENUM_TYPE APPROVAL
+ ]
+ field_values = [
+ tracker_bizobj.MakeFieldValue(
+ self.fd_1.field_id, None, 'honk', None, None, None, False),
+ tracker_bizobj.MakeFieldValue(
+ self.fd_6.field_id, 78, None, None, None, None, False, phase_id=3)]
+ phases = [tracker_pb2.Phase(phase_id=3, name='phaseName')]
+ approval_values = [tracker_pb2.ApprovalValue(
+ approval_id=3, approver_ids=[appr1.user_id], phase_id=3)]
+ labels = ['ApprovalEnum-choice1', 'label-2', 'chicken']
+ templates = [
+ tracker_pb2.TemplateDef(
+ name='Chicken', content='description', summary='summary',
+ summary_must_be_edited=True, owner_id=111, status='New',
+ labels=labels, members_only=True,
+ owner_defaults_to_member=True,
+ admin_ids=[admin1.user_id, admin2.user_id],
+ field_values=field_values, component_ids=[1],
+ component_required=True, phases=phases,
+ approval_values=approval_values),
+ tracker_pb2.TemplateDef(name='Kale')]
+ users_by_id = {
+ owner.user_id: testing_helpers.Blank(
+ display_name=owner.email, email=owner.email, banned=False),
+ admin1.user_id: testing_helpers.Blank(
+ display_name=admin1.email, email=admin1.email, banned=False),
+ admin2.user_id: testing_helpers.Blank(
+ display_name=admin2.email, email=admin2.email, banned=True),
+ appr1.user_id: testing_helpers.Blank(
+ display_name=appr1.email, email=appr1.email, banned=False),
+ }
+ actual = converters.ConvertProjectTemplateDefs(
+ templates, users_by_id, self.config)
+ expected = [
+ project_objects_pb2.TemplateDef(
+ template_name='Chicken',
+ content='description',
+ summary='summary',
+ summary_must_be_edited=True,
+ owner_ref=common_pb2.UserRef(
+ user_id=owner.user_id,
+ display_name=owner.email,
+ is_derived=False),
+ status_ref=common_pb2.StatusRef(
+ status='New',
+ is_derived=False,
+ means_open=True),
+ label_refs=[
+ common_pb2.LabelRef(label='label-2', is_derived=False),
+ common_pb2.LabelRef(label='chicken', is_derived=False)],
+ members_only=True,
+ owner_defaults_to_member=True,
+ admin_refs=[
+ common_pb2.UserRef(
+ user_id=admin1.user_id,
+ display_name=admin1.email,
+ is_derived=False),
+ common_pb2.UserRef(
+ user_id=admin2.user_id,
+ display_name=admin2.email,
+ is_derived=False)],
+ field_values=[
+ issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=self.fd_7.field_id,
+ field_name=self.fd_7.field_name,
+ type=common_pb2.ENUM_TYPE),
+ value='choice1'),
+ issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=self.fd_1.field_id,
+ field_name=self.fd_1.field_name,
+ type=common_pb2.STR_TYPE),
+ value='honk'),
+ issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=self.fd_6.field_id,
+ field_name=self.fd_6.field_name,
+ type=common_pb2.INT_TYPE),
+ value='78',
+ phase_ref=issue_objects_pb2.PhaseRef(
+ phase_name='phaseName'))],
+ component_refs=[
+ common_pb2.ComponentRef(path='dude', is_derived=False)],
+ component_required=True,
+ phases=[issue_objects_pb2.PhaseDef(
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='phaseName'))],
+ approval_values=[
+ issue_objects_pb2.Approval(
+ field_ref=common_pb2.FieldRef(
+ field_id=self.fd_3.field_id,
+ field_name=self.fd_3.field_name,
+ type=common_pb2.APPROVAL_TYPE),
+ phase_ref=issue_objects_pb2.PhaseRef(phase_name='phaseName'),
+ approver_refs=[common_pb2.UserRef(
+ user_id=appr1.user_id,
+ display_name=appr1.email,
+ is_derived=False)])],
+ ),
+ project_objects_pb2.TemplateDef(
+ template_name='Kale',
+ status_ref=common_pb2.StatusRef(
+ status='----',
+ means_open=True),
+ owner_defaults_to_member=True)]
+ self.assertEqual(actual, expected)
+
+ def testConvertTemplateDefs_Empty(self):
+ """We can convert an empty list of protoc TemplateDefs."""
+ actual = converters.ConvertProjectTemplateDefs([], {}, self.config)
+ self.assertEqual(actual, [])
+
+ def testConvertHotlist(self):
+ """We can convert a hotlist to protoc."""
+ hotlist = fake.Hotlist(
+ 'Fake-hotlist', 123, is_private=True,
+ owner_ids=[self.user_1.user_id], editor_ids=[self.user_2.user_id],
+ follower_ids=[self.user_3.user_id])
+ hotlist.summary = 'A fake hotlist.'
+ hotlist.description = 'Detailed description of the fake hotlist.'
+ hotlist.default_col_spec = 'cows tho'
+ actual = converters.ConvertHotlist(hotlist, self.users_by_id)
+ self.assertEqual(actual,
+ features_objects_pb2.Hotlist(
+ name=hotlist.name,
+ summary=hotlist.summary,
+ description=hotlist.description,
+ default_col_spec=hotlist.default_col_spec,
+ is_private=hotlist.is_private,
+ owner_ref=common_pb2.UserRef(
+ display_name=self.user_1.email,
+ user_id=self.user_1.user_id),
+ editor_refs=[common_pb2.UserRef(
+ display_name=self.user_2.email,
+ user_id=self.user_2.user_id)],
+ follower_refs=[common_pb2.UserRef(
+ display_name=testing_helpers.ObscuredEmail(
+ self.user_3.email),
+ user_id=self.user_3.user_id)]))
+
+
+ def testConvertHotlistItem(self):
+ """We can convert a HotlistItem to protoc."""
+ project_2 = self.services.project.TestAddProject(
+ 'proj2', project_id=788)
+ config_2 = tracker_bizobj.MakeDefaultProjectIssueConfig(
+ project_2.project_id)
+ config_2.field_defs = [self.fd_2]
+ self.config.field_defs = [self.fd_1]
+
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[])
+ self.services.features.UpdateHotlistItems(
+ self.cnxn, hotlist.hotlist_id, [],
+ [(self.issue_1.issue_id, 222, 12345, 'Note')])
+ issues_by_id = {self.issue_1.issue_id: self.issue_1}
+ related_refs = {}
+ harmonized_config = tracker_bizobj.HarmonizeConfigs([self.config, config_2])
+
+ actual = converters.ConvertHotlistItems(
+ hotlist.items, issues_by_id, self.users_by_id, related_refs,
+ harmonized_config)
+
+ expected_issue = converters.ConvertIssue(
+ self.issue_1, self.users_by_id, related_refs, harmonized_config)
+ self.assertEqual(
+ [features_objects_pb2.HotlistItem(
+ issue=expected_issue,
+ rank=1,
+ adder_ref=common_pb2.UserRef(
+ user_id=222,
+ display_name='two@example.com'),
+ added_timestamp=12345,
+ note='Note')],
+ actual)
+
+ def testConvertValueAndWhy(self):
+ """We can covert a dict wth 'why' and 'value' fields to a ValueAndWhy PB."""
+ actual = converters.ConvertValueAndWhy({'value': 'Foo', 'why': 'Because'})
+ self.assertEqual(
+ common_pb2.ValueAndWhy(value='Foo', why='Because'),
+ actual)
+
+ def testConvertValueAndWhyList(self):
+ """We can convert a list of value and why dicts."""
+ actual = converters.ConvertValueAndWhyList([
+ {'value': 'A', 'why': 'Because A'},
+ {'value': 'B'},
+ {'why': 'Why what?'},
+ {}])
+ self.assertEqual(
+ [common_pb2.ValueAndWhy(value='A', why='Because A'),
+ common_pb2.ValueAndWhy(value='B'),
+ common_pb2.ValueAndWhy(why='Why what?'),
+ common_pb2.ValueAndWhy()],
+ actual)
+
+ def testRedistributeEnumFieldsIntoLabels(self):
+ # function called and tests covered by
+ # IngestIssueDelta and IngestApprovalDelta
+ pass
diff --git a/api/test/features_servicer_test.py b/api/test/features_servicer_test.py
new file mode 100644
index 0000000..7a7180b
--- /dev/null
+++ b/api/test/features_servicer_test.py
@@ -0,0 +1,1039 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for the projects servicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import json
+import mock
+import unittest
+import mox
+
+from google.protobuf import wrappers_pb2
+
+from components.prpc import codes
+from components.prpc import context
+from components.prpc import server
+
+from api import converters
+from api.api_proto import common_pb2
+from api.api_proto import features_pb2
+from api.api_proto import features_objects_pb2
+from api.api_proto import issue_objects_pb2
+from framework import authdata
+from framework import exceptions
+from framework import monorailcontext
+from framework import permissions
+from framework import sorting
+from testing import fake
+from testing import testing_helpers
+from tracker import tracker_bizobj
+from services import features_svc
+from services import service_manager
+
+# Import component_helpers_test to mock cloudstorage before it is imported by
+# component_helpers via features servicer.
+from features.test import component_helpers_test
+from api import features_servicer # pylint: disable=ungrouped-imports
+
+
+class FeaturesServicerTest(unittest.TestCase):
+
+ def setUp(self):
+ self.mox = mox.Mox()
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ cache_manager=fake.CacheManager(),
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService(),
+ project=fake.ProjectService(),
+ features=fake.FeaturesService(),
+ hotlist_star=fake.HotlistStarService())
+ sorting.InitializeArtValues(self.services)
+
+ self.project = self.services.project.TestAddProject(
+ 'proj', project_id=789, owner_ids=[111], contrib_ids=[222, 333])
+ self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(789)
+ self.user1 = self.services.user.TestAddUser('owner@example.com', 111)
+ self.user2 = self.services.user.TestAddUser('editor@example.com', 222)
+ self.user3 = self.services.user.TestAddUser('foo@example.com', 333)
+ self.user4 = self.services.user.TestAddUser('bar@example.com', 444)
+ self.features_svcr = features_servicer.FeaturesServicer(
+ self.services, make_rate_limiter=False)
+ self.prpc_context = context.ServicerContext()
+ self.prpc_context.set_code(codes.StatusCode.OK)
+ self.issue_1 = fake.MakeTestIssue(
+ 789, 1, 'sum', 'New', 111, project_name='proj', issue_id=78901)
+ self.issue_2 = fake.MakeTestIssue(
+ 789, 2, 'sum', 'Fixed', 111, project_name='proj', issue_id=78902,
+ closed_timestamp=112223344)
+ self.issue_3 = fake.MakeTestIssue(
+ 789, 3, 'sum', 'New', 111, project_name='proj', issue_id=78903)
+
+ self.services.issue.TestAddIssue(self.issue_1)
+ self.services.issue.TestAddIssue(self.issue_2)
+ self.services.issue.TestAddIssue(self.issue_3)
+
+ self.project_2 = self.services.project.TestAddProject(
+ 'proj2', project_id=788, owner_ids=[111], contrib_ids=[222, 333])
+ self.config_2 = tracker_bizobj.MakeDefaultProjectIssueConfig(788)
+ self.issue_21 = fake.MakeTestIssue(
+ 788, 1, 'sum', 'New', 111, project_name='proj2', issue_id=78801)
+ self.issue_22 = fake.MakeTestIssue(
+ 788, 2, 'sum', 'New', 111, project_name='proj2', issue_id=78802)
+ self.issue_23 = fake.MakeTestIssue(
+ 788, 3, 'sum', 'New', 111, project_name='proj2', issue_id=78803)
+ self.services.issue.TestAddIssue(self.issue_21)
+ self.services.issue.TestAddIssue(self.issue_22)
+ self.services.issue.TestAddIssue(self.issue_23)
+
+ self.PAST_TIME = 123456
+
+ # For testing PredictComponent
+ self._ml_engine = component_helpers_test.FakeMLEngine(self)
+ self._top_words = None
+ self._components_by_index = None
+
+ mock.patch(
+ 'services.ml_helpers.setup_ml_engine', lambda: self._ml_engine).start()
+ mock.patch(
+ 'features.component_helpers._GetTopWords',
+ lambda _: self._top_words).start()
+ mock.patch('cloudstorage.open', self.cloudstorageOpen).start()
+ mock.patch('settings.component_features', 5).start()
+
+ self.addCleanup(mock.patch.stopall)
+
+ def cloudstorageOpen(self, name, mode):
+ """Create a file mock that returns self._components_by_index when read."""
+ open_fn = mock.mock_open(read_data=json.dumps(self._components_by_index))
+ return open_fn(name, mode)
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def CallWrapped(self, wrapped_handler, *args, **kwargs):
+ return wrapped_handler.wrapped(self.features_svcr, *args, **kwargs)
+
+ def testListHotlistsByUser_SearchByEmail(self):
+ """We can get a list of hotlists for a given email."""
+ # Public hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ # Query for issues for 'owner@example.com'
+ user_ref = common_pb2.UserRef(display_name='owner@example.com')
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're not authenticated
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+ self.assertEqual(1, len(response.hotlists))
+ hotlist = response.hotlists[0]
+ self.assertEqual(111, hotlist.owner_ref.user_id)
+ self.assertEqual('ow...@example.com', hotlist.owner_ref.display_name)
+ self.assertEqual('Fake-Hotlist', hotlist.name)
+ self.assertEqual('Summary', hotlist.summary)
+ self.assertEqual('Description', hotlist.description)
+
+ def testListHotlistsByUser_SearchByOwner(self):
+ """We can get a list of hotlists for a given user."""
+ # Public hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ # Query for issues for 'owner@example.com'
+ user_ref = common_pb2.UserRef(user_id=111)
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're authenticated as 'foo@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+ self.assertEqual(1, len(response.hotlists))
+ hotlist = response.hotlists[0]
+ self.assertEqual(111, hotlist.owner_ref.user_id)
+ # User1 and user3 share self.project.
+ self.assertEqual('owner@example.com', hotlist.owner_ref.display_name)
+ self.assertEqual('Fake-Hotlist', hotlist.name)
+ self.assertEqual('Summary', hotlist.summary)
+ self.assertEqual('Description', hotlist.description)
+
+ def testListHotlistsByUser_SearchByEditor(self):
+ """We can get a list of hotlists for a given user."""
+ # Public hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ # Query for issues for 'editor@example.com'
+ user_ref = common_pb2.UserRef(user_id=222)
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're authenticated as 'foo@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+ self.assertEqual(1, len(response.hotlists))
+ hotlist = response.hotlists[0]
+ self.assertEqual(111, hotlist.owner_ref.user_id)
+ # User1 and user3 share self.project.
+ self.assertEqual('owner@example.com', hotlist.owner_ref.display_name)
+ self.assertEqual('Fake-Hotlist', hotlist.name)
+ self.assertEqual('Summary', hotlist.summary)
+ self.assertEqual('Description', hotlist.description)
+
+ def testListHotlistsByUser_NotSignedIn(self):
+ # Public hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ # Query for issues for 'owner@example.com'
+ user_ref = common_pb2.UserRef(user_id=111)
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're not authenticated
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+
+ self.assertEqual(1, len(response.hotlists))
+ hotlist = response.hotlists[0]
+ self.assertEqual(111, hotlist.owner_ref.user_id)
+
+ def testListHotlistsByUser_Empty(self):
+ """There are no hotlists for the given user."""
+ # Public hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ # Query for issues for 'bar@example.com'
+ user_ref = common_pb2.UserRef(user_id=444)
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're authenticated as 'foo@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+
+ self.assertEqual(0, len(response.hotlists))
+
+ def testListHotlistsByUser_NoHotlists(self):
+ """There are no hotlists."""
+ # No hotlists
+ # Query for issues for 'owner@example.com'
+ user_ref = common_pb2.UserRef(user_id=111)
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're authenticated as 'foo@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+ self.assertEqual(0, len(response.hotlists))
+
+ def testListHotlistsByUser_PrivateIssueAsOwner(self):
+ # Private hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222], is_private=True)
+
+ # Query for issues for 'owner@example.com'
+ user_ref = common_pb2.UserRef(user_id=111)
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're authenticated as 'owner@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+
+ self.assertEqual(1, len(response.hotlists))
+ hotlist = response.hotlists[0]
+ self.assertEqual(111, hotlist.owner_ref.user_id)
+
+ def testListHotlistsByUser_PrivateIssueAsEditor(self):
+ # Private hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222], is_private=True)
+
+ # Query for issues for 'owner@example.com'
+ user_ref = common_pb2.UserRef(user_id=111)
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're authenticated as 'editor@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='editor@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+
+ self.assertEqual(1, len(response.hotlists))
+ hotlist = response.hotlists[0]
+ self.assertEqual(111, hotlist.owner_ref.user_id)
+
+ def testListHotlistsByUser_PrivateIssueNoAccess(self):
+ # Private hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222], is_private=True)
+
+ # Query for issues for 'owner@example.com'
+ user_ref = common_pb2.UserRef(user_id=111)
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're authenticated as 'foo@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+
+ self.assertEqual(0, len(response.hotlists))
+
+ def testListHotlistsByUser_PrivateIssueNotSignedIn(self):
+ # Private hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222], is_private=True)
+
+ # Query for issues for 'owner@example.com'
+ user_ref = common_pb2.UserRef(user_id=111)
+ request = features_pb2.ListHotlistsByUserRequest(user=user_ref)
+
+ # We're not authenticated
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByUser, mc,
+ request)
+
+ self.assertEqual(0, len(response.hotlists))
+
+ def AddIssueToHotlist(self, hotlist_id, issue_id=78901, adder_id=111):
+ self.services.features.AddIssuesToHotlists(
+ self.cnxn, [hotlist_id], [(issue_id, adder_id, 0, '')],
+ None, None, None)
+
+ def testListHotlistsByIssue_Normal(self):
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+ self.AddIssueToHotlist(hotlist.hotlist_id)
+
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ request = features_pb2.ListHotlistsByIssueRequest(issue=issue_ref)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByIssue, mc,
+ request)
+
+ self.assertEqual(1, len(response.hotlists))
+ hotlist = response.hotlists[0]
+ self.assertEqual('Fake-Hotlist', hotlist.name)
+
+ def testListHotlistsByIssue_NotSignedIn(self):
+ # Public hostlist owned by 'owner@example.com'
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+ self.AddIssueToHotlist(hotlist.hotlist_id)
+
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ request = features_pb2.ListHotlistsByIssueRequest(issue=issue_ref)
+
+ # We're not authenticated
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByIssue, mc,
+ request)
+
+ self.assertEqual(1, len(response.hotlists))
+ hotlist = response.hotlists[0]
+ self.assertEqual('Fake-Hotlist', hotlist.name)
+
+ def testListHotlistsByIssue_Empty(self):
+ """There are no hotlists with the given issue."""
+ # Public hostlist owned by 'owner@example.com'
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ request = features_pb2.ListHotlistsByIssueRequest(issue=issue_ref)
+
+ # We're authenticated as 'foo@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByIssue, mc,
+ request)
+
+ self.assertEqual(0, len(response.hotlists))
+
+ def testListHotlistsByIssue_NoHotlists(self):
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ request = features_pb2.ListHotlistsByIssueRequest(issue=issue_ref)
+
+ # We're authenticated as 'foo@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByIssue, mc,
+ request)
+ self.assertEqual(0, len(response.hotlists))
+
+ def testListHotlistsByIssue_PrivateHotlistAsOwner(self):
+ """An owner can view their private issues."""
+ # Private hostlist owned by 'owner@example.com'
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222], is_private=True)
+ self.AddIssueToHotlist(hotlist.hotlist_id)
+
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ request = features_pb2.ListHotlistsByIssueRequest(issue=issue_ref)
+
+ # We're authenticated as 'owner@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByIssue, mc,
+ request)
+
+ self.assertEqual(1, len(response.hotlists))
+ hotlist = response.hotlists[0]
+ self.assertEqual('Fake-Hotlist', hotlist.name)
+
+ def testListHotlistsByIssue_PrivateHotlistNoAccess(self):
+ # Private hostlist owned by 'owner@example.com'
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222], is_private=True)
+ self.AddIssueToHotlist(hotlist.hotlist_id)
+
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ request = features_pb2.ListHotlistsByIssueRequest(issue=issue_ref)
+
+ # We're authenticated as 'foo@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+ response = self.CallWrapped(self.features_svcr.ListHotlistsByIssue, mc,
+ request)
+
+ self.assertEqual(0, len(response.hotlists))
+
+ def testListHotlistsByIssue_NonProjectHotlists(self):
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn,
+ 'Fake-Hotlist',
+ 'Summary',
+ 'Description',
+ owner_ids=[111],
+ editor_ids=[222])
+ spam_hotlist = self.services.features.CreateHotlist(
+ self.cnxn,
+ 'Spam-Hotlist',
+ 'Summary',
+ 'Description',
+ owner_ids=[444],
+ editor_ids=[])
+ another_hotlist = self.services.features.CreateHotlist(
+ self.cnxn,
+ 'Another-Hotlist',
+ 'Summary',
+ 'Description',
+ owner_ids=[111],
+ editor_ids=[])
+ self.AddIssueToHotlist(hotlist.hotlist_id)
+ self.AddIssueToHotlist(spam_hotlist.hotlist_id)
+ self.AddIssueToHotlist(another_hotlist.hotlist_id)
+
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ request = features_pb2.ListHotlistsByIssueRequest(issue=issue_ref)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+ response = self.CallWrapped(
+ self.features_svcr.ListHotlistsByIssue, mc, request)
+
+ self.assertEqual(2, len(response.hotlists))
+ self.assertEqual('Fake-Hotlist', response.hotlists[0].name)
+ self.assertEqual('Another-Hotlist', response.hotlists[1].name)
+
+ def testListRecentlyVisitedHotlists(self):
+ hotlists = [
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[self.user2.user_id], editor_ids=[self.user1.user_id],
+ default_col_spec='chicken'),
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist-2', 'Summary', 'Description',
+ owner_ids=[self.user1.user_id], editor_ids=[self.user2.user_id],
+ default_col_spec='honk'),
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Private-Hotlist', 'Summary', 'Description',
+ owner_ids=[self.user3.user_id], editor_ids=[self.user2.user_id],
+ is_private=True)]
+
+ for hotlist in hotlists:
+ self.services.user.AddVisitedHotlist(
+ self.cnxn, self.user1.user_id, hotlist.hotlist_id)
+
+ request = features_pb2.ListRecentlyVisitedHotlistsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user1.email)
+ response = self.CallWrapped(
+ self.features_svcr.ListRecentlyVisitedHotlists, mc, request)
+
+ expected_hotlists = [
+ features_objects_pb2.Hotlist(
+ owner_ref=common_pb2.UserRef(
+ user_id=self.user2.user_id, display_name=self.user2.email),
+ editor_refs=[
+ common_pb2.UserRef(
+ user_id=self.user1.user_id, display_name=self.user1.email)
+ ],
+ name='Fake-Hotlist',
+ summary='Summary',
+ description='Description',
+ is_private=False,
+ default_col_spec='chicken'),
+ features_objects_pb2.Hotlist(
+ owner_ref=common_pb2.UserRef(
+ user_id=self.user1.user_id, display_name=self.user1.email),
+ editor_refs=[
+ common_pb2.UserRef(
+ user_id=self.user2.user_id, display_name=self.user2.email)
+ ],
+ name='Fake-Hotlist-2',
+ summary='Summary',
+ description='Description',
+ is_private=False,
+ default_col_spec='honk')
+ ]
+
+ # We don't have permission to see the last issue, because it is marked as
+ # private and we're not owners or editors.
+ self.assertEqual(expected_hotlists, list(response.hotlists))
+
+ def testListRecentlyVisitedHotlists_Anon(self):
+ request = features_pb2.ListRecentlyVisitedHotlistsRequest()
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ response = self.CallWrapped(
+ self.features_svcr.ListRecentlyVisitedHotlists, mc, request)
+ self.assertEqual(0, len(response.hotlists))
+
+ def testListStarredHotlists(self):
+ hotlists = [
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[self.user2.user_id], editor_ids=[self.user1.user_id],
+ default_col_spec='cow chicken'),
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist-2', 'Summary', 'Description',
+ owner_ids=[self.user1.user_id],
+ editor_ids=[self.user2.user_id, self.user3.user_id],
+ default_col_spec=''),
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Private-Hotlist', 'Summary', 'Description',
+ owner_ids=[self.user3.user_id], editor_ids=[self.user2.user_id],
+ is_private=True, default_col_spec='chicken')]
+
+ for hotlist in hotlists:
+ self.services.hotlist_star.SetStar(
+ self.cnxn, hotlist.hotlist_id, self.user1.user_id, True)
+
+ request = features_pb2.ListStarredHotlistsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.features_svcr.ListStarredHotlists, mc, request)
+
+ expected_hotlists = [
+ features_objects_pb2.Hotlist(
+ owner_ref=common_pb2.UserRef(
+ user_id=self.user2.user_id, display_name=self.user2.email),
+ editor_refs=[
+ common_pb2.UserRef(
+ user_id=self.user1.user_id, display_name=self.user1.email)
+ ],
+ name='Fake-Hotlist',
+ summary='Summary',
+ description='Description',
+ is_private=False,
+ default_col_spec='cow chicken'),
+ features_objects_pb2.Hotlist(
+ owner_ref=common_pb2.UserRef(
+ user_id=self.user1.user_id, display_name=self.user1.email),
+ editor_refs=[
+ common_pb2.UserRef(
+ user_id=self.user2.user_id, display_name=self.user2.email),
+ common_pb2.UserRef(
+ user_id=self.user3.user_id, display_name=self.user3.email)
+ ],
+ name='Fake-Hotlist-2',
+ summary='Summary',
+ description='Description',
+ is_private=False)
+ ]
+
+ # We don't have permission to see the last issue, because it is marked as
+ # private and we're not owners or editors.
+ self.assertEqual(expected_hotlists, list(response.hotlists))
+
+ def testListStarredHotlists_Anon(self):
+ request = features_pb2.ListStarredHotlistsRequest()
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ response = self.CallWrapped(
+ self.features_svcr.ListStarredHotlists, mc, request)
+ self.assertEqual(0, len(response.hotlists))
+
+ def CallGetStarCount(self):
+ # Query for hotlists for 'owner@example.com'
+ owner_ref = common_pb2.UserRef(user_id=111)
+ hotlist_ref = common_pb2.HotlistRef(name='Fake-Hotlist', owner=owner_ref)
+ request = features_pb2.GetHotlistStarCountRequest(hotlist_ref=hotlist_ref)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.features_svcr.GetHotlistStarCount, mc, request)
+ return response.star_count
+
+ def CallStar(self, requester='owner@example.com', starred=True):
+ # Query for hotlists for 'owner@example.com'
+ owner_ref = common_pb2.UserRef(user_id=111)
+ hotlist_ref = common_pb2.HotlistRef(name='Fake-Hotlist', owner=owner_ref)
+ request = features_pb2.StarHotlistRequest(
+ hotlist_ref=hotlist_ref, starred=starred)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=requester)
+ response = self.CallWrapped(
+ self.features_svcr.StarHotlist, mc, request)
+ return response.star_count
+
+ def testStarCount_Normal(self):
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+ self.assertEqual(0, self.CallGetStarCount())
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallGetStarCount())
+
+ def testStarCount_StarTwiceSameUser(self):
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallGetStarCount())
+
+ def testStarCount_StarTwiceDifferentUser(self):
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(2, self.CallStar(requester='user_222@example.com'))
+ self.assertEqual(2, self.CallGetStarCount())
+
+ def testStarCount_RemoveStarTwiceSameUser(self):
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallGetStarCount())
+
+ self.assertEqual(0, self.CallStar(starred=False))
+ self.assertEqual(0, self.CallStar(starred=False))
+ self.assertEqual(0, self.CallGetStarCount())
+
+ def testStarCount_RemoveStarTwiceDifferentUser(self):
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[222])
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(2, self.CallStar(requester='user_222@example.com'))
+ self.assertEqual(2, self.CallGetStarCount())
+
+ self.assertEqual(1, self.CallStar(starred=False))
+ self.assertEqual(
+ 0, self.CallStar(requester='user_222@example.com', starred=False))
+ self.assertEqual(0, self.CallGetStarCount())
+
+ def testGetHotlist(self):
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[self.user3.user_id], editor_ids=[self.user4.user_id],
+ is_private=True, default_col_spec='corgi butts')
+
+ owner_ref = common_pb2.UserRef(user_id=self.user3.user_id)
+ hotlist_ref = common_pb2.HotlistRef(
+ name=hotlist.name, owner=owner_ref)
+ request = features_pb2.GetHotlistRequest(hotlist_ref=hotlist_ref)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user4.email)
+ mc.LookupLoggedInUserPerms(None)
+ response = self.CallWrapped(
+ self.features_svcr.GetHotlist, mc, request)
+
+ self.assertEqual(
+ response.hotlist,
+ features_objects_pb2.Hotlist(
+ owner_ref=common_pb2.UserRef(
+ user_id=self.user3.user_id,
+ display_name=testing_helpers.ObscuredEmail(self.user3.email)),
+ editor_refs=[common_pb2.UserRef(
+ user_id=self.user4.user_id,
+ display_name=self.user4.email)],
+ name=hotlist.name,
+ summary=hotlist.summary,
+ description=hotlist.description,
+ default_col_spec='corgi butts',
+ is_private=True))
+
+ def testGetHotlist_BadInput(self):
+ hotlist_ref = common_pb2.HotlistRef()
+ request = features_pb2.GetHotlistRequest(hotlist_ref=hotlist_ref)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.com')
+ with self.assertRaises(features_svc.NoSuchHotlistException):
+ self.CallWrapped(self.features_svcr.GetHotlist, mc, request)
+
+ def testCreateHotlist_Normal(self):
+ request = features_pb2.CreateHotlistRequest(
+ name='Fake-Hotlist',
+ summary='Summary',
+ description='Description',
+ editor_refs=[
+ common_pb2.UserRef(user_id=222),
+ common_pb2.UserRef(display_name='foo@example.com')],
+ issue_refs=[
+ common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='proj', local_id=2)],
+ is_private=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.features_svcr.CreateHotlist, mc, request)
+
+ # Check that the hotlist was successfuly added.
+ hotlist_id = self.services.features.LookupHotlistIDs(
+ self.cnxn, ['Fake-Hotlist'], [111]).get(('fake-hotlist', 111))
+ hotlist = self.services.features.GetHotlist(self.cnxn, hotlist_id)
+ self.assertEqual('Summary', hotlist.summary)
+ self.assertEqual('Description', hotlist.description)
+ self.assertEqual([111], hotlist.owner_ids)
+ self.assertEqual([222, 333], hotlist.editor_ids)
+ self.assertEqual(
+ [self.issue_1.issue_id, self.issue_2.issue_id],
+ [item.issue_id for item in hotlist.items])
+ self.assertTrue(hotlist.is_private)
+
+ def testCreateHotlist_Simple(self):
+ request = features_pb2.CreateHotlistRequest(
+ name='Fake-Hotlist',
+ summary='Summary',
+ description='Description')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.features_svcr.CreateHotlist, mc, request)
+
+ # Check that the hotlist was successfuly added.
+ hotlist_id = self.services.features.LookupHotlistIDs(
+ self.cnxn, ['Fake-Hotlist'], [111]).get(('fake-hotlist', 111))
+ hotlist = self.services.features.GetHotlist(self.cnxn, hotlist_id)
+ self.assertEqual('Summary', hotlist.summary)
+ self.assertEqual('Description', hotlist.description)
+ self.assertEqual([111], hotlist.owner_ids)
+ self.assertEqual([], hotlist.editor_ids)
+ self.assertEqual(0, len(hotlist.items))
+ self.assertFalse(hotlist.is_private)
+
+ def testCheckHotlistName_OK(self):
+ request = features_pb2.CheckHotlistNameRequest(name='Fake-Hotlist')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ result = self.CallWrapped(self.features_svcr.CheckHotlistName, mc, request)
+ self.assertEqual('', result.error)
+
+ def testCheckHotlistName_Anon(self):
+ request = features_pb2.CheckHotlistNameRequest(name='Fake-Hotlist')
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.features_svcr.CheckHotlistName, mc, request)
+
+ def testCheckHotlistName_InvalidName(self):
+ request = features_pb2.CheckHotlistNameRequest(name='**Invalid**')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ result = self.CallWrapped(self.features_svcr.CheckHotlistName, mc, request)
+ self.assertNotEqual('', result.error)
+
+ def testCheckHotlistName_AlreadyExists(self):
+ self.services.features.CreateHotlist(
+ self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
+ owner_ids=[111], editor_ids=[])
+
+ request = features_pb2.CheckHotlistNameRequest(name='Fake-Hotlist')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ result = self.CallWrapped(self.features_svcr.CheckHotlistName, mc, request)
+ self.assertNotEqual('', result.error)
+
+ def testRemoveIssuesFromHotlists(self):
+ # Create two hotlists with issues 1 and 2.
+ hotlist_1 = self.services.features.CreateHotlist(
+ self.cnxn, 'Hotlist-1', 'Summary', 'Description', owner_ids=[111],
+ editor_ids=[])
+ hotlist_2 = self.services.features.CreateHotlist(
+ self.cnxn, 'Hotlist-2', 'Summary', 'Description', owner_ids=[111],
+ editor_ids=[])
+ self.services.features.AddIssuesToHotlists(
+ self.cnxn,
+ [hotlist_1.hotlist_id, hotlist_2.hotlist_id],
+ [(self.issue_1.issue_id, 111, 0, ''),
+ (self.issue_2.issue_id, 111, 0, '')],
+ None, None, None)
+
+ # Remove Issue 1 from both hotlists.
+ request = features_pb2.RemoveIssuesFromHotlistsRequest(
+ hotlist_refs=[
+ common_pb2.HotlistRef(
+ name='Hotlist-1',
+ owner=common_pb2.UserRef(user_id=111)),
+ common_pb2.HotlistRef(
+ name='Hotlist-2',
+ owner=common_pb2.UserRef(user_id=111))],
+ issue_refs=[
+ common_pb2.IssueRef(project_name='proj', local_id=1)])
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.features_svcr.RemoveIssuesFromHotlists, mc, request)
+
+ # Only Issue 2 should remain in both lists.
+ self.assertEqual(
+ [self.issue_2.issue_id],
+ [item.issue_id for item in hotlist_1.items])
+ self.assertEqual(
+ [self.issue_2.issue_id],
+ [item.issue_id for item in hotlist_2.items])
+
+ def testAddIssuesToHotlists(self):
+ # Create two hotlists
+ hotlist_1 = self.services.features.CreateHotlist(
+ self.cnxn, 'Hotlist-1', 'Summary', 'Description', owner_ids=[111],
+ editor_ids=[])
+ hotlist_2 = self.services.features.CreateHotlist(
+ self.cnxn, 'Hotlist-2', 'Summary', 'Description', owner_ids=[111],
+ editor_ids=[])
+
+ # Add Issue 1 to both hotlists
+ request = features_pb2.AddIssuesToHotlistsRequest(
+ note='Foo',
+ hotlist_refs=[
+ common_pb2.HotlistRef(
+ name='Hotlist-1',
+ owner=common_pb2.UserRef(user_id=111)),
+ common_pb2.HotlistRef(
+ name='Hotlist-2',
+ owner=common_pb2.UserRef(user_id=111))],
+ issue_refs=[
+ common_pb2.IssueRef(project_name='proj', local_id=1)])
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.features_svcr.AddIssuesToHotlists, mc, request)
+
+ self.assertEqual(
+ [self.issue_1.issue_id],
+ [item.issue_id for item in hotlist_1.items])
+ self.assertEqual(
+ [self.issue_1.issue_id],
+ [item.issue_id for item in hotlist_2.items])
+
+ self.assertEqual('Foo', hotlist_1.items[0].note)
+ self.assertEqual('Foo', hotlist_2.items[0].note)
+
+ def testRerankHotlistIssues(self):
+ """Rerank a hotlist."""
+ issue_3 = fake.MakeTestIssue(
+ 789, 3, 'sum', 'New', 111, project_name='proj', issue_id=78903)
+ issue_4 = fake.MakeTestIssue(
+ 789, 4, 'sum', 'New', 111, project_name='proj', issue_id=78904)
+ self.services.issue.TestAddIssue(issue_3)
+ self.services.issue.TestAddIssue(issue_4)
+
+ owner_ids = [self.user1.user_id]
+ follower_ids = [self.user2.user_id]
+ editor_ids = [self.user3.user_id]
+ hotlist_items = [
+ (78904, 31, self.user2.user_id, self.PAST_TIME, 'note'),
+ (78903, 21, self.user2.user_id, self.PAST_TIME, 'note'),
+ (78902, 11, self.user2.user_id, self.PAST_TIME, 'note'),
+ (78901, 1, self.user2.user_id, self.PAST_TIME, 'note')]
+ hotlist = self.services.features.TestAddHotlist(
+ 'RerankHotlistName', summary='summary', owner_ids=owner_ids,
+ editor_ids=editor_ids, follower_ids=follower_ids,
+ hotlist_id=1236, hotlist_item_fields=hotlist_items)
+
+ request = features_pb2.RerankHotlistIssuesRequest(
+ hotlist_ref=common_pb2.HotlistRef(
+ name='RerankHotlistName',
+ owner=common_pb2.UserRef(user_id=self.user1.user_id)),
+ moved_refs=[common_pb2.IssueRef(
+ project_name='proj', local_id=2)],
+ target_ref=common_pb2.IssueRef(project_name='proj', local_id=4),
+ split_above=True)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user1.email)
+ mc.LookupLoggedInUserPerms(self.project)
+ self.CallWrapped(self.features_svcr.RerankHotlistIssues, mc, request)
+
+ self.assertEqual(
+ [item.issue_id for item in hotlist.items],
+ [78901, 78903, 78902, 78904])
+
+ def testUpdateHotlistIssueNote(self):
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Hotlist-1', 'Summary', 'Description', owner_ids=[111],
+ editor_ids=[])
+ self.services.features.AddIssuesToHotlists(
+ self.cnxn,
+ [hotlist.hotlist_id], [(self.issue_1.issue_id, 111, 0, '')],
+ None, None, None)
+
+ request = features_pb2.UpdateHotlistIssueNoteRequest(
+ hotlist_ref=common_pb2.HotlistRef(
+ name='Hotlist-1',
+ owner=common_pb2.UserRef(user_id=111)),
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ note='Note')
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.features_svcr.UpdateHotlistIssueNote, mc, request)
+
+ self.assertEqual('Note', hotlist.items[0].note)
+
+ def testUpdateHotlistIssueNote_NotAllowed(self):
+ hotlist = self.services.features.CreateHotlist(
+ self.cnxn, 'Hotlist-1', 'Summary', 'Description', owner_ids=[222],
+ editor_ids=[])
+ self.services.features.AddIssuesToHotlists(
+ self.cnxn,
+ [hotlist.hotlist_id], [(self.issue_1.issue_id, 222, 0, '')],
+ None, None, None)
+
+ request = features_pb2.UpdateHotlistIssueNoteRequest(
+ hotlist_ref=common_pb2.HotlistRef(
+ name='Hotlist-1',
+ owner=common_pb2.UserRef(user_id=222)),
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ note='Note')
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.features_svcr.UpdateHotlistIssueNote, mc, request)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user2.email)
+ mc.LookupLoggedInUserPerms(None)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user2.email)
+ mc.LookupLoggedInUserPerms(None)
+
+ def testDeleteHotlist(self):
+ """Test we can delete a hotlist via the API."""
+ owner_ids = [self.user2.user_id]
+ editor_ids = []
+ hotlist = self.services.features.TestAddHotlist(
+ name='Hotlist-1', summary='summary', description='description',
+ owner_ids=owner_ids, editor_ids=editor_ids, hotlist_id=1235)
+ request = features_pb2.DeleteHotlistRequest(
+ hotlist_ref=common_pb2.HotlistRef(
+ name=hotlist.name,
+ owner=common_pb2.UserRef(user_id=self.user2.user_id)))
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user2.email)
+ mc.LookupLoggedInUserPerms(None)
+ self.CallWrapped(self.features_svcr.DeleteHotlist, mc, request)
+
+ self.assertTrue(
+ hotlist.hotlist_id in self.services.features.expunged_hotlist_ids)
+
+ def testPredictComponent_Normal(self):
+ """Test normal case when predicted component exists."""
+ component_id = self.services.config.CreateComponentDef(
+ cnxn=None, project_id=self.project.project_id, path='Ruta>Baga',
+ docstring='', deprecated=False, admin_ids=[], cc_ids=[], created=None,
+ creator_id=None, label_ids=[])
+
+ self._top_words = {
+ 'foo': 0,
+ 'bar': 1,
+ 'baz': 2}
+ self._components_by_index = {
+ '0': '123',
+ '1': str(component_id),
+ '2': '789'}
+ self._ml_engine.expected_features = [3, 0, 1, 0, 0]
+ self._ml_engine.scores = [5, 10, 3]
+
+ request = features_pb2.PredictComponentRequest(
+ project_name='proj',
+ text='foo baz foo foo')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ result = self.CallWrapped(self.features_svcr.PredictComponent, mc, request)
+
+ self.assertEqual(
+ common_pb2.ComponentRef(
+ path='Ruta>Baga'),
+ result.component_ref)
+
+ def testPredictComponent_NoPrediction(self):
+ """Test case when no component id was predicted."""
+ self._top_words = {
+ 'foo': 0,
+ 'bar': 1,
+ 'baz': 2}
+ self._components_by_index = {
+ '0': '123',
+ '1': '456',
+ '2': '789'}
+ self._ml_engine.expected_features = [3, 0, 1, 0, 0]
+ self._ml_engine.scores = [5, 10, 3]
+
+ request = features_pb2.PredictComponentRequest(
+ project_name='proj',
+ text='foo baz foo foo')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ result = self.CallWrapped(self.features_svcr.PredictComponent, mc, request)
+
+ self.assertEqual(common_pb2.ComponentRef(), result.component_ref)
diff --git a/api/test/issues_servicer_test.py b/api/test/issues_servicer_test.py
new file mode 100644
index 0000000..2c46f7c
--- /dev/null
+++ b/api/test/issues_servicer_test.py
@@ -0,0 +1,2693 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for the issues servicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+import sys
+import time
+import unittest
+from mock import ANY, Mock, patch
+
+from google.protobuf import empty_pb2
+
+from components.prpc import codes
+from components.prpc import context
+from components.prpc import server
+
+from api import issues_servicer
+from api import converters
+from api.api_proto import common_pb2
+from api.api_proto import issues_pb2
+from api.api_proto import issue_objects_pb2
+from api.api_proto import common_pb2
+from businesslogic import work_env
+from features import filterrules_helpers
+from features import send_notifications
+from framework import authdata
+from framework import exceptions
+from framework import framework_views
+from framework import monorailcontext
+from framework import permissions
+from search import frontendsearchpipeline
+from proto import tracker_pb2
+from proto import project_pb2
+from testing import fake
+from tracker import tracker_bizobj
+from services import service_manager
+from proto import tracker_pb2
+
+
+class IssuesServicerTest(unittest.TestCase):
+
+ NOW = 1234567890
+
+ def setUp(self):
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ features=fake.FeaturesService(),
+ issue=fake.IssueService(),
+ issue_star=fake.IssueStarService(),
+ project=fake.ProjectService(),
+ spam=fake.SpamService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService())
+ self.project = self.services.project.TestAddProject(
+ 'proj', project_id=789, owner_ids=[111], contrib_ids=[222, 333])
+ self.user_1 = self.services.user.TestAddUser('owner@example.com', 111)
+ self.user_2 = self.services.user.TestAddUser('approver2@example.com', 222)
+ self.user_3 = self.services.user.TestAddUser('approver3@example.com', 333)
+ self.user_4 = self.services.user.TestAddUser('nonmember@example.com', 444)
+ self.issue_1 = fake.MakeTestIssue(
+ 789, 1, 'sum', 'New', 111, project_name='proj',
+ opened_timestamp=self.NOW, issue_id=1001)
+ self.issue_2 = fake.MakeTestIssue(
+ 789, 2, 'sum', 'New', 111, project_name='proj', issue_id=1002)
+ self.issue_1.blocked_on_iids.append(self.issue_2.issue_id)
+ self.issue_1.blocked_on_ranks.append(sys.maxint)
+ self.services.issue.TestAddIssue(self.issue_1)
+ self.services.issue.TestAddIssue(self.issue_2)
+ self.issues_svcr = issues_servicer.IssuesServicer(
+ self.services, make_rate_limiter=False)
+ self.prpc_context = context.ServicerContext()
+ self.prpc_context.set_code(server.StatusCode.OK)
+ self.auth = authdata.AuthData(user_id=333, email='approver3@example.com')
+
+ self.fd_1 = tracker_pb2.FieldDef(
+ field_name='FirstField', field_id=1,
+ field_type=tracker_pb2.FieldTypes.STR_TYPE,
+ applicable_type='')
+ self.fd_2 = tracker_pb2.FieldDef(
+ field_name='SecField', field_id=2,
+ field_type=tracker_pb2.FieldTypes.INT_TYPE,
+ applicable_type='')
+ self.fd_3 = tracker_pb2.FieldDef(
+ field_name='LegalApproval', field_id=3,
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ applicable_type='')
+ self.fd_4 = tracker_pb2.FieldDef(
+ field_name='UserField', field_id=4,
+ field_type=tracker_pb2.FieldTypes.USER_TYPE,
+ applicable_type='')
+ self.fd_5 = tracker_pb2.FieldDef(
+ field_name='DogApproval', field_id=5,
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ applicable_type='')
+
+ def CallWrapped(self, wrapped_handler, *args, **kwargs):
+ return wrapped_handler.wrapped(self.issues_svcr, *args, **kwargs)
+
+ def testGetProjectIssueIDsAndConfig_OnlyOneProjectName(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ issue_refs = [
+ common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(local_id=2),
+ common_pb2.IssueRef(project_name='proj', local_id=3),
+ ]
+ project, issue_ids, config = self.issues_svcr._GetProjectIssueIDsAndConfig(
+ mc, issue_refs)
+ self.assertEqual(project, self.project)
+ self.assertEqual(issue_ids, [self.issue_1.issue_id, self.issue_2.issue_id])
+ self.assertEqual(
+ config,
+ self.services.config.GetProjectConfig(
+ self.cnxn, self.project.project_id))
+
+ def testGetProjectIssueIDsAndConfig_NoProjectName(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ issue_refs = [
+ common_pb2.IssueRef(local_id=2),
+ common_pb2.IssueRef(local_id=3),
+ ]
+ with self.assertRaises(exceptions.InputException):
+ self.issues_svcr._GetProjectIssueIDsAndConfig(mc, issue_refs)
+
+ def testGetProjectIssueIDsAndConfig_MultipleProjectNames(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ issue_refs = [
+ common_pb2.IssueRef(project_name='proj', local_id=2),
+ common_pb2.IssueRef(project_name='proj2', local_id=3),
+ ]
+ with self.assertRaises(exceptions.InputException):
+ self.issues_svcr._GetProjectIssueIDsAndConfig(mc, issue_refs)
+
+ def testGetProjectIssueIDsAndConfig_MissingLocalId(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ issue_refs = [
+ common_pb2.IssueRef(project_name='proj'),
+ common_pb2.IssueRef(project_name='proj', local_id=3),
+ ]
+ with self.assertRaises(exceptions.InputException):
+ self.issues_svcr._GetProjectIssueIDsAndConfig(mc, issue_refs)
+
+ def testCreateIssue_Normal(self):
+ """We can create an issue."""
+ request = issues_pb2.CreateIssueRequest(
+ project_name='proj',
+ issue=issue_objects_pb2.Issue(
+ project_name='proj', local_id=1, summary='sum'))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ response = self.CallWrapped(self.issues_svcr.CreateIssue, mc, request)
+
+ self.assertEqual('proj', response.project_name)
+
+ def testGetIssue_Normal(self):
+ """We can get an issue."""
+ request = issues_pb2.GetIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ response = self.CallWrapped(self.issues_svcr.GetIssue, mc, request)
+
+ actual = response.issue
+ self.assertEqual('proj', actual.project_name)
+ self.assertEqual(1, actual.local_id)
+ self.assertEqual(1, len(actual.blocked_on_issue_refs))
+ self.assertEqual('proj', actual.blocked_on_issue_refs[0].project_name)
+ self.assertEqual(2, actual.blocked_on_issue_refs[0].local_id)
+
+ def testGetIssue_Moved(self):
+ """We can get a moved issue."""
+ self.services.project.TestAddProject(
+ 'other', project_id=987, owner_ids=[111], contrib_ids=[111])
+ issue = fake.MakeTestIssue(987, 200, 'sum', 'New', 111, issue_id=1010)
+ self.services.issue.TestAddIssue(issue)
+ self.services.issue.TestAddMovedIssueRef(789, 404, 987, 200)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ request = issues_pb2.GetIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 404
+
+ response = self.CallWrapped(self.issues_svcr.GetIssue, mc, request)
+
+ ref = response.moved_to_ref
+ self.assertEqual(200, ref.local_id)
+ self.assertEqual('other', ref.project_name)
+
+ @patch('search.frontendsearchpipeline.FrontendSearchPipeline')
+ def testListIssues(self, mock_pipeline):
+ """We can get a list of issues from a search."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com',
+ auth=self.auth)
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, [111])
+ config = self.services.config.GetProjectConfig(self.cnxn, 789)
+
+ instance = Mock(
+ spec=True, visible_results=[self.issue_1, self.issue_2],
+ users_by_id=users_by_id, harmonized_config=config,
+ pagination=Mock(total_count=2))
+ mock_pipeline.return_value = instance
+ instance.SearchForIIDs = Mock()
+ instance.MergeAndSortIssues = Mock()
+ instance.Paginate = Mock()
+
+ request = issues_pb2.ListIssuesRequest(query='',project_names=['proj'])
+ response = self.CallWrapped(self.issues_svcr.ListIssues, mc, request)
+
+ actual_issue_1 = response.issues[0]
+ self.assertEqual(actual_issue_1.owner_ref.user_id, 111)
+ self.assertEqual('owner@example.com', actual_issue_1.owner_ref.display_name)
+ self.assertEqual(actual_issue_1.local_id, 1)
+
+ actual_issue_2 = response.issues[1]
+ self.assertEqual(actual_issue_2.owner_ref.user_id, 111)
+ self.assertEqual('owner@example.com', actual_issue_2.owner_ref.display_name)
+ self.assertEqual(actual_issue_2.local_id, 2)
+ self.assertEqual(2, response.total_results)
+
+ # TODO(zhangtiff): Add tests for ListIssues + canned queries.
+
+ @patch('search.frontendsearchpipeline.FrontendSearchPipeline')
+ def testListIssues_IncludesAttachmentCount(self, mock_pipeline):
+ """Ensure ListIssues includes correct attachment counts."""
+
+ # Add an attachment to one of the issues so we can check attachment counts.
+ issue_3 = fake.MakeTestIssue(
+ 789, 3, 'sum', 'New', 111, project_name='proj', issue_id=2003,
+ attachment_count=1)
+ issue_4 = fake.MakeTestIssue(
+ 789, 4, 'sum', 'New', 111, project_name='proj', issue_id=2004,
+ attachment_count=-10)
+ self.services.issue.TestAddIssue(issue_3)
+ self.services.issue.TestAddIssue(issue_4)
+
+ # Request the list of issues.
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com',
+ auth=self.auth)
+ mc.LookupLoggedInUserPerms(self.project)
+
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, [111])
+ config = self.services.config.GetProjectConfig(self.cnxn, 789)
+ instance = Mock(
+ spec=True, visible_results=[
+ self.issue_1, self.issue_2, issue_3, issue_4],
+ users_by_id=users_by_id, harmonized_config=config,
+ pagination=Mock(total_count=4))
+ mock_pipeline.return_value = instance
+ instance.SearchForIIDs = Mock()
+ instance.MergeAndSortIssues = Mock()
+ instance.Paginate = Mock()
+
+ request = issues_pb2.ListIssuesRequest(query='', project_names=['proj'])
+ response = self.CallWrapped(self.issues_svcr.ListIssues, mc, request)
+
+ # Ensure attachment counts match what we expect.
+ actual_issue_1 = response.issues[0]
+ self.assertEqual(actual_issue_1.attachment_count, 0)
+ self.assertEqual(actual_issue_1.local_id, 1)
+
+ actual_issue_2 = response.issues[1]
+ self.assertEqual(actual_issue_2.attachment_count, 0)
+ self.assertEqual(actual_issue_2.local_id, 2)
+
+ actual_issue_3 = response.issues[2]
+ self.assertEqual(actual_issue_3.attachment_count, 1)
+ self.assertEqual(actual_issue_3.local_id, 3)
+
+ actual_issue_4 = response.issues[3]
+ # NOTE(pawalls): It is not possible to test for presence in Proto3. Instead
+ # we test for default value here though it is semantically different
+ # and not quite the behavior we care about.
+ self.assertEqual(actual_issue_4.attachment_count, 0)
+ self.assertEqual(actual_issue_4.local_id, 4)
+
+ @patch('search.frontendsearchpipeline.FrontendSearchPipeline')
+ def testListIssues_No_visible_results(self, mock_pipeline):
+ """Ensure ListIssues handles the no visible results case."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=None, auth=None)
+ users_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, [111])
+ config = self.services.config.GetProjectConfig(self.cnxn, 789)
+
+ instance = Mock(
+ spec=True,
+ users_by_id=users_by_id,
+ harmonized_config=config,
+ # When there are no results, these default to None.
+ visible_results=None,
+ pagination=None)
+ mock_pipeline.return_value = instance
+ instance.SearchForIIDs = Mock()
+ instance.MergeAndSortIssues = Mock()
+ instance.Paginate = Mock()
+
+ request = issues_pb2.ListIssuesRequest(query='', project_names=['proj'])
+ response = self.CallWrapped(self.issues_svcr.ListIssues, mc, request)
+
+ self.assertEqual(len(response.issues), 0)
+
+ def testListReferencedIssues(self):
+ """We can get the referenced issues that exist."""
+ self.services.project.TestAddProject(
+ 'other-proj', project_id=788, owner_ids=[111])
+ other_issue = fake.MakeTestIssue(
+ 788, 1, 'sum', 'Fixed', 111, project_name='other-proj', issue_id=78801)
+ self.services.issue.TestAddIssue(other_issue)
+ # We ignore project_names or local_ids that don't exist in our DB.
+ request = issues_pb2.ListReferencedIssuesRequest(
+ issue_refs=[
+ common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='other-proj', local_id=1),
+ common_pb2.IssueRef(project_name='other-proj', local_id=2),
+ common_pb2.IssueRef(project_name='ghost-proj', local_id=1)
+ ]
+ )
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ response = self.CallWrapped(
+ self.issues_svcr.ListReferencedIssues, mc, request)
+ self.assertEqual(len(response.closed_refs), 1)
+ self.assertEqual(len(response.open_refs), 1)
+ self.assertEqual(
+ issue_objects_pb2.Issue(
+ local_id=1,
+ project_name='other-proj',
+ summary='sum',
+ status_ref=common_pb2.StatusRef(
+ status='Fixed'),
+ owner_ref=common_pb2.UserRef(
+ user_id=111,
+ display_name='owner@example.com'),
+ reporter_ref=common_pb2.UserRef(
+ user_id=111,
+ display_name='owner@example.com')),
+ response.closed_refs[0])
+ self.assertEqual(
+ issue_objects_pb2.Issue(
+ local_id=1,
+ project_name='proj',
+ summary='sum',
+ status_ref=common_pb2.StatusRef(
+ status='New',
+ means_open=True),
+ owner_ref=common_pb2.UserRef(
+ user_id=111,
+ display_name='owner@example.com'),
+ blocked_on_issue_refs=[common_pb2.IssueRef(
+ project_name='proj',
+ local_id=2)],
+ reporter_ref=common_pb2.UserRef(
+ user_id=111,
+ display_name='owner@example.com'),
+ opened_timestamp=self.NOW,
+ component_modified_timestamp=self.NOW,
+ status_modified_timestamp=self.NOW,
+ owner_modified_timestamp=self.NOW),
+ response.open_refs[0])
+
+ def testListReferencedIssues_MissingInput(self):
+ request = issues_pb2.ListReferencedIssuesRequest(
+ issue_refs=[common_pb2.IssueRef(local_id=1)])
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.ListReferencedIssues, mc, request)
+
+ def testListApplicableFieldDefs_EmptyIssueRefs(self):
+ request = issues_pb2.ListApplicableFieldDefsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.issues_svcr.ListApplicableFieldDefs, mc, request)
+ self.assertEqual(response, issues_pb2.ListApplicableFieldDefsResponse())
+
+ def testListApplicableFieldDefs_CrossProjectRequest(self):
+ issue_refs = [common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='proj2', local_id=2)]
+ request = issues_pb2.ListApplicableFieldDefsRequest(issue_refs=issue_refs)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.ListApplicableFieldDefs, mc, request)
+
+ def testListApplicableFieldDefs_MissingProjectName(self):
+ issue_refs = [common_pb2.IssueRef(local_id=1),
+ common_pb2.IssueRef(local_id=2)]
+ request = issues_pb2.ListApplicableFieldDefsRequest(issue_refs=issue_refs)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.ListApplicableFieldDefs, mc, request)
+
+ def testListApplicableFieldDefs_Normal(self):
+ self.issue_1.labels = ['Type-Feedback']
+ self.issue_2.approval_values = [
+ tracker_pb2.ApprovalValue(approval_id=self.fd_3.field_id)]
+ self.fd_1.applicable_type = 'Defect' # not applicable
+ self.fd_2.applicable_type = 'feedback' # applicable
+ self.fd_3.applicable_type = 'ignored' # is APPROVAL_TYPE, applicable
+ self.fd_4.applicable_type = '' # applicable
+ self.fd_5.applicable_type = '' # is APPROVAl_TYPE, not applicable
+ config = tracker_pb2.ProjectIssueConfig(
+ project_id=789,
+ field_defs=[self.fd_1, self.fd_2, self.fd_3, self.fd_4, self.fd_5])
+ self.services.config.StoreConfig(self.cnxn, config)
+ issue_refs = [common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='proj', local_id=2)]
+ request = issues_pb2.ListApplicableFieldDefsRequest(issue_refs=issue_refs)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.issues_svcr.ListApplicableFieldDefs, mc, request)
+ converted_field_defs = [converters.ConvertFieldDef(fd, [], {}, config, True)
+ for fd in [self.fd_2, self.fd_3, self.fd_4]]
+ self.assertEqual(response, issues_pb2.ListApplicableFieldDefsResponse(
+ field_defs=converted_field_defs))
+
+ def testUpdateIssue_Denied_Edit(self):
+ """We reject requests to update an issue when the user lacks perms."""
+ request = issues_pb2.UpdateIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ request.delta.summary.value = 'new summary'
+
+ # Anon user can never update.
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ # Signed in user cannot view this issue.
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ self.issue_1.labels = ['Restrict-View-CoreTeam']
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ # Signed in user cannot edit this issue.
+ self.issue_1.labels = ['Restrict-EditIssue-CoreTeam']
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ @patch('features.send_notifications.PrepareAndSendIssueChangeNotification')
+ def testUpdateIssue_JustAComment(self, _fake_pasicn):
+ """We check AddIssueComment when the user is only commenting."""
+ request = issues_pb2.UpdateIssueRequest()
+ request.comment_content = 'Foo'
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ # Note: no delta.
+
+ # Anon user can never update.
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ # Signed in user cannot view this issue.
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ self.issue_1.labels = ['Restrict-View-CoreTeam']
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ # Signed in user cannot edit this issue, but they can still comment.
+ self.issue_1.labels = ['Restrict-EditIssue-CoreTeam']
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ # Signed in user cannot post even a text comment.
+ self.issue_1.labels = ['Restrict-AddIssueComment-CoreTeam']
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ @patch('features.send_notifications.PrepareAndSendIssueChangeNotification')
+ def testUpdateIssue_Normal(self, fake_pasicn):
+ """We can update an issue."""
+ request = issues_pb2.UpdateIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ request.delta.summary.value = 'New summary'
+ request.delta.label_refs_add.extend([
+ common_pb2.LabelRef(label='Hot')])
+ request.comment_content = 'test comment'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ response = self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ actual = response.issue
+ # Intended stuff was changed.
+ self.assertEqual(1, len(actual.label_refs))
+ self.assertEqual('Hot', actual.label_refs[0].label)
+ self.assertEqual('New summary', actual.summary)
+
+ # Other stuff didn't change.
+ self.assertEqual('proj', actual.project_name)
+ self.assertEqual(1, actual.local_id)
+ self.assertEqual(1, len(actual.blocked_on_issue_refs))
+ self.assertEqual('proj', actual.blocked_on_issue_refs[0].project_name)
+ self.assertEqual(2, actual.blocked_on_issue_refs[0].local_id)
+
+ # A comment was added.
+ fake_pasicn.assert_called_once()
+ comments = self.services.issue.GetCommentsForIssue(
+ self.cnxn, self.issue_1.issue_id)
+ self.assertEqual(2, len(comments))
+ self.assertEqual('test comment', comments[1].content)
+
+ @patch('features.send_notifications.PrepareAndSendIssueChangeNotification')
+ def testUpdateIssue_CommentOnly(self, fake_pasicn):
+ """We can update an issue with a comment w/o making any other changes."""
+ request = issues_pb2.UpdateIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ request.comment_content = 'test comment'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ # A comment was added.
+ fake_pasicn.assert_called_once()
+ comments = self.services.issue.GetCommentsForIssue(
+ self.cnxn, self.issue_1.issue_id)
+ self.assertEqual(2, len(comments))
+ self.assertEqual('test comment', comments[1].content)
+ self.assertFalse(comments[1].is_description)
+
+ @patch('features.send_notifications.PrepareAndSendIssueChangeNotification')
+ def testUpdateIssue_CommentWithAttachments(self, fake_pasicn):
+ """We can update an issue with a comment and attachments."""
+ request = issues_pb2.UpdateIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ request.comment_content = 'test comment'
+ request.uploads.extend([
+ issue_objects_pb2.AttachmentUpload(
+ filename='a.txt',
+ content='aaaaa')])
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ # A comment with an attachment was added.
+ fake_pasicn.assert_called_once()
+ comments = self.services.issue.GetCommentsForIssue(
+ self.cnxn, self.issue_1.issue_id)
+ self.assertEqual(2, len(comments))
+ self.assertEqual('test comment', comments[1].content)
+ self.assertFalse(comments[1].is_description)
+ self.assertEqual(1, len(comments[1].attachments))
+ self.assertEqual('a.txt', comments[1].attachments[0].filename)
+ self.assertEqual(5, self.project.attachment_bytes_used)
+
+ @patch('features.send_notifications.PrepareAndSendIssueChangeNotification')
+ def testUpdateIssue_Description(self, fake_pasicn):
+ """We can update an issue's description."""
+ request = issues_pb2.UpdateIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ request.comment_content = 'new description'
+ request.is_description = True
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ # A comment was added.
+ fake_pasicn.assert_called_once()
+ comments = self.services.issue.GetCommentsForIssue(
+ self.cnxn, self.issue_1.issue_id)
+ self.assertEqual(2, len(comments))
+ self.assertEqual('new description', comments[1].content)
+ self.assertTrue(comments[1].is_description)
+
+ @patch('features.send_notifications.PrepareAndSendIssueChangeNotification')
+ def testUpdateIssue_NoOp(self, fake_pasicn):
+ """We gracefully ignore requests that have no delta or comment."""
+ request = issues_pb2.UpdateIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ response = self.CallWrapped(self.issues_svcr.UpdateIssue, mc, request)
+
+ actual = response.issue
+ # Other stuff didn't change.
+ self.assertEqual('proj', actual.project_name)
+ self.assertEqual(1, actual.local_id)
+ self.assertEqual('sum', actual.summary)
+ self.assertEqual('New', actual.status_ref.status)
+
+ # No comment was added.
+ fake_pasicn.assert_not_called()
+ comments = self.services.issue.GetCommentsForIssue(
+ self.cnxn, self.issue_1.issue_id)
+ self.assertEqual(1, len(comments))
+
+ def testStarIssue_Denied(self):
+ """We reject requests to star an issue if the user lacks perms."""
+ request = issues_pb2.StarIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ request.starred = True
+
+ # Anon user cannot star an issue.
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.StarIssue, mc, request)
+
+ # User star an issue that they cannot view.
+ self.issue_1.labels = ['Restrict-View-CoreTeam']
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.StarIssue, mc, request)
+
+ # The issue was not actually starred.
+ self.assertEqual(0, self.issue_1.star_count)
+
+ def testStarIssue_Normal(self):
+ """Users can star and unstar issues."""
+ request = issues_pb2.StarIssueRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ request.starred = True
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ # First, star it.
+ response = self.CallWrapped(self.issues_svcr.StarIssue, mc, request)
+ self.assertEqual(1, response.star_count)
+
+ # Then, unstar it.
+ request.starred = False
+ response = self.CallWrapped(self.issues_svcr.StarIssue, mc, request)
+ self.assertEqual(0, response.star_count)
+
+ def testIsIssueStared_Anon(self):
+ """Anon users can't star issues, so they always get back False."""
+ request = issues_pb2.IsIssueStarredRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ mc.LookupLoggedInUserPerms(self.project)
+
+ response = self.CallWrapped(self.issues_svcr.IsIssueStarred, mc, request)
+ self.assertFalse(response.is_starred)
+
+ def testIsIssueStared_Denied(self):
+ """Users can't ask about an issue that they cannot currently view."""
+ request = issues_pb2.IsIssueStarredRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ self.issue_1.labels = ['Restrict-View-CoreTeam']
+
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.IsIssueStarred, mc, request)
+
+ def testIsIssueStared_Normal(self):
+ """Users can star and unstar issues."""
+ request = issues_pb2.IsIssueStarredRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ # It is not initially starred by this user.
+ response = self.CallWrapped(self.issues_svcr.IsIssueStarred, mc, request)
+ self.assertFalse(response.is_starred)
+
+ # If we star it, we get response True.
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, 'fake config', self.issue_1.issue_id,
+ 333, True)
+ response = self.CallWrapped(self.issues_svcr.IsIssueStarred, mc, request)
+ self.assertTrue(response.is_starred)
+
+ def testListStarredIssues_Anon(self):
+ """Users can't see their starred issues until they sign in."""
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ mc.LookupLoggedInUserPerms(self.project)
+
+ response = self.CallWrapped(self.issues_svcr.ListStarredIssues, mc, {})
+ # Assert that response has an empty list
+ self.assertEqual(0, len(response.starred_issue_refs))
+
+ def testListStarredIssues_Normal(self):
+ """User can access which issues they've starred."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ # First, star some issues
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, 'fake config', self.issue_1.issue_id,
+ 333, True)
+ self.services.issue_star.SetStar(
+ self.cnxn, self.services, 'fake config', self.issue_2.issue_id,
+ 333, True)
+
+ # Now test that user can retrieve their star in a list
+ response = self.CallWrapped(self.issues_svcr.ListStarredIssues, mc, {})
+ self.assertEqual(2, len(response.starred_issue_refs))
+
+ def testListComments_Normal(self):
+ """We can get comments on an issue."""
+ comment = tracker_pb2.IssueComment(
+ user_id=111, timestamp=self.NOW, content='second',
+ project_id=789, issue_id=self.issue_1.issue_id, sequence=1)
+ self.services.issue.TestAddComment(comment, self.issue_1.local_id)
+ request = issues_pb2.ListCommentsRequest()
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+
+ response = self.CallWrapped(self.issues_svcr.ListComments, mc, request)
+
+ actual_0 = response.comments[0]
+ actual_1 = response.comments[1]
+ expected_0 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=0, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='owner@example.com'),
+ timestamp=self.NOW, content='sum', is_spam=False,
+ description_num=1, can_delete=True, can_flag=True)
+ expected_1 = issue_objects_pb2.Comment(
+ project_name='proj', local_id=1, sequence_num=1, is_deleted=False,
+ commenter=common_pb2.UserRef(
+ user_id=111, display_name='owner@example.com'),
+ timestamp=self.NOW, content='second', can_delete=True, can_flag=True)
+ self.assertEqual(expected_0, actual_0)
+ self.assertEqual(expected_1, actual_1)
+
+ def testListActivities_Normal(self):
+ """We can get issue activity."""
+ self.services.user.TestAddUser('user@example.com', 444)
+
+ config = tracker_pb2.ProjectIssueConfig(
+ project_id=789,
+ field_defs=[self.fd_1])
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ comment = tracker_pb2.IssueComment(
+ user_id=444, timestamp=self.NOW, content='c1',
+ project_id=789, issue_id=self.issue_1.issue_id, sequence=1)
+ self.services.issue.TestAddComment(comment, self.issue_1.local_id)
+
+ self.services.project.TestAddProject(
+ 'proj2', project_id=790, owner_ids=[111], contrib_ids=[222, 333])
+ issue_2 = fake.MakeTestIssue(
+ 790, 1, 'sum', 'New', 444, project_name='proj2',
+ opened_timestamp=self.NOW, issue_id=2001)
+ comment_2 = tracker_pb2.IssueComment(
+ user_id=444, timestamp=self.NOW, content='c2',
+ project_id=790, issue_id=issue_2.issue_id, sequence=1)
+ self.services.issue.TestAddComment(comment_2, issue_2.local_id)
+ self.services.issue.TestAddIssue(issue_2)
+
+ issue_3 = fake.MakeTestIssue(
+ 790, 2, 'sum', 'New', 111, project_name='proj2',
+ opened_timestamp=self.NOW, issue_id=2002, labels=['Restrict-View-Foo'])
+ comment_3 = tracker_pb2.IssueComment(
+ user_id=444, timestamp=self.NOW, content='c3',
+ project_id=790, issue_id=issue_3.issue_id, sequence=1)
+ self.services.issue.TestAddComment(comment_3, issue_3.local_id)
+ self.services.issue.TestAddIssue(issue_3)
+
+ request = issues_pb2.ListActivitiesRequest()
+ request.user_ref.user_id = 444
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.ListActivities, mc, request)
+
+ self.maxDiff = None
+ self.assertEqual([
+ issue_objects_pb2.Comment(
+ project_name='proj',
+ local_id=1,
+ commenter=common_pb2.UserRef(
+ user_id=444, display_name='user@example.com'),
+ timestamp=self.NOW,
+ content='c1',
+ sequence_num=1,
+ can_delete=True,
+ can_flag=True),
+ issue_objects_pb2.Comment(
+ project_name='proj2',
+ local_id=1,
+ commenter=common_pb2.UserRef(
+ user_id=444, display_name='user@example.com'),
+ timestamp=self.NOW,
+ content='sum',
+ description_num=1,
+ can_delete=True,
+ can_flag=True),
+ issue_objects_pb2.Comment(
+ project_name='proj2',
+ local_id=1,
+ commenter=common_pb2.UserRef(
+ user_id=444, display_name='user@example.com'),
+ timestamp=self.NOW,
+ content='c2',
+ sequence_num=1,
+ can_delete=True,
+ can_flag=True)],
+ sorted(
+ response.comments,
+ key=lambda c: (c.project_name, c.local_id, c.sequence_num)))
+ self.assertEqual([
+ issue_objects_pb2.IssueSummary(
+ project_name='proj',
+ local_id=1,
+ summary='sum'),
+ issue_objects_pb2.IssueSummary(
+ project_name='proj2',
+ local_id=1,
+ summary='sum')],
+ sorted(
+ response.issue_summaries,
+ key=lambda issue: (issue.project_name, issue.local_id)))
+
+ def testListActivities_Amendment(self):
+ self.services.user.TestAddUser('user@example.com', 444)
+
+ comment = tracker_pb2.IssueComment(
+ user_id=444,
+ timestamp=self.NOW,
+ amendments=[tracker_bizobj.MakeOwnerAmendment(111, 222)],
+ project_id=789,
+ issue_id=self.issue_1.issue_id,
+ content='',
+ sequence=1)
+ self.services.issue.TestAddComment(comment, self.issue_1.local_id)
+
+ request = issues_pb2.ListActivitiesRequest()
+ request.user_ref.user_id = 444
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.ListActivities, mc, request)
+
+ self.assertEqual([
+ issue_objects_pb2.Comment(
+ project_name='proj',
+ local_id=1,
+ commenter=common_pb2.UserRef(
+ user_id=444, display_name='user@example.com'),
+ timestamp=self.NOW,
+ content='',
+ sequence_num=1,
+ amendments=[issue_objects_pb2.Amendment(
+ field_name="Owner",
+ new_or_delta_value="ow...@example.com")],
+ can_delete=True,
+ can_flag=True)],
+ sorted(
+ response.comments,
+ key=lambda c: (c.project_name, c.local_id, c.sequence_num)))
+ self.assertEqual([
+ issue_objects_pb2.IssueSummary(
+ project_name='proj',
+ local_id=1,
+ summary='sum')],
+ sorted(
+ response.issue_summaries,
+ key=lambda issue: (issue.project_name, issue.local_id)))
+
+ @patch('testing.fake.IssueService.SoftDeleteComment')
+ def testDeleteComment_Invalid(self, fake_softdeletecomment):
+ """We reject requests to delete a non-existent comment."""
+ # Note: no comments added to self.issue_1 after the description.
+ request = issues_pb2.DeleteCommentRequest(
+ issue_ref=common_pb2.IssueRef(project_name='proj', local_id=1),
+ sequence_num=2, delete=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ with self.assertRaises(exceptions.NoSuchCommentException):
+ self.CallWrapped(self.issues_svcr.DeleteComment, mc, request)
+
+ fake_softdeletecomment.assert_not_called()
+
+ def testDeleteComment_Normal(self):
+ """An authorized user can delete and undelete a comment."""
+ comment_1 = tracker_pb2.IssueComment(
+ project_id=789, issue_id=self.issue_1.issue_id, content='one')
+ self.services.issue.TestAddComment(comment_1, 1)
+ comment_2 = tracker_pb2.IssueComment(
+ project_id=789, issue_id=self.issue_1.issue_id, content='two',
+ user_id=222)
+ self.services.issue.TestAddComment(comment_2, 1)
+
+ # Delete a comment.
+ request = issues_pb2.DeleteCommentRequest(
+ issue_ref=common_pb2.IssueRef(project_name='proj', local_id=1),
+ sequence_num=2, delete=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ response = self.CallWrapped(self.issues_svcr.DeleteComment, mc, request)
+
+ self.assertTrue(isinstance(response, empty_pb2.Empty))
+ self.assertEqual(111, comment_2.deleted_by)
+
+ # Undelete a comment.
+ request.delete=False
+
+ response = self.CallWrapped(self.issues_svcr.DeleteComment, mc, request)
+
+ self.assertTrue(isinstance(response, empty_pb2.Empty))
+ self.assertEqual(None, comment_2.deleted_by)
+
+ @patch('testing.fake.IssueService.SoftDeleteComment')
+ def testDeleteComment_Denied(self, fake_softdeletecomment):
+ """An unauthorized user cannot delete a comment."""
+ comment_1 = tracker_pb2.IssueComment(
+ project_id=789, issue_id=self.issue_1.issue_id, content='one',
+ user_id=222)
+ self.services.issue.TestAddComment(comment_1, 1)
+
+ request = issues_pb2.DeleteCommentRequest(
+ issue_ref=common_pb2.IssueRef(project_name='proj', local_id=1),
+ sequence_num=1, delete=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com')
+
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.DeleteComment, mc, request)
+
+ fake_softdeletecomment.assert_not_called()
+ self.assertIsNone(comment_1.deleted_by)
+
+ def testUpdateApproval_MissingFieldDef(self):
+ """Missing Approval Field Def throwns exception."""
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ field_ref = common_pb2.FieldRef(field_name='LegalApproval')
+ approval_delta = issue_objects_pb2.ApprovalDelta(
+ status=issue_objects_pb2.REVIEW_REQUESTED)
+ request = issues_pb2.UpdateApprovalRequest(
+ issue_ref=issue_ref, field_ref=field_ref, approval_delta=approval_delta)
+
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com',
+ auth=self.auth)
+
+ with self.assertRaises(exceptions.NoSuchFieldDefException):
+ self.CallWrapped(self.issues_svcr.UpdateApproval, mc, request)
+
+ def testBulkUpdateApprovals_EmptyIssueRefs(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ request = issues_pb2.BulkUpdateApprovalsRequest(
+ field_ref=common_pb2.FieldRef(field_name='LegalApproval'),
+ approval_delta=issue_objects_pb2.ApprovalDelta())
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.BulkUpdateApprovals, mc, request)
+
+ def testBulkUpdateApprovals_NoProjectName(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ issue_refs = [common_pb2.IssueRef(local_id=1),
+ common_pb2.IssueRef(local_id=2)]
+ request = issues_pb2.BulkUpdateApprovalsRequest(
+ issue_refs=issue_refs,
+ field_ref=common_pb2.FieldRef(field_name='LegalApproval'),
+ approval_delta=issue_objects_pb2.ApprovalDelta())
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.BulkUpdateApprovals, mc, request)
+
+ def testBulkUpdateApprovals_CrossProjectRequest(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ issue_refs = [common_pb2.IssueRef(project_name='p1', local_id=1),
+ common_pb2.IssueRef(project_name='p2', local_id=2)]
+ request = issues_pb2.BulkUpdateApprovalsRequest(
+ issue_refs=issue_refs,
+ field_ref=common_pb2.FieldRef(field_name='LegalApproval'),
+ approval_delta=issue_objects_pb2.ApprovalDelta())
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.BulkUpdateApprovals, mc, request)
+
+ def testBulkUpdateApprovals_NoSuchFieldDef(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ issue_refs = [common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='proj', local_id=2)]
+ request = issues_pb2.BulkUpdateApprovalsRequest(
+ issue_refs=issue_refs,
+ field_ref=common_pb2.FieldRef(field_name='LegalApproval'),
+ approval_delta=issue_objects_pb2.ApprovalDelta())
+ with self.assertRaises(exceptions.NoSuchFieldDefException):
+ self.CallWrapped(self.issues_svcr.BulkUpdateApprovals, mc, request)
+
+ def testBulkUpdateApprovals_AnonDenied(self):
+ """Anon user cannot make any updates"""
+ config = tracker_pb2.ProjectIssueConfig(
+ project_id=789,
+ field_defs=[self.fd_3])
+ self.services.config.StoreConfig(self.cnxn, config)
+ field_ref = common_pb2.FieldRef(field_name='LegalApproval')
+ approval_delta = issue_objects_pb2.ApprovalDelta()
+ issue_refs = [common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='proj', local_id=2)]
+ request = issues_pb2.BulkUpdateApprovalsRequest(
+ issue_refs=issue_refs, field_ref=field_ref,
+ approval_delta=approval_delta)
+
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.BulkUpdateApprovals, mc, request)
+
+ def testBulkUpdateApprovals_UserLacksViewPerms(self):
+ """User who cannot view issue cannot update issue."""
+ config = tracker_pb2.ProjectIssueConfig(
+ project_id=789,
+ field_defs=[self.fd_3])
+ self.services.config.StoreConfig(self.cnxn, config)
+ field_ref = common_pb2.FieldRef(field_name='LegalApproval')
+ approval_delta = issue_objects_pb2.ApprovalDelta()
+ issue_refs = [common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='proj', local_id=2)]
+ request = issues_pb2.BulkUpdateApprovalsRequest(
+ issue_refs=issue_refs, field_ref=field_ref,
+ approval_delta=approval_delta)
+
+ self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='nonmember@example.com')
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.BulkUpdateApprovals, mc, request)
+
+ @patch('time.time')
+ @patch('businesslogic.work_env.WorkEnv.BulkUpdateIssueApprovals')
+ @patch('businesslogic.work_env.WorkEnv.GetIssueRefs')
+ def testBulkUpdateApprovals_Normal(
+ self, mockGetIssueRefs, mockBulkUpdateIssueApprovals, mockTime):
+ """Issue approvals that can be updated are updated and returned."""
+ mockTime.return_value = 12345
+ mockGetIssueRefs.return_value = {1001: ('proj', 1), 1002: ('proj', 2)}
+ config = tracker_pb2.ProjectIssueConfig(
+ project_id=789,
+ field_defs=[self.fd_3])
+ self.services.config.StoreConfig(self.cnxn, config)
+ field_ref = common_pb2.FieldRef(field_name='LegalApproval')
+ issue_refs = [common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='proj', local_id=2)]
+ request = issues_pb2.BulkUpdateApprovalsRequest(
+ issue_refs=issue_refs, field_ref=field_ref,
+ approval_delta=issue_objects_pb2.ApprovalDelta(
+ status=issue_objects_pb2.APPROVED),
+ comment_content='new bulk comment')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='nonmember@example.com')
+ response = self.CallWrapped(
+ self.issues_svcr.BulkUpdateApprovals, mc, request)
+ self.assertEqual(
+ response,
+ issues_pb2.BulkUpdateApprovalsResponse(
+ issue_refs=[common_pb2.IssueRef(project_name='proj', local_id=1),
+ common_pb2.IssueRef(project_name='proj', local_id=2)]))
+
+ approval_delta = tracker_pb2.ApprovalDelta(
+ status=tracker_pb2.ApprovalStatus.APPROVED,
+ setter_id=444, set_on=12345)
+ mockBulkUpdateIssueApprovals.assert_called_once_with(
+ [1001, 1002], 3, self.project, approval_delta,
+ 'new bulk comment', send_email=False)
+
+ @patch('businesslogic.work_env.WorkEnv.BulkUpdateIssueApprovals')
+ @patch('businesslogic.work_env.WorkEnv.GetIssueRefs')
+ def testBulkUpdateApprovals_EmptyDelta(
+ self, mockGetIssueRefs, mockBulkUpdateIssueApprovals):
+ """Bulk update approval requests don't fail with an empty approval delta."""
+ mockGetIssueRefs.return_value = {1001: ('proj', 1)}
+ config = tracker_pb2.ProjectIssueConfig(
+ project_id=789,
+ field_defs=[self.fd_3])
+ self.services.config.StoreConfig(self.cnxn, config)
+ field_ref = common_pb2.FieldRef(field_name='LegalApproval')
+ issue_refs = [common_pb2.IssueRef(project_name='proj', local_id=1)]
+ request = issues_pb2.BulkUpdateApprovalsRequest(
+ issue_refs=issue_refs, field_ref=field_ref,
+ comment_content='new bulk comment',
+ send_email=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='nonmember@example.com')
+ self.CallWrapped(
+ self.issues_svcr.BulkUpdateApprovals, mc, request)
+
+ approval_delta = tracker_pb2.ApprovalDelta()
+ mockBulkUpdateIssueApprovals.assert_called_once_with(
+ [1001], 3, self.project, approval_delta,
+ 'new bulk comment', send_email=True)
+
+
+ @patch('businesslogic.work_env.WorkEnv.UpdateIssueApproval')
+ @patch('features.send_notifications.PrepareAndSendApprovalChangeNotification')
+ def testUpdateApproval(self, _mockPrepareAndSend, mockUpdateIssueApproval):
+ """We can update an approval."""
+
+ av_3 = tracker_pb2.ApprovalValue(
+ approval_id=3,
+ status=tracker_pb2.ApprovalStatus.NEEDS_REVIEW,
+ approver_ids=[333]
+ )
+ self.issue_1.approval_values = [av_3]
+
+ config = self.services.config.GetProjectConfig(
+ self.cnxn, 789)
+ config.field_defs = [self.fd_1, self.fd_3]
+
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ field_ref = common_pb2.FieldRef(field_name='LegalApproval')
+ approval_delta = issue_objects_pb2.ApprovalDelta(
+ status=issue_objects_pb2.REVIEW_REQUESTED,
+ approver_refs_add=[
+ common_pb2.UserRef(user_id=222, display_name='approver2@example.com')
+ ],
+ field_vals_add=[
+ issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(field_name='FirstField'),
+ value='string')
+ ]
+ )
+
+ request = issues_pb2.UpdateApprovalRequest(
+ issue_ref=issue_ref, field_ref=field_ref, approval_delta=approval_delta,
+ comment_content='Well, actually'
+ )
+ request.issue_ref.project_name = 'proj'
+ request.issue_ref.local_id = 1
+ request.uploads.extend([
+ issue_objects_pb2.AttachmentUpload(
+ filename='a.txt',
+ content='aaaaa')])
+ request.kept_attachments.extend([1, 2, 3])
+ request.send_email = True
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com',
+ auth=self.auth)
+
+ mockUpdateIssueApproval.return_value = [
+ tracker_pb2.ApprovalValue(
+ approval_id=3,
+ status=tracker_pb2.ApprovalStatus.REVIEW_REQUESTED,
+ setter_id=333,
+ approver_ids=[333, 222]),
+ 'comment_pb',
+ {}, # Fake issue.
+ ]
+
+ actual = self.CallWrapped(self.issues_svcr.UpdateApproval, mc, request)
+
+ expected = issues_pb2.UpdateApprovalResponse()
+ expected.approval.CopyFrom(
+ issue_objects_pb2.Approval(
+ field_ref=common_pb2.FieldRef(
+ field_id=3,
+ field_name='LegalApproval',
+ type=common_pb2.APPROVAL_TYPE),
+ approver_refs=[
+ common_pb2.UserRef(
+ user_id=333, display_name='approver3@example.com'),
+ common_pb2.UserRef(
+ user_id=222, display_name='approver2@example.com')
+ ],
+ status=issue_objects_pb2.REVIEW_REQUESTED,
+ setter_ref=common_pb2.UserRef(
+ user_id=333, display_name='approver3@example.com'),
+ phase_ref=issue_objects_pb2.PhaseRef()
+ )
+ )
+
+ work_env.WorkEnv(mc, self.services).UpdateIssueApproval.\
+ assert_called_once_with(
+ self.issue_1.issue_id, 3, ANY, u'Well, actually', False,
+ attachments=[(u'a.txt', 'aaaaa', 'text/plain')], send_email=True,
+ kept_attachments=[1, 2, 3])
+ self.assertEqual(expected, actual)
+
+ @patch('businesslogic.work_env.WorkEnv.UpdateIssueApproval')
+ @patch('features.send_notifications.PrepareAndSendApprovalChangeNotification')
+ def testUpdateApproval_IsDescription(
+ self, _mockPrepareAndSend, mockUpdateIssueApproval):
+ """We can update an approval survey."""
+
+ av_3 = tracker_pb2.ApprovalValue(approval_id=3)
+ self.issue_1.approval_values = [av_3]
+
+ config = self.services.config.GetProjectConfig(self.cnxn, 789)
+ config.field_defs = [self.fd_3]
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ field_ref = common_pb2.FieldRef(field_name='LegalApproval')
+ approval_delta = issue_objects_pb2.ApprovalDelta()
+
+ request = issues_pb2.UpdateApprovalRequest(
+ issue_ref=issue_ref, field_ref=field_ref, approval_delta=approval_delta,
+ comment_content='Better response.', is_description=True)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com',
+ auth=self.auth)
+
+ mockUpdateIssueApproval.return_value = [
+ tracker_pb2.ApprovalValue(approval_id=3),
+ 'comment_pb',
+ {}, # Fake issue.
+ ]
+
+ actual = self.CallWrapped(self.issues_svcr.UpdateApproval, mc, request)
+
+ expected = issues_pb2.UpdateApprovalResponse()
+ expected.approval.CopyFrom(
+ issue_objects_pb2.Approval(
+ field_ref=common_pb2.FieldRef(
+ field_id=3,
+ field_name='LegalApproval',
+ type=common_pb2.APPROVAL_TYPE),
+ phase_ref=issue_objects_pb2.PhaseRef()
+ )
+ )
+
+ work_env.WorkEnv(mc, self.services
+ ).UpdateIssueApproval.assert_called_once_with(
+ self.issue_1.issue_id, 3,
+ tracker_pb2.ApprovalDelta(),
+ u'Better response.', True, attachments=[], send_email=False,
+ kept_attachments=[])
+ self.assertEqual(expected, actual)
+
+ @patch('businesslogic.work_env.WorkEnv.UpdateIssueApproval')
+ @patch('features.send_notifications.PrepareAndSendApprovalChangeNotification')
+ def testUpdateApproval_EmptyDelta(
+ self, _mockPrepareAndSend, mockUpdateIssueApproval):
+ self.issue_1.approval_values = [tracker_pb2.ApprovalValue(approval_id=3)]
+
+ config = self.services.config.GetProjectConfig(self.cnxn, 789)
+ config.field_defs = [self.fd_3]
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ field_ref = common_pb2.FieldRef(field_name='LegalApproval')
+
+ request = issues_pb2.UpdateApprovalRequest(
+ issue_ref=issue_ref, field_ref=field_ref,
+ comment_content='Better response.', is_description=True)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com',
+ auth=self.auth)
+
+ mockUpdateIssueApproval.return_value = [
+ tracker_pb2.ApprovalValue(approval_id=3),
+ 'comment_pb',
+ {}, # Fake issue.
+ ]
+
+ actual = self.CallWrapped(self.issues_svcr.UpdateApproval, mc, request)
+
+ approval_value = issue_objects_pb2.Approval(
+ field_ref=common_pb2.FieldRef(
+ field_id=3,
+ field_name='LegalApproval',
+ type=common_pb2.APPROVAL_TYPE),
+ phase_ref=issue_objects_pb2.PhaseRef()
+ )
+ expected = issues_pb2.UpdateApprovalResponse(approval=approval_value)
+ self.assertEqual(expected, actual)
+
+ mockUpdateIssueApproval.assert_called_once_with(
+ self.issue_1.issue_id, 3,
+ tracker_pb2.ApprovalDelta(),
+ u'Better response.', True, attachments=[], send_email=False,
+ kept_attachments=[])
+
+ @patch('businesslogic.work_env.WorkEnv.ConvertIssueApprovalsTemplate')
+ def testConvertIssueApprovalsTemplate(self, mockWorkEnvConvertApprovals):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com',
+ auth=self.auth)
+ request = issues_pb2.ConvertIssueApprovalsTemplateRequest(
+ issue_ref=common_pb2.IssueRef(project_name='proj', local_id=1),
+ template_name='template_name', comment_content='CHICKEN',
+ send_email=True)
+ response = self.CallWrapped(
+ self.issues_svcr.ConvertIssueApprovalsTemplate, mc, request)
+ config = self.services.config.GetProjectConfig(self.cnxn, 789)
+ mockWorkEnvConvertApprovals.assert_called_once_with(
+ config, self.issue_1, 'template_name', request.comment_content,
+ send_email=request.send_email)
+ self.assertEqual(
+ response.issue,
+ issue_objects_pb2.Issue(
+ project_name='proj',
+ local_id=1,
+ summary='sum',
+ owner_ref=common_pb2.UserRef(
+ user_id=111, display_name='owner@example.com'),
+ status_ref=common_pb2.StatusRef(status='New', means_open=True),
+ blocked_on_issue_refs=[
+ common_pb2.IssueRef(project_name='proj', local_id=2)],
+ reporter_ref=common_pb2.UserRef(
+ user_id=111, display_name='owner@example.com'),
+ opened_timestamp=self.NOW,
+ component_modified_timestamp=self.NOW,
+ status_modified_timestamp=self.NOW,
+ owner_modified_timestamp=self.NOW,
+ ))
+
+ def testConvertIssueApprovalsTemplate_MissingRequiredFields(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver3@example.com',
+ auth=self.auth)
+ request = issues_pb2.ConvertIssueApprovalsTemplateRequest(
+ issue_ref=common_pb2.IssueRef(project_name='proj', local_id=1))
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(
+ self.issues_svcr.ConvertIssueApprovalsTemplate, mc, request)
+
+ request = issues_pb2.ConvertIssueApprovalsTemplateRequest(
+ template_name='name')
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(
+ self.issues_svcr.ConvertIssueApprovalsTemplate, mc, request)
+
+ @patch('businesslogic.work_env.WorkEnv.SnapshotCountsQuery')
+ def testSnapshotCounts_RequiredFields(self, mockSnapshotCountsQuery):
+ """Test that timestamp is required at all times.
+ And that label_prefix is required when group_by is 'label'.
+ """
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ # Test timestamp is required.
+ request = issues_pb2.IssueSnapshotRequest(project_name='proj')
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ # Test project_name or hotlist_id is required.
+ request = issues_pb2.IssueSnapshotRequest(timestamp=1531334109)
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ # Test label_prefix is required when group_by is 'label'.
+ request = issues_pb2.IssueSnapshotRequest(timestamp=1531334109,
+ project_name='proj', group_by='label')
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ mockSnapshotCountsQuery.assert_not_called()
+
+ @patch('businesslogic.work_env.WorkEnv.SnapshotCountsQuery')
+ def testSnapshotCounts_Basic(self, mockSnapshotCountsQuery):
+ """Tests the happy path case."""
+ request = issues_pb2.IssueSnapshotRequest(
+ timestamp=1531334109, project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mockSnapshotCountsQuery.return_value = ({'total': 123}, [], True)
+
+ response = self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ self.assertEqual(123, response.snapshot_count[0].count)
+ self.assertEqual(0, len(response.unsupported_field))
+ self.assertTrue(response.search_limit_reached)
+ mockSnapshotCountsQuery.assert_called_once_with(self.project, 1531334109,
+ '', query=None, canned_query=None, label_prefix='', hotlist=None)
+
+ @patch('businesslogic.work_env.WorkEnv.SnapshotCountsQuery')
+ @patch('search.searchpipeline.ReplaceKeywordsWithUserIDs')
+ @patch('features.savedqueries_helpers.SavedQueryIDToCond')
+ def testSnapshotCounts_ReplacesKeywords(self, mockSavedQueryIDToCond,
+ mockReplaceKeywordsWithUserIDs,
+ mockSnapshotCountsQuery):
+ """Tests that canned query is unpacked and keywords in query and canned
+ query are replaced with user IDs."""
+ request = issues_pb2.IssueSnapshotRequest(timestamp=1531334109,
+ project_name='proj', query='owner:me', canned_query=3)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mockSavedQueryIDToCond.return_value = 'cc:me'
+ mockReplaceKeywordsWithUserIDs.side_effect = [
+ ('cc:2345', []), ('owner:1234', [])]
+ mockSnapshotCountsQuery.return_value = ({'total': 789}, [], False)
+
+ response = self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ self.assertEqual(789, response.snapshot_count[0].count)
+ self.assertEqual(0, len(response.unsupported_field))
+ self.assertFalse(response.search_limit_reached)
+ mockSnapshotCountsQuery.assert_called_once_with(self.project, 1531334109,
+ '', query='owner:1234', canned_query='cc:2345', label_prefix='',
+ hotlist=None)
+
+ @patch('businesslogic.work_env.WorkEnv.SnapshotCountsQuery')
+ def testSnapshotCounts_GroupByLabel(self, mockSnapshotCountsQuery):
+ """Tests grouping by label with label_prefix and a query.
+ But no canned_query.
+ """
+ request = issues_pb2.IssueSnapshotRequest(timestamp=1531334109,
+ project_name='proj', group_by='label', label_prefix='Type',
+ query='rutabaga:rutabaga')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mockSnapshotCountsQuery.return_value = (
+ {'label1': 123, 'label2': 987},
+ ['rutabaga'],
+ True)
+
+ response = self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ self.assertEqual(2, len(response.snapshot_count))
+ self.assertEqual('label1', response.snapshot_count[0].dimension)
+ self.assertEqual(123, response.snapshot_count[0].count)
+ self.assertEqual('label2', response.snapshot_count[1].dimension)
+ self.assertEqual(987, response.snapshot_count[1].count)
+ self.assertEqual(1, len(response.unsupported_field))
+ self.assertEqual('rutabaga', response.unsupported_field[0])
+ self.assertTrue(response.search_limit_reached)
+ mockSnapshotCountsQuery.assert_called_once_with(self.project, 1531334109,
+ 'label', label_prefix='Type', query='rutabaga:rutabaga',
+ canned_query=None, hotlist=None)
+
+ @patch('businesslogic.work_env.WorkEnv.SnapshotCountsQuery')
+ def testSnapshotCounts_GroupByComponent(self, mockSnapshotCountsQuery):
+ """Tests grouping by component with a query and a canned_query."""
+ request = issues_pb2.IssueSnapshotRequest(timestamp=1531334109,
+ project_name='proj', group_by='component',
+ query='rutabaga:rutabaga', canned_query=2)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mockSnapshotCountsQuery.return_value = (
+ {'component1': 123, 'component2': 987},
+ ['rutabaga'],
+ True)
+
+ response = self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ self.assertEqual(2, len(response.snapshot_count))
+ self.assertEqual('component1', response.snapshot_count[0].dimension)
+ self.assertEqual(123, response.snapshot_count[0].count)
+ self.assertEqual('component2', response.snapshot_count[1].dimension)
+ self.assertEqual(987, response.snapshot_count[1].count)
+ self.assertEqual(1, len(response.unsupported_field))
+ self.assertEqual('rutabaga', response.unsupported_field[0])
+ self.assertTrue(response.search_limit_reached)
+ mockSnapshotCountsQuery.assert_called_once_with(self.project, 1531334109,
+ 'component', label_prefix='', query='rutabaga:rutabaga',
+ canned_query='is:open', hotlist=None)
+
+ @patch('businesslogic.work_env.WorkEnv.SnapshotCountsQuery')
+ def testSnapshotCounts_GroupByOpen(self, mockSnapshotCountsQuery):
+ """Tests grouping by open with a query."""
+ request = issues_pb2.IssueSnapshotRequest(
+ timestamp=1531334109, project_name='proj', group_by='open')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mockSnapshotCountsQuery.return_value = (
+ {'Opened': 100, 'Closed': 23}, [], True)
+
+ response = self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ self.assertEqual(2, len(response.snapshot_count))
+ self.assertEqual('Opened', response.snapshot_count[0].dimension)
+ self.assertEqual(100, response.snapshot_count[0].count)
+ self.assertEqual('Closed', response.snapshot_count[1].dimension)
+ self.assertEqual(23, response.snapshot_count[1].count)
+ mockSnapshotCountsQuery.assert_called_once_with(self.project, 1531334109,
+ 'open', label_prefix='', query=None, canned_query=None, hotlist=None)
+
+ @patch('businesslogic.work_env.WorkEnv.SnapshotCountsQuery')
+ def testSnapshotCounts_GroupByStatus(self, mockSnapshotCountsQuery):
+ """Tests grouping by status with a query."""
+ request = issues_pb2.IssueSnapshotRequest(
+ timestamp=1531334109, project_name='proj', group_by='status')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mockSnapshotCountsQuery.return_value = (
+ {'Accepted': 100, 'Fixed': 23}, [], True)
+
+ response = self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ self.assertEqual(2, len(response.snapshot_count))
+ self.assertEqual('Fixed', response.snapshot_count[0].dimension)
+ self.assertEqual(23, response.snapshot_count[0].count)
+ self.assertEqual('Accepted', response.snapshot_count[1].dimension)
+ self.assertEqual(100, response.snapshot_count[1].count)
+ mockSnapshotCountsQuery.assert_called_once_with(self.project, 1531334109,
+ 'status', label_prefix='', query=None, canned_query=None, hotlist=None)
+
+ @patch('businesslogic.work_env.WorkEnv.SnapshotCountsQuery')
+ def testSnapshotCounts_GroupByOwner(self, mockSnapshotCountsQuery):
+ """Tests grouping by status with a query."""
+ request = issues_pb2.IssueSnapshotRequest(
+ timestamp=1531334109, project_name='proj', group_by='owner')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mockSnapshotCountsQuery.return_value = ({111: 100}, [], True)
+
+ response = self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ self.assertEqual(1, len(response.snapshot_count))
+ self.assertEqual('owner@example.com', response.snapshot_count[0].dimension)
+ self.assertEqual(100, response.snapshot_count[0].count)
+ mockSnapshotCountsQuery.assert_called_once_with(self.project, 1531334109,
+ 'owner', label_prefix='', query=None, canned_query=None, hotlist=None)
+
+ @patch('businesslogic.work_env.WorkEnv.GetHotlist')
+ @patch('businesslogic.work_env.WorkEnv.SnapshotCountsQuery')
+ def testSnapshotCounts_WithHotlist(self, mockSnapshotCountsQuery,
+ mockGetHotlist):
+ """Tests grouping by status with a hotlist."""
+ request = issues_pb2.IssueSnapshotRequest(
+ timestamp=1531334109, hotlist_id=19191)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mockSnapshotCountsQuery.return_value = ({'total': 123}, [], True)
+ fake_hotlist = fake.Hotlist('hotlist_rutabaga', 19191)
+ mockGetHotlist.return_value = fake_hotlist
+
+ response = self.CallWrapped(self.issues_svcr.IssueSnapshot, mc, request)
+
+ self.assertEqual(1, len(response.snapshot_count))
+ self.assertEqual('total', response.snapshot_count[0].dimension)
+ self.assertEqual(123, response.snapshot_count[0].count)
+ mockSnapshotCountsQuery.assert_called_once_with(None, 1531334109,
+ '', label_prefix='', query=None, canned_query=None,
+ hotlist=fake_hotlist)
+
+ def AddField(self, name, field_type_str):
+ kwargs = {
+ 'cnxn': self.cnxn,
+ 'project_id': self.project.project_id,
+ 'field_name': name,
+ 'field_type_str': field_type_str}
+ kwargs.update(
+ {
+ arg: None for arg in (
+ 'applic_type', 'applic_pred', 'is_required', 'is_niche',
+ 'is_multivalued', 'min_value', 'max_value', 'regex',
+ 'needs_member', 'needs_perm', 'grants_perm', 'notify_on',
+ 'date_action_str', 'docstring')
+ })
+ kwargs.update({arg: [] for arg in ('admin_ids', 'editor_ids')})
+
+ return self.services.config.CreateFieldDef(**kwargs)
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_NoDerivedFields(self, mockGetFilterRules):
+ """When no rules match, we respond with just owner availability."""
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(user_id=111),
+ label_refs_add=[common_pb2.LabelRef(label='foo')])
+
+ mockGetFilterRules.return_value = [
+ filterrules_helpers.MakeRule('label:bar', add_labels=['baz'])]
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ issues_pb2.PresubmitIssueResponse(
+ owner_availability="User never visited",
+ owner_availability_state="never"),
+ response)
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_IncompleteOwnerEmail(self, mockGetFilterRules):
+ """User is in the process of typing in the proposed owner."""
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(display_name='owner@examp'))
+
+ mockGetFilterRules.return_value = []
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ actual = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ issues_pb2.PresubmitIssueResponse(),
+ actual)
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_NewIssue(self, mockGetFilterRules):
+ """Proposed owner has a vacation message set."""
+ self.user_1.vacation_message = 'In Galapagos Islands'
+ issue_ref = common_pb2.IssueRef(project_name='proj')
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(user_id=111),
+ label_refs_add=[common_pb2.LabelRef(label='foo')])
+
+ mockGetFilterRules.return_value = []
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ issues_pb2.PresubmitIssueResponse(
+ owner_availability='In Galapagos Islands',
+ owner_availability_state='none'),
+ response)
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_OwnerVacation(self, mockGetFilterRules):
+ """Proposed owner has a vacation message set."""
+ self.user_1.vacation_message = 'In Galapagos Islands'
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(user_id=111),
+ label_refs_add=[common_pb2.LabelRef(label='foo')])
+
+ mockGetFilterRules.return_value = []
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ issues_pb2.PresubmitIssueResponse(
+ owner_availability='In Galapagos Islands',
+ owner_availability_state='none'),
+ response)
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_OwnerIsAvailable(self, mockGetFilterRules):
+ """Proposed owner not on vacation and has visited recently."""
+ self.user_1.last_visit_timestamp = int(time.time())
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(user_id=111),
+ label_refs_add=[common_pb2.LabelRef(label='foo')])
+
+ mockGetFilterRules.return_value = []
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ issues_pb2.PresubmitIssueResponse(
+ owner_availability='',
+ owner_availability_state=''),
+ response)
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_DerivedLabels(self, mockGetFilterRules):
+ """Test that we can match label rules and return derived labels."""
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(user_id=111),
+ label_refs_add=[common_pb2.LabelRef(label='foo')])
+
+ mockGetFilterRules.return_value = [
+ filterrules_helpers.MakeRule('label:foo', add_labels=['bar', 'baz'])]
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ [common_pb2.ValueAndWhy(
+ value='bar',
+ why='Added by rule: IF label:foo THEN ADD LABEL'),
+ common_pb2.ValueAndWhy(
+ value='baz',
+ why='Added by rule: IF label:foo THEN ADD LABEL')],
+ [vnw for vnw in response.derived_labels])
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_DerivedOwner(self, mockGetFilterRules):
+ """Test that we can match component rules and return derived owners."""
+ self.services.config.CreateComponentDef(
+ self.cnxn, self.project.project_id, 'Foo', 'Foo Docstring', False,
+ [], [], 0, 111, [])
+ self.issue_1.owner_id = 0
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ comp_refs_add=[common_pb2.ComponentRef(path='Foo')])
+
+ mockGetFilterRules.return_value = [
+ filterrules_helpers.MakeRule('component:Foo', default_owner_id=222)]
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ [common_pb2.ValueAndWhy(
+ value='approver2@example.com',
+ why='Added by rule: IF component:Foo THEN SET DEFAULT OWNER')],
+ [vnw for vnw in response.derived_owners])
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_DerivedCCs(self, mockGetFilterRules):
+ """Test that we can match field rules and return derived cc emails."""
+ field_id = self.AddField('Foo', 'ENUM_TYPE')
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(user_id=111),
+ field_vals_add=[issue_objects_pb2.FieldValue(
+ value='Bar', field_ref=common_pb2.FieldRef(field_id=field_id))])
+
+ mockGetFilterRules.return_value = [
+ filterrules_helpers.MakeRule('Foo=Bar', add_cc_ids=[222, 333])]
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ [common_pb2.ValueAndWhy(
+ value='approver2@example.com',
+ why='Added by rule: IF Foo=Bar THEN ADD CC'),
+ common_pb2.ValueAndWhy(
+ value='approver3@example.com',
+ why='Added by rule: IF Foo=Bar THEN ADD CC')],
+ [vnw for vnw in response.derived_ccs])
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_DerivedCCsNonMember(self, mockGetFilterRules):
+ """Test that we can return obscured cc emails to non-members."""
+ field_id = self.AddField('Foo', 'ENUM_TYPE')
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(user_id=111),
+ field_vals_add=[issue_objects_pb2.FieldValue(
+ value='Bar', field_ref=common_pb2.FieldRef(field_id=field_id))])
+
+ mockGetFilterRules.return_value = [
+ filterrules_helpers.MakeRule('Foo=Bar', add_cc_ids=[222, 333])]
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='nonmember@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ [
+ common_pb2.ValueAndWhy(
+ value='appro...@example.com',
+ why='Added by rule: IF Foo=Bar THEN ADD CC'),
+ common_pb2.ValueAndWhy(
+ value='appro...@example.com',
+ why='Added by rule: IF Foo=Bar THEN ADD CC')
+ ], [vnw for vnw in response.derived_ccs])
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_Warnings(self, mockGetFilterRules):
+ """Test that we can match owner rules and return warnings."""
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(user_id=111))
+
+ mockGetFilterRules.return_value = [
+ filterrules_helpers.MakeRule(
+ 'owner:owner@example.com', warning='Owner is too busy')]
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ [common_pb2.ValueAndWhy(
+ value='Owner is too busy',
+ why='Added by rule: IF owner:owner@example.com THEN ADD WARNING')],
+ [vnw for vnw in response.warnings])
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_Errors(self, mockGetFilterRules):
+ """Test that we can match owner rules and return errors."""
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta(
+ owner_ref=common_pb2.UserRef(user_id=222),
+ cc_refs_add=[
+ common_pb2.UserRef(user_id=111),
+ common_pb2.UserRef(user_id=333)])
+
+ mockGetFilterRules.return_value = [
+ filterrules_helpers.MakeRule(
+ 'cc:owner@example.com', error='Owner is not to be disturbed')]
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ [common_pb2.ValueAndWhy(
+ value='Owner is not to be disturbed',
+ why='Added by rule: IF cc:owner@example.com THEN ADD ERROR')],
+ [vnw for vnw in response.errors])
+
+ @patch('testing.fake.FeaturesService.GetFilterRules')
+ def testPresubmitIssue_Errors_ExistingOwner(self, mockGetFilterRules):
+ """Test that we apply the rules to the issue + delta, not only delta."""
+ issue_ref = common_pb2.IssueRef(project_name='proj', local_id=1)
+ issue_delta = issue_objects_pb2.IssueDelta()
+
+ mockGetFilterRules.return_value = [
+ filterrules_helpers.MakeRule(
+ 'owner:owner@example.com', error='Owner is not to be disturbed')]
+
+ request = issues_pb2.PresubmitIssueRequest(
+ issue_ref=issue_ref, issue_delta=issue_delta)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.issues_svcr.PresubmitIssue, mc, request)
+
+ self.assertEqual(
+ [common_pb2.ValueAndWhy(
+ value='Owner is not to be disturbed',
+ why='Added by rule: IF owner:owner@example.com THEN ADD ERROR')],
+ [vnw for vnw in response.errors])
+
+ def testRerankBlockedOnIssues_SplitBelow(self):
+ issues = []
+ for idx in range(3, 6):
+ issues.append(fake.MakeTestIssue(
+ 789, idx, 'sum', 'New', 111, project_name='proj', issue_id=1000+idx))
+ self.services.issue.TestAddIssue(issues[-1])
+ self.issue_1.blocked_on_iids.append(issues[-1].issue_id)
+ self.issue_1.blocked_on_ranks.append(self.issue_1.blocked_on_ranks[-1]-1)
+
+ request = issues_pb2.RerankBlockedOnIssuesRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ moved_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=2),
+ target_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=4),
+ split_above=False)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.issues_svcr.RerankBlockedOnIssues, mc, request)
+
+ self.assertEqual(
+ [3, 4, 2, 5],
+ [blocked_on_ref.local_id
+ for blocked_on_ref in response.blocked_on_issue_refs])
+
+ def testRerankBlockedOnIssues_SplitAbove(self):
+ self.project.committer_ids.append(222)
+ issues = []
+ for idx in range(3, 6):
+ issues.append(fake.MakeTestIssue(
+ 789, idx, 'sum', 'New', 111, project_name='proj', issue_id=1000+idx))
+ self.services.issue.TestAddIssue(issues[-1])
+ self.issue_1.blocked_on_iids.append(issues[-1].issue_id)
+ self.issue_1.blocked_on_ranks.append(self.issue_1.blocked_on_ranks[-1]-1)
+
+ request = issues_pb2.RerankBlockedOnIssuesRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ moved_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=2),
+ target_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=4),
+ split_above=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver2@example.com')
+ response = self.CallWrapped(
+ self.issues_svcr.RerankBlockedOnIssues, mc, request)
+
+ self.assertEqual(
+ [3, 2, 4, 5],
+ [blocked_on_ref.local_id
+ for blocked_on_ref in response.blocked_on_issue_refs])
+
+ def testRerankBlockedOnIssues_CantEditIssue(self):
+ self.project.committer_ids.append(222)
+ issues = []
+ for idx in range(3, 6):
+ issues.append(fake.MakeTestIssue(
+ 789, idx, 'sum', 'New', 111, project_name='proj', issue_id=1000+idx))
+ self.services.issue.TestAddIssue(issues[-1])
+ self.issue_1.blocked_on_iids.append(issues[-1].issue_id)
+ self.issue_1.blocked_on_ranks.append(self.issue_1.blocked_on_ranks[-1]-1)
+
+ self.issue_1.labels = ['Restrict-EditIssue-Foo']
+
+ request = issues_pb2.RerankBlockedOnIssuesRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ moved_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=2),
+ target_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=4),
+ split_above=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver2@example.com')
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.RerankBlockedOnIssues, mc, request)
+
+ def testRerankBlockedOnIssues_ComplexPermissions(self):
+ """We can rerank blocked on issues, regardless of perms on other issues.
+
+ If Issue 1 is blocked on Issue 3 and Issue 4, we should be able to reorder
+ them as long as we have permission to edit Issue 1, even if we don't have
+ permission to view or edit Issues 3 or 4.
+ """
+ # Issue 3 is in proj2, which we don't have access to.
+ project_2 = self.services.project.TestAddProject(
+ 'proj2', project_id=790, owner_ids=[222], contrib_ids=[333])
+ project_2.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+ issue_3 = fake.MakeTestIssue(
+ 790, 3, 'sum', 'New', 111, project_name='proj2', issue_id=1003)
+
+ # Issue 4 requires a permission we don't have in order to edit it.
+ issue_4 = fake.MakeTestIssue(
+ 789, 4, 'sum', 'New', 111, project_name='proj', issue_id=1004)
+ issue_4.labels = ['Restrict-EditIssue-Foo']
+
+ self.services.issue.TestAddIssue(issue_3)
+ self.services.issue.TestAddIssue(issue_4)
+
+ self.issue_1.blocked_on_iids = [1003, 1004]
+ self.issue_1.blocked_on_ranks = [2, 1]
+
+ request = issues_pb2.RerankBlockedOnIssuesRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ moved_ref=common_pb2.IssueRef(
+ project_name='proj2',
+ local_id=3),
+ target_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=4),
+ split_above=False)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.issues_svcr.RerankBlockedOnIssues, mc, request)
+
+ self.assertEqual(
+ [4, 3],
+ [blocked_on_ref.local_id
+ for blocked_on_ref in response.blocked_on_issue_refs])
+
+ def testDeleteIssue_Delete(self):
+ """We can delete an issue."""
+ issue = self.services.issue.GetIssue(self.cnxn, self.issue_1.issue_id)
+ self.assertFalse(issue.deleted)
+
+ request = issues_pb2.DeleteIssueRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ delete=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.issues_svcr.DeleteIssue, mc, request)
+
+ issue = self.services.issue.GetIssue(self.cnxn, self.issue_1.issue_id)
+ self.assertTrue(issue.deleted)
+
+ def testDeleteIssue_Undelete(self):
+ """We can undelete an issue."""
+ self.services.issue.SoftDeleteIssue(
+ self.cnxn, self.project.project_id, 1, True, self.services.user)
+ issue = self.services.issue.GetIssue(self.cnxn, self.issue_1.issue_id)
+ self.assertTrue(issue.deleted)
+
+ request = issues_pb2.DeleteIssueRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ delete=False)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.issues_svcr.DeleteIssue, mc, request)
+
+ issue = self.services.issue.GetIssue(self.cnxn, self.issue_1.issue_id)
+ self.assertFalse(issue.deleted)
+
+ def testDeleteIssueComment_Delete(self):
+ """We can delete an issue comment."""
+ comment = tracker_pb2.IssueComment(
+ project_id=self.project.project_id,
+ issue_id=self.issue_1.issue_id,
+ user_id=111,
+ content='Foo',
+ timestamp=12345)
+ self.services.issue.TestAddComment(comment, self.issue_1.local_id)
+
+ request = issues_pb2.DeleteIssueCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ delete=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.issues_svcr.DeleteIssueComment, mc, request)
+
+ comment = self.services.issue.GetComment(self.cnxn, comment.id)
+ self.assertEqual(111, comment.deleted_by)
+
+ def testDeleteIssueComment_Undelete(self):
+ """We can undelete an issue comment."""
+ comment = tracker_pb2.IssueComment(
+ project_id=self.project.project_id,
+ issue_id=self.issue_1.issue_id,
+ user_id=111,
+ content='Foo',
+ timestamp=12345,
+ deleted_by=111)
+ self.services.issue.TestAddComment(comment, self.issue_1.local_id)
+
+ request = issues_pb2.DeleteIssueCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ delete=False)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.issues_svcr.DeleteIssueComment, mc, request)
+
+ comment = self.services.issue.GetComment(self.cnxn, comment.id)
+ self.assertIsNone(comment.deleted_by)
+
+ def testDeleteIssueComment_InvalidSequenceNum(self):
+ """We can handle invalid sequence numbers."""
+ request = issues_pb2.DeleteIssueCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ delete=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.DeleteIssueComment, mc, request)
+
+ def testDeleteAttachment_Delete(self):
+ """We can delete an issue comment attachment."""
+ comment = tracker_pb2.IssueComment(
+ project_id=self.project.project_id,
+ issue_id=self.issue_1.issue_id,
+ user_id=111,
+ content='Foo',
+ timestamp=12345)
+ self.services.issue.TestAddComment(comment, self.issue_1.local_id)
+ attachment = tracker_pb2.Attachment()
+ self.services.issue.TestAddAttachment(attachment, comment.id, 1)
+
+ request = issues_pb2.DeleteAttachmentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ attachment_id=attachment.attachment_id,
+ delete=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(
+ self.issues_svcr.DeleteAttachment, mc, request)
+
+ self.assertTrue(attachment.deleted)
+
+ def testDeleteAttachment_Undelete(self):
+ """We can undelete an issue comment attachment."""
+ comment = tracker_pb2.IssueComment(
+ project_id=self.project.project_id,
+ issue_id=self.issue_1.issue_id,
+ user_id=111,
+ content='Foo',
+ timestamp=12345,
+ deleted_by=111)
+ self.services.issue.TestAddComment(comment, self.issue_1.local_id)
+ attachment = tracker_pb2.Attachment(deleted=True)
+ self.services.issue.TestAddAttachment(attachment, comment.id, 1)
+
+ request = issues_pb2.DeleteAttachmentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ attachment_id=attachment.attachment_id,
+ delete=False)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(
+ self.issues_svcr.DeleteAttachment, mc, request)
+
+ self.assertFalse(attachment.deleted)
+
+ def testDeleteAttachment_InvalidSequenceNum(self):
+ """We can handle invalid sequence numbers."""
+ request = issues_pb2.DeleteAttachmentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ attachment_id=1234,
+ delete=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(
+ self.issues_svcr.DeleteAttachment, mc, request)
+
+ def testFlagIssues_Normal(self):
+ """Test that an user can flag an issue as spam."""
+ self.services.user.TestAddUser('user@example.com', 999)
+
+ request = issues_pb2.FlagIssuesRequest(
+ issue_refs=[
+ common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ common_pb2.IssueRef(
+ project_name='proj',
+ local_id=2)],
+ flag=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@example.com')
+ self.CallWrapped(self.issues_svcr.FlagIssues, mc, request)
+
+ issue_id = self.issue_1.issue_id
+ self.assertEqual(
+ [999], self.services.spam.reports_by_issue_id[issue_id])
+ self.assertNotIn(
+ 999, self.services.spam.manual_verdicts_by_issue_id[issue_id])
+
+ issue_id2 = self.issue_2.issue_id
+ self.assertEqual(
+ [999], self.services.spam.reports_by_issue_id[issue_id2])
+ self.assertNotIn(
+ 999, self.services.spam.manual_verdicts_by_issue_id[issue_id2])
+
+ def testFlagIssues_Unflag(self):
+ """Test that we can un-flag an issue as spam."""
+ self.services.spam.FlagIssues(
+ self.cnxn, self.services.issue, [self.issue_1], 111, True)
+ self.services.spam.RecordManualIssueVerdicts(
+ self.cnxn, self.services.issue, [self.issue_1], 111, True)
+
+ request = issues_pb2.FlagIssuesRequest(
+ issue_refs=[
+ common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1)],
+ flag=False)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.issues_svcr.FlagIssues, mc, request)
+
+ issue_id = self.issue_1.issue_id
+ self.assertEqual([], self.services.spam.reports_by_issue_id[issue_id])
+ self.assertFalse(
+ self.services.spam.manual_verdicts_by_issue_id[issue_id][111])
+
+ def testFlagIssues_OwnerAutoVerdict(self):
+ """Test that an owner can flag an issue as spam and it is a verdict."""
+ request = issues_pb2.FlagIssuesRequest(
+ issue_refs=[
+ common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1)],
+ flag=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.issues_svcr.FlagIssues, mc, request)
+
+ issue_id = self.issue_1.issue_id
+ self.assertEqual(
+ [111], self.services.spam.reports_by_issue_id[issue_id])
+ self.assertTrue(
+ self.services.spam.manual_verdicts_by_issue_id[issue_id][111])
+
+ def testFlagIssues_CommitterAutoVerdict(self):
+ """Test that an owner can flag an issue as spam and it is a verdict."""
+ self.services.user.TestAddUser('committer@example.com', 999)
+ self.services.project.TestAddProjectMembers(
+ [999], self.project, fake.COMMITTER_ROLE)
+
+ request = issues_pb2.FlagIssuesRequest(
+ issue_refs=[
+ common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1)],
+ flag=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='committer@example.com')
+ self.CallWrapped(self.issues_svcr.FlagIssues, mc, request)
+
+ issue_id = self.issue_1.issue_id
+ self.assertEqual(
+ [999], self.services.spam.reports_by_issue_id[issue_id])
+ self.assertTrue(
+ self.services.spam.manual_verdicts_by_issue_id[issue_id][999])
+
+ def testFlagIssues_ContributorAutoVerdict(self):
+ """Test that an owner can flag an issue as spam and it is a verdict."""
+ request = issues_pb2.FlagIssuesRequest(
+ issue_refs=[
+ common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1)],
+ flag=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver2@example.com')
+ self.CallWrapped(self.issues_svcr.FlagIssues, mc, request)
+
+ issue_id = self.issue_1.issue_id
+ self.assertEqual(
+ [222], self.services.spam.reports_by_issue_id[issue_id])
+ self.assertTrue(
+ self.services.spam.manual_verdicts_by_issue_id[issue_id][222])
+
+ def testFlagIssues_NotAllowed(self):
+ """Test that anon users cannot flag issues as spam."""
+ request = issues_pb2.FlagIssuesRequest(
+ issue_refs=[
+ common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1)],
+ flag=True)
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.FlagIssues, mc, request)
+
+ self.assertEqual(
+ [], self.services.spam.reports_by_issue_id[self.issue_1.issue_id])
+ self.assertEqual({}, self.services.spam.manual_verdicts_by_issue_id)
+
+ def testFlagIssues_CrossProjectNotAllowed(self):
+ """Test that cross-project requests are rejected."""
+ request = issues_pb2.FlagIssuesRequest(
+ issue_refs=[
+ common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ common_pb2.IssueRef(
+ project_name='proj2',
+ local_id=2)],
+ flag=True)
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.FlagIssues, mc, request)
+
+ self.assertEqual(
+ [], self.services.spam.reports_by_issue_id[self.issue_1.issue_id])
+ self.assertEqual({}, self.services.spam.manual_verdicts_by_issue_id)
+
+ def testFlagIssues_MissingIssueRefs(self):
+ request = issues_pb2.FlagIssuesRequest(flag=True)
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.FlagIssues, mc, request)
+
+ def testFlagComment_InvalidSequenceNumber(self):
+ """Test that we reject requests with invalid sequence numbers."""
+ request = issues_pb2.FlagCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ flag=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@example.com')
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.FlagComment, mc, request)
+
+ def testFlagComment_Normal(self):
+ """Test that an user can flag a comment as spam."""
+ self.services.user.TestAddUser('user@example.com', 999)
+ comment = tracker_pb2.IssueComment(
+ project_id=789, content='soon to be deleted', user_id=111,
+ issue_id=self.issue_1.issue_id)
+ self.services.issue.TestAddComment(comment, 1)
+
+ request = issues_pb2.FlagCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ flag=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@example.com')
+ self.CallWrapped(self.issues_svcr.FlagComment, mc, request)
+
+ comment_reports = self.services.spam.comment_reports_by_issue_id
+ manual_verdicts = self.services.spam.manual_verdicts_by_comment_id
+ self.assertEqual([999], comment_reports[self.issue_1.issue_id][comment.id])
+ self.assertNotIn(999, manual_verdicts[comment.id])
+
+ def testFlagComment_Unflag(self):
+ """Test that we can un-flag a comment as spam."""
+ comment = tracker_pb2.IssueComment(
+ project_id=789, content='soon to be deleted', user_id=999,
+ issue_id=self.issue_1.issue_id)
+ self.services.issue.TestAddComment(comment, 1)
+
+ self.services.spam.FlagComment(
+ self.cnxn, self.issue_1, comment.id, 999, 111, True)
+ self.services.spam.RecordManualCommentVerdict(
+ self.cnxn, self.services.issue, self.services.user, comment.id, 111,
+ True)
+
+ request = issues_pb2.FlagCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ flag=False)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.issues_svcr.FlagComment, mc, request)
+
+ comment_reports = self.services.spam.comment_reports_by_issue_id
+ manual_verdicts = self.services.spam.manual_verdicts_by_comment_id
+ self.assertEqual([], comment_reports[self.issue_1.issue_id][comment.id])
+ self.assertFalse(manual_verdicts[comment.id][111])
+
+ def testFlagComment_OwnerAutoVerdict(self):
+ """Test that an owner can flag a comment as spam and it is a verdict."""
+ comment = tracker_pb2.IssueComment(
+ project_id=789, content='soon to be deleted', user_id=999,
+ issue_id=self.issue_1.issue_id)
+ self.services.issue.TestAddComment(comment, 1)
+
+ request = issues_pb2.FlagCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ flag=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.issues_svcr.FlagComment, mc, request)
+
+ comment_reports = self.services.spam.comment_reports_by_issue_id
+ manual_verdicts = self.services.spam.manual_verdicts_by_comment_id
+ self.assertEqual([111], comment_reports[self.issue_1.issue_id][comment.id])
+ self.assertTrue(manual_verdicts[comment.id][111])
+
+ def testFlagComment_CommitterAutoVerdict(self):
+ """Test that an owner can flag an issue as spam and it is a verdict."""
+ self.services.user.TestAddUser('committer@example.com', 999)
+ self.services.project.TestAddProjectMembers(
+ [999], self.project, fake.COMMITTER_ROLE)
+
+ comment = tracker_pb2.IssueComment(
+ project_id=789, content='soon to be deleted', user_id=999,
+ issue_id=self.issue_1.issue_id)
+ self.services.issue.TestAddComment(comment, 1)
+
+ request = issues_pb2.FlagCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ flag=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='committer@example.com')
+ self.CallWrapped(self.issues_svcr.FlagComment, mc, request)
+
+ comment_reports = self.services.spam.comment_reports_by_issue_id
+ manual_verdicts = self.services.spam.manual_verdicts_by_comment_id
+ self.assertEqual([999], comment_reports[self.issue_1.issue_id][comment.id])
+ self.assertTrue(manual_verdicts[comment.id][999])
+
+ def testFlagComment_ContributorAutoVerdict(self):
+ """Test that an owner can flag an issue as spam and it is a verdict."""
+ comment = tracker_pb2.IssueComment(
+ project_id=789, content='soon to be deleted', user_id=999,
+ issue_id=self.issue_1.issue_id)
+ self.services.issue.TestAddComment(comment, 1)
+
+ request = issues_pb2.FlagCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ flag=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver2@example.com')
+ self.CallWrapped(self.issues_svcr.FlagComment, mc, request)
+
+ comment_reports = self.services.spam.comment_reports_by_issue_id
+ manual_verdicts = self.services.spam.manual_verdicts_by_comment_id
+ self.assertEqual([222], comment_reports[self.issue_1.issue_id][comment.id])
+ self.assertTrue(manual_verdicts[comment.id][222])
+
+ def testFlagComment_NotAllowed(self):
+ """Test that anon users cannot flag issues as spam."""
+ comment = tracker_pb2.IssueComment(
+ project_id=789, content='soon to be deleted', user_id=999,
+ issue_id=self.issue_1.issue_id)
+ self.services.issue.TestAddComment(comment, 1)
+
+ request = issues_pb2.FlagCommentRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ sequence_num=1,
+ flag=True)
+ mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.FlagComment, mc, request)
+
+ comment_reports = self.services.spam.comment_reports_by_issue_id
+ manual_verdicts = self.services.spam.manual_verdicts_by_comment_id
+ self.assertNotIn(comment.id, comment_reports[self.issue_1.issue_id])
+ self.assertEqual({}, manual_verdicts[comment.id])
+
+ def testListIssuePermissions_Normal(self):
+ issue_1 = fake.MakeTestIssue(
+ 789, 1, 'sum', 'New', 111, project_name='proj', issue_id=1001)
+ self.services.issue.TestAddIssue(issue_1)
+
+ request = issues_pb2.ListIssuePermissionsRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@example.com')
+
+ response = self.CallWrapped(
+ self.issues_svcr.ListIssuePermissions, mc, request)
+ self.assertEqual(
+ issues_pb2.ListIssuePermissionsResponse(
+ permissions=[
+ 'addissuecomment',
+ 'createissue',
+ 'deleteown',
+ 'flagspam',
+ 'setstar',
+ 'view']),
+ response)
+
+ def testListIssuePermissions_DeletedIssue(self):
+ issue_1 = fake.MakeTestIssue(
+ 789, 1, 'sum', 'New', 111, project_name='proj', issue_id=1001)
+ issue_1.deleted = True
+ self.services.issue.TestAddIssue(issue_1)
+
+ request = issues_pb2.ListIssuePermissionsRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver2@example.com')
+
+ response = self.CallWrapped(
+ self.issues_svcr.ListIssuePermissions, mc, request)
+ self.assertEqual(
+ issues_pb2.ListIssuePermissionsResponse(permissions=['view']),
+ response)
+
+ def testListIssuePermissions_CanViewDeletedIssue(self):
+ issue_1 = fake.MakeTestIssue(
+ 789, 1, 'sum', 'New', 111, project_name='proj', issue_id=1001)
+ issue_1.deleted = True
+ self.services.issue.TestAddIssue(issue_1)
+
+ request = issues_pb2.ListIssuePermissionsRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ response = self.CallWrapped(
+ self.issues_svcr.ListIssuePermissions, mc, request)
+ self.assertEqual(
+ issues_pb2.ListIssuePermissionsResponse(permissions=[
+ 'deleteissue',
+ 'view']),
+ response)
+
+ def testListIssuePermissions_IssueRestrictions(self):
+ issue_1 = fake.MakeTestIssue(
+ 789, 1, 'sum', 'New', 111, project_name='proj', issue_id=1001)
+ issue_1.labels = ['Restrict-SetStar-CustomPerm']
+ self.services.issue.TestAddIssue(issue_1)
+
+ request = issues_pb2.ListIssuePermissionsRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver2@example.com')
+
+ response = self.CallWrapped(
+ self.issues_svcr.ListIssuePermissions, mc, request)
+ self.assertEqual(
+ issues_pb2.ListIssuePermissionsResponse(
+ permissions=[
+ 'addissuecomment',
+ 'createissue',
+ 'deleteown',
+ 'flagspam',
+ 'verdictspam',
+ 'view']),
+ response)
+
+ def testListIssuePermissions_IssueGrantedPerms(self):
+ self.services.config.CreateFieldDef(
+ self.cnxn, 789, 'Field Name', 'USER_TYPE', None, None, None, None, None,
+ None, None, None, None, None, 'CustomPerm', None, None, 'Docstring', [],
+ [])
+ issue_1 = fake.MakeTestIssue(
+ 789, 1, 'sum', 'New', 111, project_name='proj', issue_id=1001)
+ issue_1.labels = ['Restrict-SetStar-CustomPerm']
+ issue_1.field_values = [tracker_pb2.FieldValue(user_id=222, field_id=123)]
+ self.services.issue.TestAddIssue(issue_1)
+
+ request = issues_pb2.ListIssuePermissionsRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='approver2@example.com')
+
+ response = self.CallWrapped(
+ self.issues_svcr.ListIssuePermissions, mc, request)
+ self.assertEqual(
+ issues_pb2.ListIssuePermissionsResponse(
+ permissions=[
+ 'addissuecomment',
+ 'createissue',
+ 'customperm',
+ 'deleteown',
+ 'flagspam',
+ 'setstar',
+ 'verdictspam',
+ 'view']),
+ response)
+
+ @patch('services.tracker_fulltext.IndexIssues')
+ @patch('services.tracker_fulltext.UnindexIssues')
+ def testMoveIssue_Normal(self, _mock_index, _mock_unindex):
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
+ self.services.issue.TestAddIssue(issue)
+ self.project.owner_ids = [111]
+ target_project = self.services.project.TestAddProject(
+ 'dest', project_id=988, committer_ids=[111])
+
+ request = issues_pb2.MoveIssueRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ target_project_name='dest')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.issues_svcr.MoveIssue, mc, request)
+
+ self.assertEqual(
+ issues_pb2.MoveIssueResponse(
+ new_issue_ref=common_pb2.IssueRef(
+ project_name='dest',
+ local_id=1)),
+ response)
+
+ moved_issue = self.services.issue.GetIssueByLocalID(self.cnxn,
+ target_project.project_id, 1)
+ self.assertEqual(target_project.project_id, moved_issue.project_id)
+ self.assertEqual(issue.summary, moved_issue.summary)
+ self.assertEqual(moved_issue.reporter_id, 111)
+
+ @patch('services.tracker_fulltext.IndexIssues')
+ def testCopyIssue_Normal(self, _mock_index):
+ issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
+ self.services.issue.TestAddIssue(issue)
+ self.project.owner_ids = [111]
+
+ request = issues_pb2.CopyIssueRequest(
+ issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=1),
+ target_project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.issues_svcr.CopyIssue, mc, request)
+
+ self.assertEqual(
+ issues_pb2.CopyIssueResponse(
+ new_issue_ref=common_pb2.IssueRef(
+ project_name='proj',
+ local_id=3)),
+ response)
+
+ copied_issue = self.services.issue.GetIssueByLocalID(self.cnxn,
+ self.project.project_id, 3)
+ self.assertEqual(self.project.project_id, copied_issue.project_id)
+ self.assertEqual(issue.summary, copied_issue.summary)
+ self.assertEqual(copied_issue.reporter_id, 111)
diff --git a/api/test/monorail_servicer_test.py b/api/test/monorail_servicer_test.py
new file mode 100644
index 0000000..8c5a1d3
--- /dev/null
+++ b/api/test/monorail_servicer_test.py
@@ -0,0 +1,484 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for MonorailServicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import time
+import unittest
+import mock
+import mox
+
+from components.prpc import server
+from components.prpc import codes
+from components.prpc import context
+from google.appengine.ext import testbed
+from google.protobuf import json_format
+
+import settings
+from api import monorail_servicer
+from framework import authdata
+from framework import exceptions
+from framework import framework_constants
+from framework import monorailcontext
+from framework import permissions
+from framework import ratelimiter
+from framework import xsrf
+from services import cachemanager_svc
+from services import config_svc
+from services import service_manager
+from services import features_svc
+from testing import fake
+from testing import testing_helpers
+
+
+class MonorailServicerFunctionsTest(unittest.TestCase):
+
+ def testConvertPRPCStatusToHTTPStatus(self):
+ """We can convert pRPC status codes to http codes for monitoring."""
+ prpc_context = context.ServicerContext()
+
+ prpc_context.set_code(codes.StatusCode.OK)
+ self.assertEqual(
+ 200, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
+
+ prpc_context.set_code(codes.StatusCode.INVALID_ARGUMENT)
+ self.assertEqual(
+ 400, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
+
+ prpc_context.set_code(codes.StatusCode.PERMISSION_DENIED)
+ self.assertEqual(
+ 403, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
+
+ prpc_context.set_code(codes.StatusCode.NOT_FOUND)
+ self.assertEqual(
+ 404, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
+
+ prpc_context.set_code(codes.StatusCode.INTERNAL)
+ self.assertEqual(
+ 500, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
+
+
+class UpdateSomethingRequest(testing_helpers.Blank):
+ """A fake request that would do a write."""
+ pass
+
+
+class ListSomethingRequest(testing_helpers.Blank):
+ """A fake request that would do a read."""
+ pass
+
+
+class TestableServicer(monorail_servicer.MonorailServicer):
+ """Fake servicer class."""
+
+ def __init__(self, services):
+ super(TestableServicer, self).__init__(services)
+ self.was_called = False
+ self.seen_mc = None
+ self.seen_request = None
+
+ @monorail_servicer.PRPCMethod
+ def CalcSomething(self, mc, request):
+ """Raise the test exception, or return what we got for verification."""
+ self.was_called = True
+ self.seen_mc = mc
+ self.seen_request = request
+ assert mc
+ assert request
+ if request.exc_class:
+ raise request.exc_class()
+ else:
+ return 'fake response proto'
+
+
+class MonorailServicerTest(unittest.TestCase):
+
+ def setUp(self):
+ self.mox = mox.Mox()
+ self.testbed = testbed.Testbed()
+ self.testbed.activate()
+ self.testbed.init_memcache_stub()
+ self.testbed.init_datastore_v3_stub()
+ self.testbed.init_user_stub()
+
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService(),
+ project=fake.ProjectService(),
+ cache_manager=fake.CacheManager())
+ self.project = self.services.project.TestAddProject(
+ 'proj', project_id=789, owner_ids=[111])
+ # allowlisted_bot's email is allowlisted in testing/api_clients.cfg.
+ self.allowlisted_bot = self.services.user.TestAddUser(
+ '123456789@developer.gserviceaccount.com', 999)
+ # allowlisted_client_id_user is used to test accounts that are only
+ # allowlisted with the client_id.
+ self.allowlisted_client_id_user = self.services.user.TestAddUser(
+ 'allowlisted-with-client-id@developer.gserviceaccount.com', 888)
+ self.non_member = self.services.user.TestAddUser(
+ 'nonmember@example.com', 222)
+ self.allowed_domain_user = self.services.user.TestAddUser(
+ 'chickenchicken@google.com', 333)
+ self.test_user = self.services.user.TestAddUser('test@example.com', 420)
+ self.svcr = TestableServicer(self.services)
+ self.nonmember_token = xsrf.GenerateToken(222, xsrf.XHR_SERVLET_PATH)
+ self.request = UpdateSomethingRequest(exc_class=None)
+ self.prpc_context = context.ServicerContext()
+ self.prpc_context.set_code(codes.StatusCode.OK)
+ self.prpc_context._invocation_metadata = [
+ (monorail_servicer.XSRF_TOKEN_HEADER, self.nonmember_token)]
+
+ self.oauth_patcher = mock.patch(
+ 'google.appengine.api.oauth.get_current_user')
+ self.mock_oauth_gcu = self.oauth_patcher.start()
+ self.mock_oauth_gcu.return_value = None
+
+ self.oauth_client_id_patcher = mock.patch(
+ 'google.appengine.api.oauth.get_client_id')
+ self.mock_oauth_gcid = self.oauth_client_id_patcher.start()
+ self.mock_oauth_gcid.return_value = "1234common.clientid"
+
+ # TODO(b/144508063): remove this workaround.
+ self.oauth_authorized_scopes_patcher = mock.patch(
+ 'google.appengine.api.oauth.get_authorized_scopes')
+ self.mock_oauth_gas = self.oauth_authorized_scopes_patcher.start()
+ self.mock_oauth_gas.return_value = [framework_constants.MONORAIL_SCOPE]
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+ self.testbed.deactivate()
+
+ def SetUpRecordMonitoringStats(self):
+ self.mox.StubOutWithMock(json_format, 'MessageToJson')
+ json_format.MessageToJson(self.request).AndReturn('json of request')
+ json_format.MessageToJson('fake response proto').AndReturn(
+ 'json of response')
+ self.mox.ReplayAll()
+
+ def testRun_SiteWide_Normal(self):
+ """Calling the handler through the decorator."""
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ self.SetUpRecordMonitoringStats()
+ # pylint: disable=unexpected-keyword-arg
+ response = self.svcr.CalcSomething(
+ self.request, self.prpc_context, cnxn=self.cnxn)
+ self.assertIsNone(self.svcr.seen_mc.cnxn) # Because of CleanUp().
+ self.assertEqual(self.svcr.seen_mc.auth.email, self.non_member.email)
+ self.assertIn(permissions.CREATE_HOTLIST.lower(),
+ self.svcr.seen_mc.perms.perm_names)
+ self.assertNotIn(permissions.ADMINISTER_SITE.lower(),
+ self.svcr.seen_mc.perms.perm_names)
+ self.assertEqual(self.request, self.svcr.seen_request)
+ self.assertEqual('fake response proto', response)
+ self.assertEqual(codes.StatusCode.OK, self.prpc_context._code)
+
+ def testRun_RequesterBanned(self):
+ """If we reject the request, give PERMISSION_DENIED."""
+ self.non_member.banned = 'Spammer'
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ self.SetUpRecordMonitoringStats()
+ # pylint: disable=unexpected-keyword-arg
+ self.svcr.CalcSomething(
+ self.request, self.prpc_context, cnxn=self.cnxn)
+ self.assertFalse(self.svcr.was_called)
+ self.assertEqual(
+ codes.StatusCode.PERMISSION_DENIED, self.prpc_context._code)
+
+ def testRun_AnonymousRequester(self):
+ """Test we properly process anonymous users with valid tokens."""
+ self.prpc_context._invocation_metadata = [
+ (monorail_servicer.XSRF_TOKEN_HEADER,
+ xsrf.GenerateToken(0, xsrf.XHR_SERVLET_PATH))]
+ self.SetUpRecordMonitoringStats()
+ # pylint: disable=unexpected-keyword-arg
+ response = self.svcr.CalcSomething(
+ self.request, self.prpc_context, cnxn=self.cnxn)
+ self.assertIsNone(self.svcr.seen_mc.cnxn) # Because of CleanUp().
+ self.assertIsNone(self.svcr.seen_mc.auth.email)
+ self.assertNotIn(permissions.CREATE_HOTLIST.lower(),
+ self.svcr.seen_mc.perms.perm_names)
+ self.assertNotIn(permissions.ADMINISTER_SITE.lower(),
+ self.svcr.seen_mc.perms.perm_names)
+ self.assertEqual(self.request, self.svcr.seen_request)
+ self.assertEqual('fake response proto', response)
+ self.assertEqual(codes.StatusCode.OK, self.prpc_context._code)
+
+ def testRun_DistributedInvalidation(self):
+ """The Run method must call DoDistributedInvalidation()."""
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ self.SetUpRecordMonitoringStats()
+ # pylint: disable=unexpected-keyword-arg
+ self.svcr.CalcSomething(
+ self.request, self.prpc_context, cnxn=self.cnxn)
+ self.assertIsNotNone(self.services.cache_manager.last_call)
+
+ def testRun_HandlerErrorResponse(self):
+ """An expected exception in the method causes an error status."""
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ self.SetUpRecordMonitoringStats()
+ # pylint: disable=attribute-defined-outside-init
+ self.request.exc_class = exceptions.NoSuchUserException
+ # pylint: disable=unexpected-keyword-arg
+ response = self.svcr.CalcSomething(
+ self.request, self.prpc_context, cnxn=self.cnxn)
+ self.assertTrue(self.svcr.was_called)
+ self.assertIsNone(self.svcr.seen_mc.cnxn) # Because of CleanUp().
+ self.assertEqual(self.svcr.seen_mc.auth.email, self.non_member.email)
+ self.assertEqual(self.request, self.svcr.seen_request)
+ self.assertIsNone(response)
+ self.assertEqual(codes.StatusCode.NOT_FOUND, self.prpc_context._code)
+
+ def testRun_HandlerProgrammingError(self):
+ """An unexception in the handler method is re-raised."""
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ self.SetUpRecordMonitoringStats()
+ # pylint: disable=attribute-defined-outside-init
+ self.request.exc_class = NotImplementedError
+ self.assertRaises(
+ NotImplementedError,
+ self.svcr.CalcSomething,
+ self.request, self.prpc_context, cnxn=self.cnxn)
+ self.assertTrue(self.svcr.was_called)
+ self.assertIsNone(self.svcr.seen_mc.cnxn) # Because of CleanUp().
+
+ def testGetAndAssertRequesterAuth_Cookie_Anon(self):
+ """We get and allow requests from anon user using cookie auth."""
+ metadata = {
+ monorail_servicer.XSRF_TOKEN_HEADER: xsrf.GenerateToken(
+ 0, xsrf.XHR_SERVLET_PATH)}
+ # Signed out.
+ self.assertIsNone(self.svcr.GetAndAssertRequesterAuth(
+ self.cnxn, metadata, self.services).email)
+
+ def testGetAndAssertRequesterAuth_Cookie_SignedIn(self):
+ """We get and allow requests from signed in users using cookie auth."""
+ metadata = dict(self.prpc_context.invocation_metadata())
+ # Signed in with cookie auth.
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ user_auth = self.svcr.GetAndAssertRequesterAuth(
+ self.cnxn, metadata, self.services)
+ self.assertEqual(self.non_member.email, user_auth.email)
+
+ def testGetAndAssertRequester_Anon_BadToken(self):
+ """We get the email address of the signed in user using oauth."""
+ metadata = {}
+ # Anonymous user has invalid token.
+ with self.assertRaises(permissions.PermissionException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ def testGetAndAssertRequester_Oauth_AllowedDomain_NoMonorailScope(self):
+ """We reject users with allowed domains but no monorail scope."""
+ metadata = {}
+ self.mock_oauth_gcu.return_value = None
+
+ with self.assertRaises(permissions.PermissionException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ def testGetAndAssertRequester_Oauth_BadDomain_MonorailScope(self):
+ """We reject users with bad domains using the monorail scope."""
+ metadata = {}
+ def side_effect(scope=None):
+ if scope == framework_constants.MONORAIL_SCOPE:
+ return testing_helpers.Blank(
+ email=lambda: 'testchicken@chicken.com', client_id=lambda: 7899)
+ return None
+ self.mock_oauth_gcu.side_effect = side_effect
+
+ with self.assertRaises(permissions.PermissionException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ def testGetAndAssertRequester_Oauth_AllowedDomain_MonorailScope(self):
+ """We get and allow users with allowed domains using the monorail scope."""
+ metadata = {}
+ def side_effect(scope=None):
+ if scope == framework_constants.MONORAIL_SCOPE:
+ return testing_helpers.Blank(
+ email=lambda: self.allowed_domain_user.email,
+ client_id=lambda: 7899)
+ return None
+ self.mock_oauth_gcu.side_effect = side_effect
+
+ user_auth = self.svcr.GetAndAssertRequesterAuth(
+ self.cnxn, metadata, self.services)
+ self.assertEqual(user_auth.email, self.allowed_domain_user.email)
+
+ def testGetAndAssertRequesterAuth_Oauth_Allowlisted(self):
+ metadata = {}
+ # Signed in with oauth.
+ self.mock_oauth_gcu.return_value = testing_helpers.Blank(
+ email=lambda: self.allowlisted_bot.email)
+
+ bot_auth = self.svcr.GetAndAssertRequesterAuth(
+ self.cnxn, metadata, self.services)
+ self.assertEqual(bot_auth.email, self.allowlisted_bot.email)
+
+ def testGetAndAssertRequesterAuth_Oauth_NotAllowlisted(self):
+ metadata = {}
+ # Signed in with oauth.
+ self.mock_oauth_gcu.return_value = testing_helpers.Blank(
+ email=lambda: 'who-is-this@test.com')
+
+ with self.assertRaises(permissions.PermissionException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ def testGetAndAssertRequesterAuth_Oauth_ClientIDOnly(self):
+ """We get and allow accounts that only have their client_id allowlisted."""
+ metadata = {}
+ self.mock_oauth_gcu.return_value = testing_helpers.Blank(
+ email=lambda: self.allowlisted_client_id_user.email)
+ self.mock_oauth_gcid.return_value = "98723764876"
+ both_auth = self.svcr.GetAndAssertRequesterAuth(
+ self.cnxn, metadata, self.services)
+ self.assertEqual(both_auth.email, self.allowlisted_client_id_user.email)
+
+ def testGetAndAssertRequesterAuth_Banned(self):
+ self.non_member.banned = 'Spammer'
+ metadata = dict(self.prpc_context.invocation_metadata())
+ # Signed in with cookie auth.
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ with self.assertRaises(permissions.BannedUserException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ def testGetRequester_TestAccountOnAppspot(self):
+ """Specifying test_account is ignored on deployed server."""
+ # pylint: disable=attribute-defined-outside-init
+ metadata = {'x-test-account': 'test@example.com'}
+ with self.assertRaises(exceptions.InputException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ def testGetRequester_TestAccountOnDev(self):
+ """For integration testing, we can set test_account on dev_server."""
+ try:
+ orig_local_mode = settings.local_mode
+ settings.local_mode = True
+
+ # pylint: disable=attribute-defined-outside-init
+ metadata = {'x-test-account': 'test@example.com'}
+ test_auth = self.svcr.GetAndAssertRequesterAuth(
+ self.cnxn, metadata, self.services)
+ self.assertEqual('test@example.com', test_auth.email)
+
+ # pylint: disable=attribute-defined-outside-init
+ metadata = {'x-test-account': 'test@anythingelse.com'}
+ with self.assertRaises(exceptions.InputException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+ finally:
+ settings.local_mode = orig_local_mode
+
+ def testAssertBaseChecks_SiteIsReadOnly_Write(self):
+ """We reject writes and allow reads when site is read-only."""
+ orig_read_only = settings.read_only
+ try:
+ settings.read_only = True
+ metadata = {}
+ self.assertRaises(
+ permissions.PermissionException,
+ self.svcr.AssertBaseChecks, self.request, metadata)
+ finally:
+ settings.read_only = orig_read_only
+
+ def testAssertBaseChecks_SiteIsReadOnly_Read(self):
+ """We reject writes and allow reads when site is read-only."""
+ orig_read_only = settings.read_only
+ try:
+ settings.read_only = True
+ metadata = {monorail_servicer.XSRF_TOKEN_HEADER: self.nonmember_token}
+
+ # Our default request is an update.
+ with self.assertRaises(permissions.PermissionException):
+ self.svcr.AssertBaseChecks(self.request, metadata)
+
+ # A method name starting with "List" or "Get" will run OK.
+ self.request = ListSomethingRequest(exc_class=None)
+ self.svcr.AssertBaseChecks(self.request, metadata)
+ finally:
+ settings.read_only = orig_read_only
+
+ def testGetRequestProject(self):
+ """We get a project specified by request field project_name."""
+ # No project specified.
+ self.assertIsNone(self.svcr.GetRequestProject(self.cnxn, self.request))
+
+ # Existing project specified.
+ # pylint: disable=attribute-defined-outside-init
+ self.request.project_name = 'proj'
+ self.assertEqual(
+ self.project, self.svcr.GetRequestProject(self.cnxn, self.request))
+
+ # Bad project specified.
+ # pylint: disable=attribute-defined-outside-init
+ self.request.project_name = 'not-a-proj'
+ self.assertIsNone(self.svcr.GetRequestProject(self.cnxn, self.request))
+
+ def CheckExceptionStatus(self, e, expected_code, details=None):
+ mc = monorailcontext.MonorailContext(self.services)
+ self.prpc_context.set_code(codes.StatusCode.OK)
+ processed = self.svcr.ProcessException(e, self.prpc_context, mc)
+ if expected_code:
+ self.assertTrue(processed)
+ self.assertEqual(expected_code, self.prpc_context._code)
+ else:
+ self.assertFalse(processed)
+ # Uncaught exceptions should indicate an error.
+ self.assertEqual(codes.StatusCode.INTERNAL, self.prpc_context._code)
+ if details is not None:
+ self.assertEqual(details, self.prpc_context._details)
+
+ def testProcessException(self):
+ """Expected exceptions are converted to pRPC codes, expected not."""
+ self.CheckExceptionStatus(
+ exceptions.NoSuchUserException(), codes.StatusCode.NOT_FOUND)
+ self.CheckExceptionStatus(
+ exceptions.NoSuchProjectException(), codes.StatusCode.NOT_FOUND)
+ self.CheckExceptionStatus(
+ exceptions.NoSuchIssueException(), codes.StatusCode.NOT_FOUND)
+ self.CheckExceptionStatus(
+ exceptions.NoSuchComponentException(), codes.StatusCode.NOT_FOUND)
+ self.CheckExceptionStatus(
+ permissions.BannedUserException(), codes.StatusCode.PERMISSION_DENIED)
+ self.CheckExceptionStatus(
+ permissions.PermissionException(), codes.StatusCode.PERMISSION_DENIED)
+ self.CheckExceptionStatus(
+ exceptions.GroupExistsException(), codes.StatusCode.ALREADY_EXISTS)
+ self.CheckExceptionStatus(
+ exceptions.InvalidComponentNameException(),
+ codes.StatusCode.INVALID_ARGUMENT)
+ self.CheckExceptionStatus(
+ exceptions.InputException('echoed values'),
+ codes.StatusCode.INVALID_ARGUMENT,
+ details='Invalid arguments: echoed values')
+ self.CheckExceptionStatus(
+ exceptions.FilterRuleException(),
+ codes.StatusCode.INVALID_ARGUMENT,
+ details='Violates filter rule that should error.')
+ self.CheckExceptionStatus(
+ ratelimiter.ApiRateLimitExceeded('client_id', 'email'),
+ codes.StatusCode.PERMISSION_DENIED)
+ self.CheckExceptionStatus(
+ features_svc.HotlistAlreadyExists(), codes.StatusCode.ALREADY_EXISTS)
+ self.CheckExceptionStatus(NotImplementedError(), None)
+
+ def testProcessException_ErrorMessageEscaped(self):
+ """If we ever echo user input in error messages, it is escaped.."""
+ self.CheckExceptionStatus(
+ exceptions.InputException('echoed <script>"code"</script>'),
+ codes.StatusCode.INVALID_ARGUMENT,
+ details=('Invalid arguments: echoed '
+ '<script>"code"</script>'))
+
+ def testRecordMonitoringStats_RequestClassDoesNotEndInRequest(self):
+ """We cope with request proto class names that do not end in 'Request'."""
+ self.request = 'this is a string'
+ self.SetUpRecordMonitoringStats()
+ start_time = 1522559788.939511
+ now = 1522569311.892738
+ self.svcr.RecordMonitoringStats(
+ start_time, self.request, 'fake response proto', self.prpc_context,
+ now=now)
diff --git a/api/test/projects_servicer_test.py b/api/test/projects_servicer_test.py
new file mode 100644
index 0000000..b3084c3
--- /dev/null
+++ b/api/test/projects_servicer_test.py
@@ -0,0 +1,1086 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for the projects servicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+from mock import patch
+
+from components.prpc import codes
+from components.prpc import context
+from components.prpc import server
+
+from api import projects_servicer
+from api.api_proto import common_pb2
+from api.api_proto import issue_objects_pb2
+from api.api_proto import project_objects_pb2
+from api.api_proto import projects_pb2
+from framework import authdata
+from framework import exceptions
+from framework import framework_constants
+from framework import monorailcontext
+from framework import permissions
+from proto import tracker_pb2
+from proto import project_pb2
+from tracker import tracker_bizobj
+from tracker import tracker_constants
+from testing import fake
+from testing import testing_helpers
+from services import service_manager
+
+
+class ProjectsServicerTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService(),
+ project=fake.ProjectService(),
+ project_star=fake.ProjectStarService(),
+ features=fake.FeaturesService())
+
+ self.admin = self.services.user.TestAddUser('admin@example.com', 123)
+ self.admin.is_site_admin = True
+ self.owner = self.services.user.TestAddUser('owner@example.com', 111)
+ self.services.user.TestAddUser('user_222@example.com', 222)
+ self.services.user.TestAddUser('user_333@example.com', 333)
+ self.services.user.TestAddUser('user_444@example.com', 444)
+ self.services.user.TestAddUser('user_666@example.com', 666)
+
+ # User group 888 has members: user_555 and proj@monorail.com
+ self.services.user.TestAddUser('group888@googlegroups.com', 888)
+ self.services.usergroup.TestAddGroupSettings(
+ 888, 'group888@googlegroups.com')
+ self.services.usergroup.TestAddMembers(888, [555, 1001])
+
+ # User group 999 has members: user_111 and user_444
+ self.services.user.TestAddUser('group999@googlegroups.com', 999)
+ self.services.usergroup.TestAddGroupSettings(
+ 999, 'group999@googlegroups.com')
+ self.services.usergroup.TestAddMembers(999, [111, 444])
+
+ # User group 777 has members: user_666 and group 999.
+ self.services.user.TestAddUser('group777@googlegroups.com', 777)
+ self.services.usergroup.TestAddGroupSettings(
+ 777, 'group777@googlegroups.com')
+ self.services.usergroup.TestAddMembers(777, [666, 999])
+
+ self.project = self.services.project.TestAddProject(
+ 'proj',
+ project_id=789,
+ owner_ids=[111],
+ committer_ids=[222],
+ contrib_ids=[333])
+ self.projects_svcr = projects_servicer.ProjectsServicer(
+ self.services, make_rate_limiter=False)
+ self.prpc_context = context.ServicerContext()
+ self.prpc_context.set_code(codes.StatusCode.OK)
+
+ def CallWrapped(self, wrapped_handler, *args, **kwargs):
+ return wrapped_handler.wrapped(self.projects_svcr, *args, **kwargs)
+
+ def testListProjects_Normal(self):
+ """We can get a list of all projects on the site."""
+ request = projects_pb2.ListProjectsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.projects_svcr.ListProjects, mc, request)
+ self.assertEqual(2, len(response.projects))
+
+ def testGetConfig_Normal(self):
+ """We can get a project config."""
+ request = projects_pb2.GetConfigRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.projects_svcr.GetConfig, mc, request)
+ self.assertEqual('proj', response.project_name)
+
+ def testGetConfig_NoSuchProject(self):
+ """We reject a request to get a config for a non-existent project."""
+ request = projects_pb2.GetConfigRequest(project_name='unknown-proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ self.CallWrapped(self.projects_svcr.GetConfig, mc, request)
+
+ def testGetConfig_PermissionDenied(self):
+ """We reject a request to get a config for a non-viewable project."""
+ self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+ request = projects_pb2.GetConfigRequest(project_name='proj')
+
+ # User is a member of the members-only project.
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.projects_svcr.GetConfig, mc, request)
+ self.assertEqual('proj', response.project_name)
+
+ # User is not a member of the members-only project.
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='nonmember@example.com')
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.projects_svcr.GetConfig, mc, request)
+
+ @patch('businesslogic.work_env.WorkEnv.ListProjectTemplates')
+ def testListProjectTemplates_Normal(self, mockListProjectTemplates):
+ fd_1 = tracker_pb2.FieldDef(
+ field_name='FirstField', field_id=1,
+ field_type=tracker_pb2.FieldTypes.STR_TYPE)
+ fd_2 = tracker_pb2.FieldDef(
+ field_name='LegalApproval', field_id=2,
+ field_type=tracker_pb2.FieldTypes.APPROVAL_TYPE)
+ component = tracker_pb2.ComponentDef(component_id=1, path='dude')
+ status_def = tracker_pb2.StatusDef(status='New', means_open=True)
+ config = tracker_pb2.ProjectIssueConfig(
+ project_id=789, field_defs=[fd_1, fd_2], component_defs=[component],
+ well_known_statuses=[status_def])
+ self.services.config.StoreConfig(self.cnxn, config)
+ admin1 = self.services.user.TestAddUser('admin@example.com', 222)
+ appr1 = self.services.user.TestAddUser('approver@example.com', 333)
+ setter = self.services.user.TestAddUser('setter@example.com', 444)
+ template = tracker_pb2.TemplateDef(
+ name='Chicken', content='description', summary='summary',
+ status='New', admin_ids=[admin1.user_id],
+ field_values=[tracker_bizobj.MakeFieldValue(
+ fd_1.field_id, None, 'Cow', None, None, None, False)],
+ component_ids=[component.component_id],
+ approval_values=[tracker_pb2.ApprovalValue(
+ approval_id=2, approver_ids=[appr1.user_id],
+ setter_id=setter.user_id)])
+ mockListProjectTemplates.return_value = [template]
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ request = projects_pb2.ListProjectTemplatesRequest(project_name='proj')
+ response = self.CallWrapped(
+ self.projects_svcr.ListProjectTemplates, mc, request)
+ self.assertEqual(
+ response,
+ projects_pb2.ListProjectTemplatesResponse(
+ templates=[project_objects_pb2.TemplateDef(
+ template_name='Chicken',
+ content='description',
+ summary='summary',
+ status_ref=common_pb2.StatusRef(
+ status='New',
+ is_derived=False,
+ means_open=True),
+ owner_defaults_to_member=True,
+ admin_refs=[
+ common_pb2.UserRef(
+ user_id=admin1.user_id,
+ display_name=testing_helpers.ObscuredEmail(admin1.email),
+ is_derived=False)],
+ field_values=[
+ issue_objects_pb2.FieldValue(
+ field_ref=common_pb2.FieldRef(
+ field_id=fd_1.field_id,
+ field_name=fd_1.field_name,
+ type=common_pb2.STR_TYPE),
+ value='Cow')],
+ component_refs=[
+ common_pb2.ComponentRef(
+ path=component.path, is_derived=False)],
+ approval_values=[
+ issue_objects_pb2.Approval(
+ field_ref=common_pb2.FieldRef(
+ field_id=fd_2.field_id,
+ field_name=fd_2.field_name,
+ type=common_pb2.APPROVAL_TYPE),
+ setter_ref=common_pb2.UserRef(
+ user_id=setter.user_id,
+ display_name=testing_helpers.ObscuredEmail(
+ setter.email)),
+ phase_ref=issue_objects_pb2.PhaseRef(),
+ approver_refs=[common_pb2.UserRef(
+ user_id=appr1.user_id,
+ display_name=testing_helpers.ObscuredEmail(appr1.email),
+ is_derived=False)])],
+ )]))
+
+ def testListProjectTemplates_NoProjectName(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ request = projects_pb2.ListProjectTemplatesRequest()
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.projects_svcr.ListProjectTemplates, mc, request)
+
+ def testListProjectTemplates_NoSuchProject(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ request = projects_pb2.ListProjectTemplatesRequest(project_name='ghost')
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ self.CallWrapped(self.projects_svcr.ListProjectTemplates, mc, request)
+
+ def testListProjectTemplates_PermissionDenied(self):
+ self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='nonmember@example.com')
+ request = projects_pb2.GetConfigRequest(project_name='proj')
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.projects_svcr.ListProjectTemplates, mc, request)
+
+ def testGetPresentationConfig_Normal(self):
+ """Test getting project summary, thumbnail url, custom issue entry, etc."""
+ config = tracker_pb2.ProjectIssueConfig(project_id=789)
+ self.project.summary = 'project summary'
+ config.custom_issue_entry_url = 'issue entry url'
+ config.member_default_query = 'default query'
+ config.default_col_spec = 'ID Summary'
+ config.default_sort_spec = 'Priority Status'
+ config.default_x_attr = 'Priority'
+ config.default_y_attr = 'Status'
+ self.project.revision_url_format = 'revision url format'
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ request = projects_pb2.GetPresentationConfigRequest(project_name='proj')
+ response = self.CallWrapped(
+ self.projects_svcr.GetPresentationConfig, mc, request)
+
+ self.assertEqual('project summary', response.project_summary)
+ self.assertEqual('issue entry url', response.custom_issue_entry_url)
+ self.assertEqual('default query', response.default_query)
+ self.assertEqual('ID Summary', response.default_col_spec)
+ self.assertEqual('Priority Status', response.default_sort_spec)
+ self.assertEqual('Priority', response.default_x_attr)
+ self.assertEqual('Status', response.default_y_attr)
+ self.assertEqual('revision url format', response.revision_url_format)
+
+ def testGetPresentationConfig_SavedQueriesAllowed(self):
+ """Only project members or higher can see project saved queries."""
+ self.services.features.UpdateCannedQueries(self.cnxn, 789, [
+ tracker_pb2.SavedQuery(query_id=101, name='test', query='owner:me'),
+ tracker_pb2.SavedQuery(query_id=202, name='hello', query='world')
+ ])
+
+ # User 333 is a contributor.
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user_333@example.com')
+
+ request = projects_pb2.GetPresentationConfigRequest(project_name='proj')
+ response = self.CallWrapped(self.projects_svcr.GetPresentationConfig, mc,
+ request)
+
+ self.assertEqual(2, len(response.saved_queries))
+
+ self.assertEqual(101, response.saved_queries[0].query_id)
+ self.assertEqual('test', response.saved_queries[0].name)
+ self.assertEqual('owner:me', response.saved_queries[0].query)
+
+ self.assertEqual(202, response.saved_queries[1].query_id)
+ self.assertEqual('hello', response.saved_queries[1].name)
+ self.assertEqual('world', response.saved_queries[1].query)
+
+ def testGetPresentationConfig_SavedQueriesDenied(self):
+ """Only project members or higher can see project saved queries."""
+ self.services.features.UpdateCannedQueries(self.cnxn, 789, [
+ tracker_pb2.SavedQuery(query_id=101, name='test', query='owner:me'),
+ tracker_pb2.SavedQuery(query_id=202, name='hello', query='world')
+ ])
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='nonmember@example.com')
+
+ request = projects_pb2.GetPresentationConfigRequest(project_name='proj')
+ response = self.CallWrapped(self.projects_svcr.GetPresentationConfig, mc,
+ request)
+
+ self.assertEqual(0, len(response.saved_queries))
+
+ def testGetCustomPermissions_Normal(self):
+ self.project.extra_perms = [
+ project_pb2.Project.ExtraPerms(
+ member_id=111,
+ perms=['FooPerm', 'BarPerm'])]
+
+ request = projects_pb2.GetConfigRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.org')
+ response = self.CallWrapped(
+ self.projects_svcr.GetCustomPermissions, mc, request)
+ self.assertEqual(['BarPerm', 'FooPerm'], response.permissions)
+
+ def testGetCustomPermissions_PermissionsAreDedupped(self):
+ self.project.extra_perms = [
+ project_pb2.Project.ExtraPerms(
+ member_id=111,
+ perms=['FooPerm', 'FooPerm']),
+ project_pb2.Project.ExtraPerms(
+ member_id=222,
+ perms=['FooPerm'])]
+
+ request = projects_pb2.GetConfigRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.org')
+ response = self.CallWrapped(
+ self.projects_svcr.GetCustomPermissions, mc, request)
+ self.assertEqual(['FooPerm'], response.permissions)
+
+ def testGetCustomPermissions_PermissionsAreSorted(self):
+ self.project.extra_perms = [
+ project_pb2.Project.ExtraPerms(
+ member_id=111,
+ perms=['FooPerm', 'BarPerm']),
+ project_pb2.Project.ExtraPerms(
+ member_id=222,
+ perms=['BazPerm'])]
+
+ request = projects_pb2.GetConfigRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.org')
+ response = self.CallWrapped(
+ self.projects_svcr.GetCustomPermissions, mc, request)
+ self.assertEqual(['BarPerm', 'BazPerm', 'FooPerm'], response.permissions)
+
+ def testGetCustomPermissions_IgnoreStandardPermissions(self):
+ self.project.extra_perms = [
+ project_pb2.Project.ExtraPerms(
+ member_id=111,
+ perms=permissions.STANDARD_PERMISSIONS + ['FooPerm'])]
+
+ request = projects_pb2.GetConfigRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.org')
+ response = self.CallWrapped(
+ self.projects_svcr.GetCustomPermissions, mc, request)
+ self.assertEqual(['FooPerm'], response.permissions)
+
+ def testGetCustomPermissions_NoCustomPermissions(self):
+ self.project.extra_perms = []
+ request = projects_pb2.GetConfigRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='foo@example.org')
+ response = self.CallWrapped(
+ self.projects_svcr.GetCustomPermissions, mc, request)
+ self.assertEqual([], response.permissions)
+
+ def assertVisibleMembers(self, expected_user_ids, expected_group_ids,
+ requester=None):
+ request = projects_pb2.GetVisibleMembersRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=requester)
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.GetVisibleMembers, mc, request)
+ self.assertEqual(
+ expected_user_ids,
+ [user_ref.user_id for user_ref in response.user_refs])
+ # Assert that we get the full email address.
+ self.assertEqual(
+ [self.services.user.LookupUserEmail(self.cnxn, user_id)
+ for user_id in expected_user_ids],
+ [user_ref.display_name for user_ref in response.user_refs])
+ self.assertEqual(
+ expected_group_ids,
+ [group_ref.user_id for group_ref in response.group_refs])
+ # Assert that we get the full email address.
+ self.assertEqual(
+ [self.services.user.LookupUserEmail(self.cnxn, user_id)
+ for user_id in expected_group_ids],
+ [group_ref.display_name for group_ref in response.group_refs])
+ return response
+
+ def testGetVisibleMembers_Normal(self):
+ # Not logged in - Test users have their email addresses obscured to
+ # non-project members by default.
+ self.assertVisibleMembers([], [])
+ # Logged in as non project member
+ self.assertVisibleMembers([], [], requester='foo@example.com')
+ # Logged in as owner
+ self.assertVisibleMembers([111, 222, 333], [],
+ requester='owner@example.com')
+ # Logged in as committer
+ self.assertVisibleMembers([111, 222, 333], [],
+ requester='user_222@example.com')
+ # Logged in as contributor
+ self.assertVisibleMembers([111, 222, 333], [],
+ requester='user_333@example.com')
+
+ def testGetVisibleMembers_OnlyOwnersSeeContributors(self):
+ self.project.only_owners_see_contributors = True
+ # Not logged in
+ with self.assertRaises(permissions.PermissionException):
+ self.assertVisibleMembers([111, 222], [])
+ # Logged in with a non-member
+ with self.assertRaises(permissions.PermissionException):
+ self.assertVisibleMembers([111, 222], [], requester='foo@example.com')
+ # Logged in as owner
+ self.assertVisibleMembers([111, 222, 333], [],
+ requester='owner@example.com')
+ # Logged in as committer
+ self.assertVisibleMembers([111, 222, 333], [],
+ requester='user_222@example.com')
+ # Logged in as contributor
+ with self.assertRaises(permissions.PermissionException):
+ self.assertVisibleMembers(
+ [111, 222], [], requester='user_333@example.com')
+
+ def testGetVisibleMembers_MemberIsGroup(self):
+ self.project.contributor_ids.extend([999])
+ self.assertVisibleMembers([999, 111, 222, 333, 444], [999],
+ requester='owner@example.com')
+
+ def testGetVisibleMembers_AcExclusion(self):
+ self.services.project.ac_exclusion_ids[self.project.project_id] = [333]
+ self.assertVisibleMembers([111, 222], [], requester='owner@example.com')
+
+ def testGetVisibleMembers_NoExpand(self):
+ self.services.project.no_expand_ids[self.project.project_id] = [999]
+ self.project.contributor_ids.extend([999])
+ self.assertVisibleMembers([999, 111, 222, 333], [999],
+ requester='owner@example.com')
+
+ def testGetVisibleMembers_ObscuredEmails(self):
+ # Unobscure the owner's email. Non-project members can see.
+ self.services.user.UpdateUserSettings(
+ self.cnxn, 111, self.owner, obscure_email=False)
+
+ # Not logged in
+ self.assertVisibleMembers([111], [])
+ # Logged in as not a project member
+ self.assertVisibleMembers([111], [], requester='foo@example.com')
+ # Logged in as owner
+ self.assertVisibleMembers(
+ [111, 222, 333], [], requester='owner@example.com')
+ # Logged in as committer
+ self.assertVisibleMembers(
+ [111, 222, 333], [], requester='user_222@example.com')
+ # Logged in as contributor
+ self.assertVisibleMembers(
+ [111, 222, 333], [], requester='user_333@example.com')
+
+ def testListStatuses(self):
+ request = projects_pb2.ListStatusesRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListStatuses, mc, request)
+ self.assertFalse(response.restrict_to_known)
+ self.assertEqual(
+ [('New', True),
+ ('Accepted', True),
+ ('Started', True),
+ ('Fixed', False),
+ ('Verified', False),
+ ('Invalid', False),
+ ('Duplicate', False),
+ ('WontFix', False),
+ ('Done', False)],
+ [(status_def.status, status_def.means_open)
+ for status_def in response.status_defs])
+ self.assertEqual(
+ [('Duplicate', False)],
+ [(status_def.status, status_def.means_open)
+ for status_def in response.statuses_offer_merge])
+
+ def testListComponents(self):
+ self.services.config.CreateComponentDef(
+ self.cnxn, self.project.project_id, 'Foo', 'Foo Component', True, [],
+ [], True, 111, [])
+ self.services.config.CreateComponentDef(
+ self.cnxn, self.project.project_id, 'Bar', 'Bar Component', False, [],
+ [], True, 111, [])
+ self.services.config.CreateComponentDef(
+ self.cnxn, self.project.project_id, 'Bar>Baz', 'Baz Component',
+ False, [], [], True, 111, [])
+
+ request = projects_pb2.ListComponentsRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListComponents, mc, request)
+
+ self.assertEqual(
+ [project_objects_pb2.ComponentDef(
+ path='Foo',
+ docstring='Foo Component',
+ deprecated=True),
+ project_objects_pb2.ComponentDef(
+ path='Bar',
+ docstring='Bar Component',
+ deprecated=False),
+ project_objects_pb2.ComponentDef(
+ path='Bar>Baz',
+ docstring='Baz Component',
+ deprecated=False)],
+ list(response.component_defs))
+
+ def testListComponents_IncludeAdminInfo(self):
+ self.services.config.CreateComponentDef(
+ self.cnxn, self.project.project_id, 'Foo', 'Foo Component', True, [],
+ [], 1234567, 111, [])
+ self.services.config.CreateComponentDef(
+ self.cnxn, self.project.project_id, 'Bar', 'Bar Component', False, [],
+ [], 1234568, 111, [])
+ self.services.config.CreateComponentDef(
+ self.cnxn, self.project.project_id, 'Bar>Baz', 'Baz Component',
+ False, [], [], 1234569, 111, [])
+ creator_ref = common_pb2.UserRef(
+ user_id=111,
+ display_name='owner@example.com')
+
+ request = projects_pb2.ListComponentsRequest(
+ project_name='proj', include_admin_info=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListComponents, mc, request)
+
+ self.assertEqual(
+ [project_objects_pb2.ComponentDef(
+ path='Foo',
+ docstring='Foo Component',
+ deprecated=True,
+ created=1234567,
+ creator_ref=creator_ref),
+ project_objects_pb2.ComponentDef(
+ path='Bar',
+ docstring='Bar Component',
+ deprecated=False,
+ created=1234568,
+ creator_ref=creator_ref),
+ project_objects_pb2.ComponentDef(
+ path='Bar>Baz',
+ docstring='Baz Component',
+ deprecated=False,
+ created=1234569,
+ creator_ref=creator_ref),
+ ],
+ list(response.component_defs))
+
+ def AddField(self, name, **kwargs):
+ if kwargs.get('needs_perm'):
+ kwargs['needs_member'] = True
+ kwargs.setdefault('cnxn', self.cnxn)
+ kwargs.setdefault('project_id', self.project.project_id)
+ kwargs.setdefault('field_name', name)
+ kwargs.setdefault('field_type_str', 'USER_TYPE')
+ for arg in ('applic_type', 'applic_pred', 'is_required', 'is_niche',
+ 'is_multivalued', 'min_value', 'max_value', 'regex',
+ 'needs_member', 'needs_perm', 'grants_perm', 'notify_on',
+ 'date_action_str', 'docstring'):
+ kwargs.setdefault(arg, None)
+ for arg in ('admin_ids', 'editor_ids'):
+ kwargs.setdefault(arg, [])
+
+ self.services.config.CreateFieldDef(**kwargs)
+
+ def testListFields_Normal(self):
+ self.AddField('Foo Field', needs_perm=permissions.EDIT_ISSUE)
+
+ request = projects_pb2.ListFieldsRequest(
+ project_name='proj', include_user_choices=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(1, len(response.field_defs))
+ field = response.field_defs[0]
+ self.assertEqual('Foo Field', field.field_ref.field_name)
+ self.assertEqual(
+ [111, 222],
+ sorted([user_ref.user_id for user_ref in field.user_choices]))
+ self.assertEqual(
+ ['owner@example.com', 'user_222@example.com'],
+ sorted([user_ref.display_name for user_ref in field.user_choices]))
+
+ def testListFields_DontIncludeUserChoices(self):
+ self.AddField('Foo Field', needs_perm=permissions.EDIT_ISSUE)
+
+ request = projects_pb2.ListFieldsRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(1, len(response.field_defs))
+ field = response.field_defs[0]
+ self.assertEqual(0, len(field.user_choices))
+
+ def testListFields_IncludeAdminInfo(self):
+ self.AddField('Foo Field', needs_perm=permissions.EDIT_ISSUE, is_niche=True,
+ applic_type='Foo Applic Type')
+
+ request = projects_pb2.ListFieldsRequest(
+ project_name='proj', include_admin_info=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(1, len(response.field_defs))
+ field = response.field_defs[0]
+ self.assertEqual('Foo Field', field.field_ref.field_name)
+ self.assertEqual(True, field.is_niche)
+ self.assertEqual('Foo Applic Type', field.applicable_type)
+
+ def testListFields_EnumFieldChoices(self):
+ self.AddField('Type', field_type_str='ENUM_TYPE')
+
+ request = projects_pb2.ListFieldsRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(1, len(response.field_defs))
+ field = response.field_defs[0]
+ self.assertEqual('Type', field.field_ref.field_name)
+ self.assertEqual(
+ ['Defect', 'Enhancement', 'Task', 'Other'],
+ [label.label for label in field.enum_choices])
+
+ def testListFields_CustomPermission(self):
+ self.AddField('Foo Field', needs_perm='FooPerm')
+ self.project.extra_perms = [
+ project_pb2.Project.ExtraPerms(
+ member_id=111,
+ perms=['UnrelatedPerm']),
+ project_pb2.Project.ExtraPerms(
+ member_id=222,
+ perms=['FooPerm'])]
+
+ request = projects_pb2.ListFieldsRequest(
+ project_name='proj', include_user_choices=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(1, len(response.field_defs))
+ field = response.field_defs[0]
+ self.assertEqual('Foo Field', field.field_ref.field_name)
+ self.assertEqual(
+ [222],
+ sorted([user_ref.user_id for user_ref in field.user_choices]))
+ self.assertEqual(
+ ['user_222@example.com'],
+ sorted([user_ref.display_name for user_ref in field.user_choices]))
+
+ def testListFields_IndirectPermission(self):
+ """Test that the permissions of effective ids are also considered."""
+ self.AddField('Foo Field', needs_perm='FooPerm')
+ self.project.contributor_ids.extend([999])
+ self.project.extra_perms = [
+ project_pb2.Project.ExtraPerms(
+ member_id=999,
+ perms=['FooPerm', 'BarPerm'])]
+
+ request = projects_pb2.ListFieldsRequest(
+ project_name='proj', include_user_choices=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(1, len(response.field_defs))
+ field = response.field_defs[0]
+ self.assertEqual('Foo Field', field.field_ref.field_name)
+ # Users 111 and 444 are members of group 999, which has the needed
+ # permission.
+ self.assertEqual(
+ [111, 444, 999],
+ sorted([user_ref.user_id for user_ref in field.user_choices]))
+ self.assertEqual(
+ ['group999@googlegroups.com', 'owner@example.com',
+ 'user_444@example.com'],
+ sorted([user_ref.display_name for user_ref in field.user_choices]))
+
+ def testListFields_TwiceIndirectPermission(self):
+ """Test that only direct memberships are considered."""
+ self.AddField('Foo Field', needs_perm='FooPerm')
+ # User group 777 has members: user_666 and group 999.
+ self.project.contributor_ids.extend([777])
+ self.project.contributor_ids.extend([999])
+ self.project.extra_perms = [
+ project_pb2.Project.ExtraPerms(
+ member_id=777, perms=['FooPerm', 'BarPerm'])
+ ]
+
+ request = projects_pb2.ListFieldsRequest(
+ project_name='proj', include_user_choices=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(1, len(response.field_defs))
+ field = response.field_defs[0]
+ self.assertEqual('Foo Field', field.field_ref.field_name)
+ self.assertEqual(
+ [666, 777, 999],
+ sorted([user_ref.user_id for user_ref in field.user_choices]))
+ self.assertEqual(
+ [
+ 'group777@googlegroups.com', 'group999@googlegroups.com',
+ 'user_666@example.com'
+ ], sorted([user_ref.display_name for user_ref in field.user_choices]))
+
+ def testListFields_NoPermissionsNeeded(self):
+ self.AddField('Foo Field')
+
+ request = projects_pb2.ListFieldsRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(1, len(response.field_defs))
+ field = response.field_defs[0]
+ self.assertEqual('Foo Field', field.field_ref.field_name)
+
+ def testListFields_MultipleFields(self):
+ self.AddField('Bar Field', needs_perm=permissions.VIEW)
+ self.AddField('Foo Field', needs_perm=permissions.EDIT_ISSUE)
+
+ request = projects_pb2.ListFieldsRequest(
+ project_name='proj', include_user_choices=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(2, len(response.field_defs))
+ field_defs = sorted(
+ response.field_defs, key=lambda field: field.field_ref.field_name)
+
+ self.assertEqual(
+ ['Bar Field', 'Foo Field'],
+ [field.field_ref.field_name for field in field_defs])
+ self.assertEqual(
+ [[111, 222, 333],
+ [111, 222]],
+ [sorted(user_ref.user_id for user_ref in field.user_choices)
+ for field in field_defs])
+ self.assertEqual(
+ [['owner@example.com', 'user_222@example.com', 'user_333@example.com'],
+ ['owner@example.com', 'user_222@example.com']],
+ [sorted(user_ref.display_name for user_ref in field.user_choices)
+ for field in field_defs])
+
+ def testListFields_NoFields(self):
+ request = projects_pb2.ListFieldsRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.ListFields, mc, request)
+
+ self.assertEqual(0, len(response.field_defs))
+
+ def testGetLabelOptions_Normal(self):
+ request = projects_pb2.GetLabelOptionsRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.GetLabelOptions, mc, request)
+
+ expected_label_names = [
+ label[0] for label in tracker_constants.DEFAULT_WELL_KNOWN_LABELS]
+ expected_label_names += [
+ 'Restrict-View-EditIssue', 'Restrict-AddIssueComment-EditIssue',
+ 'Restrict-View-CoreTeam']
+ self.assertEqual(
+ sorted(expected_label_names),
+ sorted(label.label for label in response.label_options))
+
+ def testGetLabelOptions_CustomPermissions(self):
+ self.project.extra_perms = [
+ project_pb2.Project.ExtraPerms(
+ member_id=222,
+ perms=['FooPerm', 'BarPerm'])]
+
+ request = projects_pb2.GetLabelOptionsRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.GetLabelOptions, mc, request)
+
+ expected_label_names = [
+ label[0] for label in tracker_constants.DEFAULT_WELL_KNOWN_LABELS]
+ expected_label_names += [
+ 'Restrict-View-EditIssue', 'Restrict-AddIssueComment-EditIssue']
+ expected_label_names += [
+ 'Restrict-%s-%s' % (std_perm, custom_perm)
+ for std_perm in permissions.STANDARD_ISSUE_PERMISSIONS
+ for custom_perm in ('BarPerm', 'FooPerm')]
+
+ self.assertEqual(
+ sorted(expected_label_names),
+ sorted(label.label for label in response.label_options))
+
+ def testGetLabelOptions_FieldMasksLabel(self):
+ self.AddField('Type', field_type_str='ENUM_TYPE')
+
+ request = projects_pb2.GetLabelOptionsRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.GetLabelOptions, mc, request)
+
+ expected_label_names = [
+ label[0] for label in tracker_constants.DEFAULT_WELL_KNOWN_LABELS
+ if not label[0].startswith('Type-')
+ ]
+ expected_label_names += [
+ 'Restrict-View-EditIssue', 'Restrict-AddIssueComment-EditIssue',
+ 'Restrict-View-CoreTeam']
+ self.assertEqual(
+ sorted(expected_label_names),
+ sorted(label.label for label in response.label_options))
+
+ def CallGetStarCount(self):
+ request = projects_pb2.GetProjectStarCountRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.projects_svcr.GetProjectStarCount, mc, request)
+ return response.star_count
+
+ def CallStar(self, requester='owner@example.com', starred=True):
+ request = projects_pb2.StarProjectRequest(
+ project_name='proj', starred=starred)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=requester)
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.StarProject, mc, request)
+ return response.star_count
+
+ def testStarCount_Normal(self):
+ self.assertEqual(0, self.CallGetStarCount())
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallGetStarCount())
+
+ def testStarCount_StarTwiceSameUser(self):
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallGetStarCount())
+
+ def testStarCount_StarTwiceDifferentUser(self):
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(2, self.CallStar(requester='user_222@example.com'))
+ self.assertEqual(2, self.CallGetStarCount())
+
+ def testStarCount_RemoveStarTwiceSameUser(self):
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallGetStarCount())
+
+ self.assertEqual(0, self.CallStar(starred=False))
+ self.assertEqual(0, self.CallStar(starred=False))
+ self.assertEqual(0, self.CallGetStarCount())
+
+ def testStarCount_RemoveStarTwiceDifferentUser(self):
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(2, self.CallStar(requester='user_222@example.com'))
+ self.assertEqual(2, self.CallGetStarCount())
+
+ self.assertEqual(1, self.CallStar(starred=False))
+ self.assertEqual(
+ 0, self.CallStar(requester='user_222@example.com', starred=False))
+ self.assertEqual(0, self.CallGetStarCount())
+
+ def testCheckProjectName_OK(self):
+ """We can check a project name."""
+ request = projects_pb2.CheckProjectNameRequest(project_name='foo')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.CheckProjectName, mc, request)
+
+ self.assertEqual('', response.error)
+
+ def testCheckProjectName_InvalidProjectName(self):
+ """We reject an invalid project name."""
+ request = projects_pb2.CheckProjectNameRequest(project_name='Foo')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.CheckProjectName, mc, request)
+
+ self.assertNotEqual('', response.error)
+
+ def testCheckProjectName_NotAllowed(self):
+ """Users that can't create a project shouldn't get any information."""
+ request = projects_pb2.CheckProjectNameRequest(project_name='Foo')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.projects_svcr.CheckProjectName, mc, request)
+
+ def testCheckProjectName_ProjectAlreadyExists(self):
+ """There is already a project with that name."""
+ request = projects_pb2.CheckProjectNameRequest(project_name='proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.CheckProjectName, mc, request)
+
+ self.assertNotEqual('', response.error)
+
+ def testCheckComponentName_OK(self):
+ request = projects_pb2.CheckComponentNameRequest(
+ project_name='proj',
+ component_name='Component')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.CheckComponentName, mc, request)
+
+ self.assertEqual('', response.error)
+
+ def testCheckComponentName_ParentComponentOK(self):
+ self.services.config.CreateComponentDef(
+ self.cnxn, self.project.project_id, 'Component', 'Docstring',
+ False, [], [], 0, 111, [])
+ request = projects_pb2.CheckComponentNameRequest(
+ project_name='proj',
+ parent_path='Component',
+ component_name='Path')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.CheckComponentName, mc, request)
+
+ self.assertEqual('', response.error)
+
+ def testCheckComponentName_InvalidComponentName(self):
+ request = projects_pb2.CheckComponentNameRequest(
+ project_name='proj',
+ component_name='Component-')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.CheckComponentName, mc, request)
+
+ self.assertNotEqual('', response.error)
+
+ def testCheckComponentName_ComponentAlreadyExists(self):
+ self.services.config.CreateComponentDef(
+ self.cnxn, self.project.project_id, 'Component', 'Docstring',
+ False, [], [], 0, 111, [])
+ request = projects_pb2.CheckComponentNameRequest(
+ project_name='proj',
+ component_name='Component')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.projects_svcr.CheckComponentName, mc, request)
+
+ self.assertNotEqual('', response.error)
+
+ def testCheckComponentName_NotAllowedToViewProject(self):
+ self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+ request = projects_pb2.CheckComponentNameRequest(
+ project_name='proj',
+ parent_path='Component',
+ component_name='Path')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user_444@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.projects_svcr.CheckComponentName, mc, request)
+
+ def testCheckComponentName_ParentComponentDoesntExist(self):
+ request = projects_pb2.CheckComponentNameRequest(
+ project_name='proj',
+ parent_path='Component',
+ component_name='Path')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(exceptions.NoSuchComponentException):
+ self.CallWrapped(self.projects_svcr.CheckComponentName, mc, request)
+
+ def testCheckFieldName_OK(self):
+ request = projects_pb2.CheckFieldNameRequest(
+ project_name='proj',
+ field_name='Foo')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
+ self.assertEqual('', response.error)
+
+ def testCheckFieldName_InvalidFieldName(self):
+ request = projects_pb2.CheckFieldNameRequest(
+ project_name='proj',
+ field_name='**Foo**')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
+ self.assertNotEqual('', response.error)
+
+ def testCheckFieldName_InvalidFieldName_ApproverSuffix(self):
+ request = projects_pb2.CheckFieldNameRequest(
+ project_name='proj',
+ field_name='Foo-aPprOver')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
+ self.assertNotEqual('', response.error)
+
+ def testCheckFieldName_FieldAlreadyExists(self):
+ self.AddField('Foo')
+ request = projects_pb2.CheckFieldNameRequest(
+ project_name='proj',
+ field_name='Foo')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
+ self.assertNotEqual('', response.error)
+
+ def testCheckFieldName_FieldIsPrefixOfAnother(self):
+ self.AddField('Foo-Bar')
+ request = projects_pb2.CheckFieldNameRequest(
+ project_name='proj',
+ field_name='Foo')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
+ self.assertNotEqual('', response.error)
+
+ def testCheckFieldName_AnotherFieldIsPrefix(self):
+ self.AddField('Foo')
+ request = projects_pb2.CheckFieldNameRequest(
+ project_name='proj',
+ field_name='Foo-Bar')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='admin@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
+ self.assertNotEqual('', response.error)
+
+ def testCheckFieldName_NotAllowedToViewProject(self):
+ self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+ request = projects_pb2.CheckFieldNameRequest(
+ project_name='proj',
+ field_name='Foo')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user_444@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.projects_svcr.CheckFieldName, mc, request)
diff --git a/api/test/resource_name_converters_test.py b/api/test/resource_name_converters_test.py
new file mode 100644
index 0000000..e9ca437
--- /dev/null
+++ b/api/test/resource_name_converters_test.py
@@ -0,0 +1,773 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for converting between resource names and external ids."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from mock import Mock, patch
+import unittest
+import re
+
+from api import resource_name_converters as rnc
+from framework import exceptions
+from testing import fake
+from services import service_manager
+from proto import tracker_pb2
+
+class ResourceNameConverterTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ issue=fake.IssueService(),
+ project=fake.ProjectService(),
+ user=fake.UserService(),
+ features=fake.FeaturesService(),
+ template=fake.TemplateService(),
+ config=fake.ConfigService())
+ self.cnxn = fake.MonorailConnection()
+ self.PAST_TIME = 12345
+ self.project_1 = self.services.project.TestAddProject(
+ 'proj', project_id=789)
+ self.project_2 = self.services.project.TestAddProject(
+ 'goose', project_id=788)
+ self.dne_project_id = 1999
+
+ self.issue_1 = fake.MakeTestIssue(
+ self.project_1.project_id, 1, 'sum', 'New', 111,
+ project_name=self.project_1.project_name)
+ self.issue_2 = fake.MakeTestIssue(
+ self.project_2.project_id, 2, 'sum', 'New', 111,
+ project_name=self.project_2.project_name)
+ self.services.issue.TestAddIssue(self.issue_1)
+ self.services.issue.TestAddIssue(self.issue_2)
+
+ self.user_1 = self.services.user.TestAddUser('user_111@example.com', 111)
+ self.user_2 = self.services.user.TestAddUser('user_222@example.com', 222)
+ self.user_3 = self.services.user.TestAddUser('user_333@example.com', 333)
+
+ hotlist_items = [
+ (self.issue_1.issue_id, 9, self.user_2.user_id, self.PAST_TIME, 'note'),
+ (self.issue_2.issue_id, 1, self.user_1.user_id, self.PAST_TIME, 'note')]
+ self.hotlist_1 = self.services.features.TestAddHotlist(
+ 'HotlistName', owner_ids=[], editor_ids=[],
+ hotlist_item_fields=hotlist_items)
+
+ self.template_1 = self.services.template.TestAddIssueTemplateDef(
+ 1, self.project_1.project_id, 'template_1_name')
+ self.template_2 = self.services.template.TestAddIssueTemplateDef(
+ 2, self.project_2.project_id, 'template_2_name')
+ self.dne_template_id = 3
+
+ self.field_def_1_name = 'test_field'
+ self.field_def_1 = self.services.config.CreateFieldDef(
+ self.cnxn, self.project_1.project_id, self.field_def_1_name, 'STR_TYPE',
+ None, None, None, None, None, None, None, None, None, None, None, None,
+ None, None, [], [])
+ self.approval_def_1_name = 'approval_field_1'
+ self.approval_def_1_id = self.services.config.CreateFieldDef(
+ self.cnxn, self.project_1.project_id, self.approval_def_1_name,
+ 'APPROVAL_TYPE', None, None, None, None, None, None, None, None, None,
+ None, None, None, None, None, [], [])
+ self.component_def_1_path = 'Foo'
+ self.component_def_1_id = self.services.config.CreateComponentDef(
+ self.cnxn, self.project_1.project_id, self.component_def_1_path, '',
+ False, [], [], None, 111, [])
+ self.component_def_2_path = 'Foo>Bar>Hey123_I-am-valid'
+ self.component_def_2_id = self.services.config.CreateComponentDef(
+ self.cnxn, self.project_1.project_id, self.component_def_2_path, '',
+ False, [], [], None, 111, [])
+ self.component_def_3_path = 'Fizz'
+ self.component_def_3_id = self.services.config.CreateComponentDef(
+ self.cnxn, self.project_2.project_id, self.component_def_3_path, '',
+ False, [], [], None, 111, [])
+ self.dne_component_def_id = 999
+ self.dne_field_def_id = 999999
+ self.psq_1 = tracker_pb2.SavedQuery(
+ query_id=2, name='psq1 name', base_query_id=1, query='foo=bar')
+ self.psq_2 = tracker_pb2.SavedQuery(
+ query_id=3, name='psq2 name', base_query_id=1, query='fizz=buzz')
+ self.dne_psq_id = 987
+ self.services.features.UpdateCannedQueries(
+ self.cnxn, self.project_1.project_id, [self.psq_1, self.psq_2])
+
+ def testGetResourceNameMatch(self):
+ """We can get a resource name match."""
+ regex = re.compile(r'name\/(?P<group_name>[a-z]+)$')
+ match = rnc._GetResourceNameMatch('name/honque', regex)
+ self.assertEqual(match.group('group_name'), 'honque')
+
+ def testGetResouceNameMatch_InvalidName(self):
+ """An exception is raised if there is not match."""
+ regex = re.compile(r'name\/(?P<group_name>[a-z]+)$')
+ with self.assertRaises(exceptions.InputException):
+ rnc._GetResourceNameMatch('honque/honque', regex)
+
+ def testIngestApprovalDefName(self):
+ approval_id = rnc.IngestApprovalDefName(
+ self.cnxn, 'projects/proj/approvalDefs/123', self.services)
+ self.assertEqual(approval_id, 123)
+
+ def testIngestApprovalDefName_InvalidName(self):
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestApprovalDefName(
+ self.cnxn, 'projects/proj/approvalDefs/123d', self.services)
+
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.IngestApprovalDefName(
+ self.cnxn, 'projects/garbage/approvalDefs/123', self.services)
+
+ def testIngestFieldDefName(self):
+ """We can get a FieldDef's resource name match."""
+ self.assertEqual(
+ rnc.IngestFieldDefName(
+ self.cnxn, 'projects/proj/fieldDefs/123', self.services),
+ (789, 123))
+
+ def testIngestFieldDefName_InvalidName(self):
+ """An exception is raised if the FieldDef's resource name is invalid"""
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestFieldDefName(
+ self.cnxn, 'projects/proj/fieldDefs/7Dog', self.services)
+
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestFieldDefName(
+ self.cnxn, 'garbage/proj/fieldDefs/123', self.services)
+
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.IngestFieldDefName(
+ self.cnxn, 'projects/garbage/fieldDefs/123', self.services)
+
+ def testIngestHotlistName(self):
+ """We can get a Hotlist's resource name match."""
+ self.assertEqual(rnc.IngestHotlistName('hotlists/78909'), 78909)
+
+ def testIngestHotlistName_InvalidName(self):
+ """An exception is raised if the Hotlist's resource name is invalid"""
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestHotlistName('hotlists/789honk789')
+
+ def testIngestHotlistItemNames(self):
+ """We can get Issue IDs from HotlistItems resource names."""
+ names = [
+ 'hotlists/78909/items/proj.1',
+ 'hotlists/78909/items/goose.2']
+ self.assertEqual(
+ rnc.IngestHotlistItemNames(self.cnxn, names, self.services),
+ [self.issue_1.issue_id, self.issue_2.issue_id])
+
+ def testIngestHotlistItemNames_ProjectNotFound(self):
+ """Exception is raised if a project is not found."""
+ names = [
+ 'hotlists/78909/items/proj.1',
+ 'hotlists/78909/items/chicken.2']
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.IngestHotlistItemNames(self.cnxn, names, self.services)
+
+ def testIngestHotlistItemNames_MultipleProjectsNotFound(self):
+ """Aggregated exceptions raised if projects are not found."""
+ names = [
+ 'hotlists/78909/items/proj.1', 'hotlists/78909/items/chicken.2',
+ 'hotlists/78909/items/cow.3'
+ ]
+ with self.assertRaisesRegexp(exceptions.NoSuchProjectException,
+ 'Project chicken not found.\n' +
+ 'Project cow not found.'):
+ rnc.IngestHotlistItemNames(self.cnxn, names, self.services)
+
+ def testIngestHotlistItems_IssueNotFound(self):
+ """Exception is raised if an Issue is not found."""
+ names = [
+ 'hotlists/78909/items/proj.1',
+ 'hotlists/78909/items/goose.5']
+ with self.assertRaisesRegexp(exceptions.NoSuchIssueException, '%r' % names):
+ rnc.IngestHotlistItemNames(self.cnxn, names, self.services)
+
+ def testConvertHotlistName(self):
+ """We can get a Hotlist's resource name."""
+ self.assertEqual(rnc.ConvertHotlistName(10), 'hotlists/10')
+
+ def testConvertHotlistItemNames(self):
+ """We can get Hotlist items' resource names."""
+ expected_dict = {
+ self.hotlist_1.items[0].issue_id: 'hotlists/7739/items/proj.1',
+ self.hotlist_1.items[1].issue_id: 'hotlists/7739/items/goose.2',
+ }
+ self.assertEqual(
+ rnc.ConvertHotlistItemNames(
+ self.cnxn, self.hotlist_1.hotlist_id, expected_dict.keys(),
+ self.services), expected_dict)
+
+ def testIngestApprovalValueName(self):
+ project_id, issue_id, approval_def_id = rnc.IngestApprovalValueName(
+ self.cnxn, 'projects/proj/issues/1/approvalValues/404', self.services)
+ self.assertEqual(project_id, self.project_1.project_id)
+ self.assertEqual(issue_id, self.issue_1.issue_id)
+ self.assertEqual(404, approval_def_id) # We don't verify it exists.
+
+ def testIngestApprovalValueName_ProjectDoesNotExist(self):
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.IngestApprovalValueName(
+ self.cnxn, 'projects/noproj/issues/1/approvalValues/1', self.services)
+
+ def testIngestApprovalValueName_IssueDoesNotExist(self):
+ with self.assertRaisesRegexp(exceptions.NoSuchIssueException,
+ 'projects/proj/issues/404'):
+ rnc.IngestApprovalValueName(
+ self.cnxn, 'projects/proj/issues/404/approvalValues/1', self.services)
+
+ def testIngestApprovalValueName_InvalidStart(self):
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestApprovalValueName(
+ self.cnxn, 'zprojects/proj/issues/1/approvalValues/1', self.services)
+
+ def testIngestApprovalValueName_InvalidEnd(self):
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestApprovalValueName(
+ self.cnxn, 'projects/proj/issues/1/approvalValues/1z', self.services)
+
+ def testIngestApprovalValueName_InvalidCollection(self):
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestApprovalValueName(
+ self.cnxn, 'projects/proj/issues/1/approvalValue/1', self.services)
+
+ def testIngestIssueName(self):
+ """We can get an Issue global id from its resource name."""
+ self.assertEqual(
+ rnc.IngestIssueName(self.cnxn, 'projects/proj/issues/1', self.services),
+ self.issue_1.issue_id)
+
+ def testIngestIssueName_ProjectDoesNotExist(self):
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.IngestIssueName(self.cnxn, 'projects/noproj/issues/1', self.services)
+
+ def testIngestIssueName_IssueDoesNotExist(self):
+ with self.assertRaises(exceptions.NoSuchIssueException):
+ rnc.IngestIssueName(self.cnxn, 'projects/proj/issues/2', self.services)
+
+ def testIngestIssueName_InvalidLocalId(self):
+ """Issue resource name Local IDs are digits."""
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestIssueName(self.cnxn, 'projects/proj/issues/x', self.services)
+
+ def testIngestIssueName_InvalidProjectId(self):
+ """Project names are more than 1 character."""
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestIssueName(self.cnxn, 'projects/p/issues/1', self.services)
+
+ def testIngestIssueName_InvalidFormat(self):
+ """Issue resource names must begin with the project resource name."""
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestIssueName(self.cnxn, 'issues/1', self.services)
+
+ def testIngestIssueName_Moved(self):
+ """We can get a moved issue."""
+ moved_to_project_id = 987
+ self.services.project.TestAddProject(
+ 'other', project_id=moved_to_project_id)
+ new_issue_id = 1010
+ issue = fake.MakeTestIssue(
+ moved_to_project_id, 200, 'sum', 'New', 111, issue_id=new_issue_id)
+ self.services.issue.TestAddIssue(issue)
+ self.services.issue.TestAddMovedIssueRef(
+ self.project_1.project_id, 404, moved_to_project_id, 200)
+
+ self.assertEqual(
+ rnc.IngestIssueName(
+ self.cnxn, 'projects/proj/issues/404', self.services), new_issue_id)
+
+ def testIngestIssueNames(self):
+ """We can get an Issue global ids from resource names."""
+ self.assertEqual(
+ rnc.IngestIssueNames(
+ self.cnxn, ['projects/proj/issues/1', 'projects/goose/issues/2'],
+ self.services), [self.issue_1.issue_id, self.issue_2.issue_id])
+
+ def testIngestIssueNames_EmptyList(self):
+ """We get an empty list when providing an empty list of issue names."""
+ self.assertEqual(rnc.IngestIssueNames(self.cnxn, [], self.services), [])
+
+ def testIngestIssueNames_WithBadInputs(self):
+ """We aggregate input exceptions."""
+ with self.assertRaisesRegexp(
+ exceptions.InputException,
+ 'Invalid resource name: projects/proj/badformat/1.\n' +
+ 'Invalid resource name: badformat/proj/issues/1.'):
+ rnc.IngestIssueNames(
+ self.cnxn, [
+ 'projects/proj/badformat/1', 'badformat/proj/issues/1',
+ 'projects/proj/issues/1'
+ ], self.services)
+
+ def testIngestIssueNames_OneDoesNotExist(self):
+ """We get an exception if one issue name provided does not exist."""
+ with self.assertRaises(exceptions.NoSuchIssueException):
+ rnc.IngestIssueNames(
+ self.cnxn, ['projects/proj/issues/1', 'projects/proj/issues/2'],
+ self.services)
+
+ def testIngestIssueNames_ManyDoNotExist(self):
+ """We get an exception if one issue name provided does not exist."""
+ dne_issues = ['projects/proj/issues/2', 'projects/proj/issues/3']
+ with self.assertRaisesRegexp(exceptions.NoSuchIssueException,
+ '%r' % dne_issues):
+ rnc.IngestIssueNames(self.cnxn, dne_issues, self.services)
+
+ def testIngestIssueNames_ProjectsNotExist(self):
+ """Aggregated exceptions raised if projects are not found."""
+ with self.assertRaisesRegexp(exceptions.NoSuchProjectException,
+ 'Project chicken not found.\n' +
+ 'Project cow not found.'):
+ rnc.IngestIssueNames(
+ self.cnxn, [
+ 'projects/chicken/issues/2', 'projects/cow/issues/3',
+ 'projects/proj/issues/1'
+ ], self.services)
+
+ def testIngestProjectFromIssue(self):
+ self.assertEqual(rnc.IngestProjectFromIssue('projects/xyz/issues/1'), 'xyz')
+
+ def testIngestProjectFromIssue_InvalidName(self):
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestProjectFromIssue('projects/xyz')
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestProjectFromIssue('garbage')
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestProjectFromIssue('projects/xyz/issues/garbage')
+
+ def testIngestCommentName(self):
+ name = 'projects/proj/issues/1/comments/0'
+ actual = rnc.IngestCommentName(self.cnxn, name, self.services)
+ self.assertEqual(
+ actual, (self.project_1.project_id, self.issue_1.issue_id, 0))
+
+ def testIngestCommentName_InputException(self):
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestCommentName(self.cnxn, 'misspelled name', self.services)
+
+ def testIngestCommentName_NoSuchProject(self):
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.IngestCommentName(
+ self.cnxn, 'projects/doesnotexist/issues/1/comments/0', self.services)
+
+ def testIngestCommentName_NoSuchIssue(self):
+ with self.assertRaisesRegexp(exceptions.NoSuchIssueException,
+ "['projects/proj/issues/404']"):
+ rnc.IngestCommentName(
+ self.cnxn, 'projects/proj/issues/404/comments/0', self.services)
+
+ def testConvertCommentNames(self):
+ """We can create comment names."""
+ expected = {
+ 0: 'projects/proj/issues/1/comments/0',
+ 1: 'projects/proj/issues/1/comments/1'
+ }
+ self.assertEqual(rnc.CreateCommentNames(1, 'proj', [0, 1]), expected)
+
+ def testConvertCommentNames_Empty(self):
+ """Converting an empty list of comments returns an empty dict."""
+ self.assertEqual(rnc.CreateCommentNames(1, 'proj', []), {})
+
+ def testConvertIssueName(self):
+ """We can create an Issue resource name from an issue_id."""
+ self.assertEqual(
+ rnc.ConvertIssueName(self.cnxn, self.issue_1.issue_id, self.services),
+ 'projects/proj/issues/1')
+
+ def testConvertIssueName_NotFound(self):
+ """Exception is raised if the issue is not found."""
+ with self.assertRaises(exceptions.NoSuchIssueException):
+ rnc.ConvertIssueName(self.cnxn, 3279, self.services)
+
+ def testConvertIssueNames(self):
+ """We can create Issue resource names from issue_ids."""
+ self.assertEqual(
+ rnc.ConvertIssueNames(
+ self.cnxn, [self.issue_1.issue_id, 3279], self.services),
+ {self.issue_1.issue_id: 'projects/proj/issues/1'})
+
+ def testConvertApprovalValueNames(self):
+ """We can create ApprovalValue resource names."""
+ self.issue_1.approval_values = [tracker_pb2.ApprovalValue(
+ approval_id=self.approval_def_1_id)]
+
+ expected_name = (
+ 'projects/proj/issues/1/approvalValues/%d' % self.approval_def_1_id)
+ self.assertEqual(
+ {self.approval_def_1_id: expected_name},
+ rnc.ConvertApprovalValueNames(
+ self.cnxn, self.issue_1.issue_id, self.services))
+
+ def testIngestUserName(self):
+ """We can get a User ID from User resource name."""
+ name = 'users/111'
+ self.assertEqual(rnc.IngestUserName(self.cnxn, name, self.services), 111)
+
+ def testIngestUserName_DisplayName(self):
+ """We can get a User ID from User resource name with a display name set."""
+ name = 'users/%s' % self.user_3.email
+ self.assertEqual(rnc.IngestUserName(self.cnxn, name, self.services), 333)
+
+ def testIngestUserName_NoSuchUser(self):
+ """When autocreate=False, we raise an exception if a user is not found."""
+ name = 'users/chicken@test.com'
+ with self.assertRaises(exceptions.NoSuchUserException):
+ rnc.IngestUserName(self.cnxn, name, self.services)
+
+ def testIngestUserName_InvalidEmail(self):
+ """We raise an exception if a given resource name's email is invalid."""
+ name = 'users/chickentest.com'
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestUserName(self.cnxn, name, self.services)
+
+ def testIngestUserName_Autocreate(self):
+ """When autocreate=True create a new User if they don't already exist."""
+ new_email = 'chicken@test.com'
+ name = 'users/%s' % new_email
+ user_id = rnc.IngestUserName(
+ self.cnxn, name, self.services, autocreate=True)
+
+ new_id = self.services.user.LookupUserID(
+ self.cnxn, new_email, autocreate=False)
+ self.assertEqual(new_id, user_id)
+
+ def testIngestUserNames(self):
+ """We can get User IDs from User resource names."""
+ names = ['users/111', 'users/222', 'users/%s' % self.user_3.email]
+ expected_ids = [111, 222, 333]
+ self.assertEqual(
+ rnc.IngestUserNames(self.cnxn, names, self.services), expected_ids)
+
+ def testIngestUserNames_NoSuchUser(self):
+ """When autocreate=False, we raise an exception if a user is not found."""
+ names = [
+ 'users/111', 'users/chicken@test.com',
+ 'users/%s' % self.user_3.email
+ ]
+ with self.assertRaises(exceptions.NoSuchUserException):
+ rnc.IngestUserNames(self.cnxn, names, self.services)
+
+ def testIngestUserNames_InvalidEmail(self):
+ """We raise an exception if a given resource name's email is invalid."""
+ names = [
+ 'users/111', 'users/chickentest.com',
+ 'users/%s' % self.user_3.email
+ ]
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestUserNames(self.cnxn, names, self.services)
+
+ def testIngestUserNames_Autocreate(self):
+ """When autocreate=True we create new Users if they don't already exist."""
+ new_email = 'user_444@example.com'
+ names = [
+ 'users/111',
+ 'users/%s' % new_email,
+ 'users/%s' % self.user_3.email
+ ]
+ ids = rnc.IngestUserNames(self.cnxn, names, self.services, autocreate=True)
+
+ new_id = self.services.user.LookupUserID(
+ self.cnxn, new_email, autocreate=False)
+ expected_ids = [111, new_id, 333]
+ self.assertEqual(expected_ids, ids)
+
+ def testConvertUserName(self):
+ """We can convert a single User ID to resource name."""
+ self.assertEqual(rnc.ConvertUserName(111), 'users/111')
+
+ def testConvertUserNames(self):
+ """We can get User resource names."""
+ expected_dict = {111: 'users/111', 222: 'users/222', 333: 'users/333'}
+ self.assertEqual(rnc.ConvertUserNames(expected_dict.keys()), expected_dict)
+
+ def testConvertUserNames_Empty(self):
+ """We can process an empty Users list."""
+ self.assertEqual(rnc.ConvertUserNames([]), {})
+
+ def testConvertProjectStarName(self):
+ """We can convert a User ID and Project ID to resource name."""
+ name = rnc.ConvertProjectStarName(
+ self.cnxn, 111, self.project_1.project_id, self.services)
+ expected = 'users/111/projectStars/{}'.format(self.project_1.project_name)
+ self.assertEqual(name, expected)
+
+ def testConvertProjectStarName_NoSuchProjectException(self):
+ """Throws an exception when Project ID is invalid."""
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertProjectStarName(self.cnxn, 111, 123455, self.services)
+
+ def testIngestProjectName(self):
+ """We can get project name from Project resource names."""
+ name = 'projects/{}'.format(self.project_1.project_name)
+ expected = self.project_1.project_id
+ self.assertEqual(
+ rnc.IngestProjectName(self.cnxn, name, self.services), expected)
+
+ def testIngestProjectName_InvalidName(self):
+ """An exception is raised if the Hotlist's resource name is invalid"""
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestProjectName(self.cnxn, 'projects/', self.services)
+
+ def testConvertTemplateNames(self):
+ """We can get IssueTemplate resource names."""
+ expected_resource_name = 'projects/{}/templates/{}'.format(
+ self.project_1.project_name, self.template_1.template_id)
+ expected = {self.template_1.template_id: expected_resource_name}
+
+ self.assertEqual(
+ rnc.ConvertTemplateNames(
+ self.cnxn, self.project_1.project_id, [self.template_1.template_id],
+ self.services), expected)
+
+ def testConvertTemplateNames_NoSuchProjectException(self):
+ """We get an exception if project with id does not exist."""
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertTemplateNames(
+ self.cnxn, self.dne_project_id, [self.template_1.template_id],
+ self.services)
+
+ def testConvertTemplateNames_NonExistentTemplate(self):
+ """We only return templates that exist."""
+ self.assertEqual(
+ rnc.ConvertTemplateNames(
+ self.cnxn, self.project_1.project_id, [self.dne_template_id],
+ self.services), {})
+
+ def testConvertTemplateNames_TemplateInProject(self):
+ """We only return templates in the project."""
+ expected_resource_name = 'projects/{}/templates/{}'.format(
+ self.project_2.project_name, self.template_2.template_id)
+ expected = {self.template_2.template_id: expected_resource_name}
+
+ self.assertEqual(
+ rnc.ConvertTemplateNames(
+ self.cnxn, self.project_2.project_id,
+ [self.template_1.template_id, self.template_2.template_id],
+ self.services), expected)
+
+ def testIngestTemplateName(self):
+ name = 'projects/{}/templates/{}'.format(
+ self.project_1.project_name, self.template_1.template_id)
+ actual = rnc.IngestTemplateName(self.cnxn, name, self.services)
+ expected = (self.template_1.template_id, self.project_1.project_id)
+ self.assertEqual(actual, expected)
+
+ def testIngestTemplateName_DoesNotExist(self):
+ """We will ingest templates that don't exist."""
+ name = 'projects/{}/templates/{}'.format(
+ self.project_1.project_name, self.dne_template_id)
+ actual = rnc.IngestTemplateName(self.cnxn, name, self.services)
+ expected = (self.dne_template_id, self.project_1.project_id)
+ self.assertEqual(actual, expected)
+
+ def testIngestTemplateName_InvalidName(self):
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestTemplateName(
+ self.cnxn, 'projects/asdf/misspelled_template/123', self.services)
+
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.IngestTemplateName(
+ self.cnxn, 'projects/asdf/templates/123', self.services)
+
+ def testConvertStatusDefNames(self):
+ """We can get Status resource name."""
+ expected_resource_name = 'projects/{}/statusDefs/{}'.format(
+ self.project_1.project_name, self.issue_1.status)
+
+ actual = rnc.ConvertStatusDefNames(
+ self.cnxn, [self.issue_1.status], self.project_1.project_id,
+ self.services)
+ self.assertEqual(actual[self.issue_1.status], expected_resource_name)
+
+ def testConvertStatusDefNames_NoSuchProjectException(self):
+ """We can get an exception if project with id does not exist."""
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertStatusDefNames(
+ self.cnxn, [self.issue_1.status], self.dne_project_id, self.services)
+
+ def testConvertLabelDefNames(self):
+ """We can get Label resource names."""
+ expected_label = 'some label'
+ expected_resource_name = 'projects/{}/labelDefs/{}'.format(
+ self.project_1.project_name, expected_label)
+
+ self.assertEqual(
+ rnc.ConvertLabelDefNames(
+ self.cnxn, [expected_label], self.project_1.project_id,
+ self.services), {expected_label: expected_resource_name})
+
+ def testConvertLabelDefNames_NoSuchProjectException(self):
+ """We can get an exception if project with id does not exist."""
+ some_label = 'some label'
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertLabelDefNames(
+ self.cnxn, [some_label], self.dne_project_id, self.services)
+
+ def testConvertComponentDefNames(self):
+ """We can get Component resource names."""
+ expected_id = 123456
+ expected_resource_name = 'projects/{}/componentDefs/{}'.format(
+ self.project_1.project_name, expected_id)
+
+ self.assertEqual(
+ rnc.ConvertComponentDefNames(
+ self.cnxn, [expected_id], self.project_1.project_id, self.services),
+ {expected_id: expected_resource_name})
+
+ def testConvertComponentDefNames_NoSuchProjectException(self):
+ """We can get an exception if project with id does not exist."""
+ component_id = 123456
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertComponentDefNames(
+ self.cnxn, [component_id], self.dne_project_id, self.services)
+
+ def testIngestComponentDefNames(self):
+ names = [
+ 'projects/proj/componentDefs/%d' % self.component_def_1_id,
+ 'projects/proj/componentDefs/%s' % self.component_def_2_path
+ ]
+ actual = rnc.IngestComponentDefNames(self.cnxn, names, self.services)
+ self.assertEqual(actual, [
+ (self.project_1.project_id, self.component_def_1_id),
+ (self.project_1.project_id, self.component_def_2_id)])
+
+ def testIngestComponentDefNames_NoSuchProjectException(self):
+ names = ['projects/xyz/componentDefs/%d' % self.component_def_1_id]
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.IngestComponentDefNames(self.cnxn, names, self.services)
+
+ names = ['projects/xyz/componentDefs/1', 'projects/zyz/componentDefs/1']
+ expected_error = 'Project not found: xyz.\nProject not found: zyz.'
+ with self.assertRaisesRegexp(exceptions.NoSuchProjectException,
+ expected_error):
+ rnc.IngestComponentDefNames(self.cnxn, names, self.services)
+
+ def testIngestComponentDefNames_NoSuchComponentException(self):
+ names = ['projects/proj/componentDefs/%d' % self.dne_component_def_id]
+ with self.assertRaises(exceptions.NoSuchComponentException):
+ rnc.IngestComponentDefNames(self.cnxn, names, self.services)
+
+ names = [
+ 'projects/proj/componentDefs/999', 'projects/proj/componentDefs/cow'
+ ]
+ expected_error = 'Component not found: 999.\nComponent not found: \'cow\'.'
+ with self.assertRaisesRegexp(exceptions.NoSuchComponentException,
+ expected_error):
+ rnc.IngestComponentDefNames(self.cnxn, names, self.services)
+
+ def testIngestComponentDefNames_InvalidNames(self):
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestComponentDefNames(
+ self.cnxn, ['projects/proj/componentDefs/not.path.or.id'],
+ self.services)
+
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestComponentDefNames(
+ self.cnxn, ['projects/proj/componentDefs/Foo>'], self.services)
+
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestComponentDefNames(
+ self.cnxn, ['projects/proj/componentDefs/>Bar'], self.services)
+
+ with self.assertRaises(exceptions.InputException):
+ rnc.IngestComponentDefNames(
+ self.cnxn, ['projects/proj/componentDefs/Foo>123Bar'], self.services)
+
+ def testIngestComponentDefNames_CrossProject(self):
+ names = [
+ 'projects/proj/componentDefs/%d' % self.component_def_1_id,
+ 'projects/goose/componentDefs/%s' % self.component_def_3_path
+ ]
+ actual = rnc.IngestComponentDefNames(self.cnxn, names, self.services)
+ self.assertEqual(actual, [
+ (self.project_1.project_id, self.component_def_1_id),
+ (self.project_2.project_id, self.component_def_3_id)])
+
+ def testConvertFieldDefNames(self):
+ """Returns resource names for fields that exist and ignores others."""
+ expected_key = self.field_def_1
+ expected_value = 'projects/{}/fieldDefs/{}'.format(
+ self.project_1.project_name, self.field_def_1)
+
+ field_ids = [self.field_def_1, self.dne_field_def_id]
+ self.assertEqual(
+ rnc.ConvertFieldDefNames(
+ self.cnxn, field_ids, self.project_1.project_id, self.services),
+ {expected_key: expected_value})
+
+ def testConvertFieldDefNames_NoSuchProjectException(self):
+ field_ids = [self.field_def_1, self.dne_field_def_id]
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertFieldDefNames(
+ self.cnxn, field_ids, self.dne_project_id, self.services)
+
+ def testConvertApprovalDefNames(self):
+ outcome = rnc.ConvertApprovalDefNames(
+ self.cnxn, [self.approval_def_1_id], self.project_1.project_id,
+ self.services)
+
+ expected_key = self.approval_def_1_id
+ expected_value = 'projects/{}/approvalDefs/{}'.format(
+ self.project_1.project_name, self.approval_def_1_id)
+ self.assertEqual(outcome, {expected_key: expected_value})
+
+ def testConvertApprovalDefNames_NoSuchProjectException(self):
+ approval_ids = [self.approval_def_1_id]
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertApprovalDefNames(
+ self.cnxn, approval_ids, self.dne_project_id, self.services)
+
+ def testConvertProjectName(self):
+ self.assertEqual(
+ rnc.ConvertProjectName(
+ self.cnxn, self.project_1.project_id, self.services),
+ 'projects/{}'.format(self.project_1.project_name))
+
+ def testConvertProjectName_NoSuchProjectException(self):
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertProjectName(self.cnxn, self.dne_project_id, self.services)
+
+ def testConvertProjectConfigName(self):
+ self.assertEqual(
+ rnc.ConvertProjectConfigName(
+ self.cnxn, self.project_1.project_id, self.services),
+ 'projects/{}/config'.format(self.project_1.project_name))
+
+ def testConvertProjectConfigName_NoSuchProjectException(self):
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertProjectConfigName(
+ self.cnxn, self.dne_project_id, self.services)
+
+ def testConvertProjectMemberName(self):
+ self.assertEqual(
+ rnc.ConvertProjectMemberName(
+ self.cnxn, self.project_1.project_id, 111, self.services),
+ 'projects/{}/members/{}'.format(self.project_1.project_name, 111))
+
+ def testConvertProjectMemberName_NoSuchProjectException(self):
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertProjectMemberName(
+ self.cnxn, self.dne_project_id, 111, self.services)
+
+ def testConvertProjectSavedQueryNames(self):
+ query_ids = [self.psq_1.query_id, self.psq_2.query_id, self.dne_psq_id]
+ outcome = rnc.ConvertProjectSavedQueryNames(
+ self.cnxn, query_ids, self.project_1.project_id, self.services)
+
+ expected_value_1 = 'projects/{}/savedQueries/{}'.format(
+ self.project_1.project_name, self.psq_1.name)
+ expected_value_2 = 'projects/{}/savedQueries/{}'.format(
+ self.project_1.project_name, self.psq_2.name)
+ self.assertEqual(
+ outcome, {
+ self.psq_1.query_id: expected_value_1,
+ self.psq_2.query_id: expected_value_2
+ })
+
+ def testConvertProjectSavedQueryNames_NoSuchProjectException(self):
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ rnc.ConvertProjectSavedQueryNames(
+ self.cnxn, [self.psq_1.query_id], self.dne_project_id, self.services)
diff --git a/api/test/sitewide_servicer_test.py b/api/test/sitewide_servicer_test.py
new file mode 100644
index 0000000..3259fbb
--- /dev/null
+++ b/api/test/sitewide_servicer_test.py
@@ -0,0 +1,143 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for the sitewide servicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import time
+import unittest
+
+import mock
+from components.prpc import codes
+from components.prpc import context
+from components.prpc import server
+
+import settings
+from api import sitewide_servicer
+from api.api_proto import common_pb2
+from api.api_proto import sitewide_pb2
+from framework import monorailcontext
+from framework import xsrf
+from services import service_manager
+from testing import fake
+
+
+class SitewideServicerTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ usergroup=fake.UserGroupService(),
+ user=fake.UserService())
+ self.user_1 = self.services.user.TestAddUser('owner@example.com', 111)
+ self.sitewide_svcr = sitewide_servicer.SitewideServicer(
+ self.services, make_rate_limiter=False)
+
+ def CallWrapped(self, wrapped_handler, *args, **kwargs):
+ return wrapped_handler.wrapped(self.sitewide_svcr, *args, **kwargs)
+
+ @mock.patch('services.secrets_svc.GetXSRFKey')
+ @mock.patch('time.time')
+ def testRefreshToken(self, mockTime, mockGetXSRFKey):
+ """We can refresh an expired token."""
+ mockGetXSRFKey.side_effect = lambda: 'fakeXSRFKey'
+ # The token is at the brink of being too old
+ mockTime.side_effect = lambda: 1 + xsrf.REFRESH_TOKEN_TIMEOUT_SEC
+
+ token_path = 'token_path'
+ token = xsrf.GenerateToken(111, token_path, 1)
+
+ request = sitewide_pb2.RefreshTokenRequest(
+ token=token, token_path=token_path)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.sitewide_svcr.RefreshToken, mc, request)
+
+ self.assertEqual(
+ sitewide_pb2.RefreshTokenResponse(
+ token='QSaKMyXhY752g7n8a34HyTo4NjQwMDE=',
+ token_expires_sec=870901),
+ response)
+
+ @mock.patch('services.secrets_svc.GetXSRFKey')
+ @mock.patch('time.time')
+ def testRefreshToken_InvalidToken(self, mockTime, mockGetXSRFKey):
+ """We reject attempts to refresh an invalid token."""
+ mockGetXSRFKey.side_effect = ['fakeXSRFKey']
+ mockTime.side_effect = [123]
+
+ token_path = 'token_path'
+ token = 'invalidToken'
+
+ request = sitewide_pb2.RefreshTokenRequest(
+ token=token, token_path=token_path)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ with self.assertRaises(xsrf.TokenIncorrect):
+ self.CallWrapped(self.sitewide_svcr.RefreshToken, mc, request)
+
+ @mock.patch('services.secrets_svc.GetXSRFKey')
+ @mock.patch('time.time')
+ def testRefreshToken_TokenTooOld(self, mockTime, mockGetXSRFKey):
+ """We reject attempts to refresh a token that's too old."""
+ mockGetXSRFKey.side_effect = lambda: 'fakeXSRFKey'
+ mockTime.side_effect = lambda: 2 + xsrf.REFRESH_TOKEN_TIMEOUT_SEC
+
+ token_path = 'token_path'
+ token = xsrf.GenerateToken(111, token_path, 1)
+
+ request = sitewide_pb2.RefreshTokenRequest(
+ token=token, token_path=token_path)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ with self.assertRaises(xsrf.TokenIncorrect):
+ self.CallWrapped(self.sitewide_svcr.RefreshToken, mc, request)
+
+ def testGetServerStatus_Normal(self):
+ request = sitewide_pb2.GetServerStatusRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.sitewide_svcr.GetServerStatus, mc, request)
+
+ self.assertEqual(
+ sitewide_pb2.GetServerStatusResponse(),
+ response)
+
+ @mock.patch('settings.banner_message', 'Message')
+ def testGetServerStatus_BannerMessage(self):
+ request = sitewide_pb2.GetServerStatusRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.sitewide_svcr.GetServerStatus, mc, request)
+
+ self.assertEqual(
+ sitewide_pb2.GetServerStatusResponse(banner_message='Message'),
+ response)
+
+ @mock.patch('settings.banner_time', (2019, 6, 13, 18, 30))
+ def testGetServerStatus_BannerTime(self):
+ request = sitewide_pb2.GetServerStatusRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.sitewide_svcr.GetServerStatus, mc, request)
+
+ self.assertEqual(
+ sitewide_pb2.GetServerStatusResponse(banner_time=1560450600),
+ response)
+
+ @mock.patch('settings.read_only', True)
+ def testGetServerStatus_ReadOnly(self):
+ request = sitewide_pb2.GetServerStatusRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.sitewide_svcr.GetServerStatus, mc, request)
+
+ self.assertEqual(
+ sitewide_pb2.GetServerStatusResponse(read_only=True),
+ response)
diff --git a/api/test/users_servicer_test.py b/api/test/users_servicer_test.py
new file mode 100644
index 0000000..aa25d18
--- /dev/null
+++ b/api/test/users_servicer_test.py
@@ -0,0 +1,606 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for the users servicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+import mox
+from components.prpc import codes
+from components.prpc import context
+from components.prpc import server
+
+from api import users_servicer
+from api.api_proto import common_pb2
+from api.api_proto import users_pb2
+from api.api_proto import user_objects_pb2
+from framework import authdata
+from framework import exceptions
+from framework import monorailcontext
+from framework import permissions
+from proto import project_pb2
+from proto import tracker_pb2
+from proto import user_pb2
+from testing import fake
+from services import service_manager
+
+
+class UsersServicerTest(unittest.TestCase):
+
+ def setUp(self):
+ self.mox = mox.Mox()
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ user=fake.UserService(),
+ user_star=fake.UserStarService(),
+ usergroup=fake.UserGroupService(),
+ project=fake.ProjectService(),
+ project_star=fake.ProjectStarService(),
+ features=fake.FeaturesService())
+ self.project = self.services.project.TestAddProject('proj', project_id=987)
+ self.user = self.services.user.TestAddUser('owner@example.com', 111)
+ self.user_2 = self.services.user.TestAddUser('test2@example.com', 222)
+ self.group1_id = self.services.usergroup.CreateGroup(
+ self.cnxn, self.services, 'group1@test.com', 'anyone')
+ self.group2_id = self.services.usergroup.CreateGroup(
+ self.cnxn, self.services, 'group2@test.com', 'anyone')
+ self.services.usergroup.UpdateMembers(
+ self.cnxn, self.group1_id, [111], 'member')
+ self.services.usergroup.UpdateMembers(
+ self.cnxn, self.group2_id, [222, 111], 'owner')
+ self.users_svcr = users_servicer.UsersServicer(
+ self.services, make_rate_limiter=False)
+ self.prpc_context = context.ServicerContext()
+ self.prpc_context.set_code(codes.StatusCode.OK)
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def CallWrapped(self, wrapped_handler, *args, **kwargs):
+ return wrapped_handler.wrapped(self.users_svcr, *args, **kwargs)
+
+ def testGetMemberships(self):
+ request = users_pb2.GetMembershipsRequest(
+ user_ref=common_pb2.UserRef(
+ display_name='owner@example.com', user_id=111))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ response = self.CallWrapped(self.users_svcr.GetMemberships, mc, request)
+ expected_group_refs = [
+ common_pb2.UserRef(
+ display_name='group1@test.com', user_id=self.group1_id),
+ common_pb2.UserRef(
+ display_name='group2@test.com', user_id=self.group2_id)
+ ]
+
+ self.assertItemsEqual(expected_group_refs, response.group_refs)
+
+ def testGetMemberships_NonExistentUser(self):
+ request = users_pb2.GetMembershipsRequest(
+ user_ref=common_pb2.UserRef(
+ display_name='ghost@example.com', user_id=888)
+ )
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='')
+
+ with self.assertRaises(exceptions.NoSuchUserException):
+ self.CallWrapped(self.users_svcr.GetMemberships, mc, request)
+
+ def testGetUser(self):
+ """We can get a user by email address."""
+ user_ref = common_pb2.UserRef(display_name='test2@example.com')
+ request = users_pb2.GetUserRequest(user_ref=user_ref)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.users_svcr.GetUser, mc, request)
+ self.assertEqual(response.display_name, 'test2@example.com')
+ self.assertEqual(response.user_id, 222)
+ self.assertFalse(response.is_site_admin)
+
+ self.user_2.is_site_admin = True
+ response = self.CallWrapped(
+ self.users_svcr.GetUser, mc, request)
+ self.assertTrue(response.is_site_admin)
+
+ def testListReferencedUsers(self):
+ """We can get all valid users by email addresses."""
+ request = users_pb2.ListReferencedUsersRequest(
+ # we ignore emails that are empty or belong to non-existent users.
+ user_refs=[
+ common_pb2.UserRef(display_name='test2@example.com'),
+ common_pb2.UserRef(display_name='ghost@example.com'),
+ common_pb2.UserRef(display_name=''),
+ common_pb2.UserRef()])
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.users_svcr.ListReferencedUsers, mc, request)
+ self.assertEqual(len(response.users), 1)
+ self.assertEqual(response.users[0].user_id, 222)
+
+ def testListReferencedUsers_Deprecated(self):
+ """We can get all valid users by email addresses."""
+ request = users_pb2.ListReferencedUsersRequest(
+ # we ignore emails that are empty or belong to non-existent users.
+ emails=[
+ 'test2@example.com',
+ 'ghost@example.com',
+ ''])
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.users_svcr.ListReferencedUsers, mc, request)
+ self.assertEqual(len(response.users), 1)
+ self.assertEqual(response.users[0].user_id, 222)
+
+ def CallGetStarCount(self):
+ request = users_pb2.GetUserStarCountRequest(
+ user_ref=common_pb2.UserRef(user_id=222))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(
+ self.users_svcr.GetUserStarCount, mc, request)
+ return response.star_count
+
+ def CallStar(self, requester='owner@example.com', starred=True):
+ request = users_pb2.StarUserRequest(
+ user_ref=common_pb2.UserRef(user_id=222), starred=starred)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=requester)
+ response = self.CallWrapped(
+ self.users_svcr.StarUser, mc, request)
+ return response.star_count
+
+ def testStarCount_Normal(self):
+ self.assertEqual(0, self.CallGetStarCount())
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallGetStarCount())
+
+ def testStarCount_StarTwiceSameUser(self):
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallGetStarCount())
+
+ def testStarCount_StarTwiceDifferentUser(self):
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(2, self.CallStar(requester='test2@example.com'))
+ self.assertEqual(2, self.CallGetStarCount())
+
+ def testStarCount_RemoveStarTwiceSameUser(self):
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(1, self.CallGetStarCount())
+
+ self.assertEqual(0, self.CallStar(starred=False))
+ self.assertEqual(0, self.CallStar(starred=False))
+ self.assertEqual(0, self.CallGetStarCount())
+
+ def testStarCount_RemoveStarTwiceDifferentUser(self):
+ self.assertEqual(1, self.CallStar())
+ self.assertEqual(2, self.CallStar(requester='test2@example.com'))
+ self.assertEqual(2, self.CallGetStarCount())
+
+ self.assertEqual(1, self.CallStar(starred=False))
+ self.assertEqual(
+ 0, self.CallStar(requester='test2@example.com', starred=False))
+ self.assertEqual(0, self.CallGetStarCount())
+
+ def testSetExpandPermsPreference_KeepOpen(self):
+ request = users_pb2.SetExpandPermsPreferenceRequest(expand_perms=True)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.users_svcr.SetExpandPermsPreference, mc, request)
+
+ user = self.services.user.GetUser(self.cnxn, self.user.user_id)
+ self.assertTrue(user.keep_people_perms_open)
+
+ def testSetExpandPermsPreference_DontKeepOpen(self):
+ request = users_pb2.SetExpandPermsPreferenceRequest(expand_perms=False)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.users_svcr.SetExpandPermsPreference, mc, request)
+
+ user = self.services.user.GetUser(self.cnxn, self.user.user_id)
+ self.assertFalse(user.keep_people_perms_open)
+
+ def testGetUserSavedQueries_Anon(self):
+ """Anon has empty saved queries."""
+ request = users_pb2.GetSavedQueriesRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=None)
+ response = self.CallWrapped(self.users_svcr.GetSavedQueries, mc, request)
+
+ self.assertEqual(0, len(response.saved_queries))
+
+ def testGetUserSavedQueries_Mine(self):
+ """See your own queries."""
+ self.services.features.UpdateUserSavedQueries(self.cnxn, 111, [
+ tracker_pb2.SavedQuery(query_id=101, name='test', query='owner:me'),
+ tracker_pb2.SavedQuery(query_id=202, name='hello', query='world',
+ executes_in_project_ids=[987])
+ ])
+ request = users_pb2.GetUserPrefsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.users_svcr.GetSavedQueries, mc, request)
+
+ self.assertEqual(2, len(response.saved_queries))
+
+ self.assertEqual('test', response.saved_queries[0].name)
+ self.assertEqual('owner:me', response.saved_queries[0].query)
+ self.assertEqual('hello', response.saved_queries[1].name)
+ self.assertEqual('world', response.saved_queries[1].query)
+ self.assertEqual(['proj'], response.saved_queries[1].project_names)
+
+
+ def testGetUserSavedQueries_Other_Allowed(self):
+ """See other people's queries if you're an admin."""
+ self.services.features.UpdateUserSavedQueries(self.cnxn, 111, [
+ tracker_pb2.SavedQuery(query_id=101, name='test', query='owner:me'),
+ tracker_pb2.SavedQuery(query_id=202, name='hello', query='world',
+ executes_in_project_ids=[987])
+ ])
+ self.user_2.is_site_admin = True
+
+ request = users_pb2.GetSavedQueriesRequest()
+ request.user_ref.display_name = 'owner@example.com'
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='test2@example.com')
+
+ response = self.CallWrapped(self.users_svcr.GetSavedQueries, mc, request)
+
+ self.assertEqual(2, len(response.saved_queries))
+
+ self.assertEqual('test', response.saved_queries[0].name)
+ self.assertEqual('owner:me', response.saved_queries[0].query)
+ self.assertEqual('hello', response.saved_queries[1].name)
+ self.assertEqual('world', response.saved_queries[1].query)
+ self.assertEqual(['proj'], response.saved_queries[1].project_names)
+
+ def testGetUserSavedQueries_Other_Denied(self):
+ """Can't see other people's queries unless you're an admin."""
+ self.services.features.UpdateUserSavedQueries(self.cnxn, 111, [
+ tracker_pb2.SavedQuery(query_id=101, name='test', query='owner:me'),
+ tracker_pb2.SavedQuery(query_id=202, name='hello', query='world',
+ executes_in_project_ids=[987])
+ ])
+
+ request = users_pb2.GetSavedQueriesRequest()
+ request.user_ref.display_name = 'owner@example.com'
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='test2@example.com')
+
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.users_svcr.GetSavedQueries, mc, request)
+
+ def testGetUserPrefs_Anon(self):
+ """Anon always has empty prefs."""
+ request = users_pb2.GetUserPrefsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=None)
+ response = self.CallWrapped(self.users_svcr.GetUserPrefs, mc, request)
+
+ self.assertEqual(0, len(response.prefs))
+
+ def testGetUserPrefs_Mine_Empty(self):
+ """User who never set any pref gets empty prefs."""
+ request = users_pb2.GetUserPrefsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.users_svcr.GetUserPrefs, mc, request)
+
+ self.assertEqual(0, len(response.prefs))
+
+ def testGetUserPrefs_Mine_Some(self):
+ """User who set a pref gets it back."""
+ self.services.user.SetUserPrefs(
+ self.cnxn, 111,
+ [user_pb2.UserPrefValue(name='code_font', value='true')])
+ request = users_pb2.GetUserPrefsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ response = self.CallWrapped(self.users_svcr.GetUserPrefs, mc, request)
+
+ self.assertEqual(1, len(response.prefs))
+ self.assertEqual('code_font', response.prefs[0].name)
+ self.assertEqual('true', response.prefs[0].value)
+
+ def testGetUserPrefs_Other_Allowed(self):
+ """A site admin can read another user's prefs."""
+ self.services.user.SetUserPrefs(
+ self.cnxn, 111,
+ [user_pb2.UserPrefValue(name='code_font', value='true')])
+ self.user_2.is_site_admin = True
+
+ request = users_pb2.GetUserPrefsRequest()
+ request.user_ref.display_name = 'owner@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='test2@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(self.users_svcr.GetUserPrefs, mc, request)
+
+ self.assertEqual(1, len(response.prefs))
+ self.assertEqual('code_font', response.prefs[0].name)
+ self.assertEqual('true', response.prefs[0].value)
+
+ def testGetUserPrefs_Other_Denied(self):
+ """A non-admin cannot read another user's prefs."""
+ self.services.user.SetUserPrefs(
+ self.cnxn, 111,
+ [user_pb2.UserPrefValue(name='code_font', value='true')])
+ # user2 is not a site admin.
+
+ request = users_pb2.GetUserPrefsRequest()
+ request.user_ref.display_name = 'owner@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='test2@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.users_svcr.GetUserPrefs, mc, request)
+
+ def testSetUserPrefs_Anon(self):
+ """Anon cannot set prefs."""
+ request = users_pb2.SetUserPrefsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=None)
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.users_svcr.SetUserPrefs, mc, request)
+
+ def testSetUserPrefs_Mine_Empty(self):
+ """Setting zero prefs is a no-op.."""
+ request = users_pb2.SetUserPrefsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.users_svcr.SetUserPrefs, mc, request)
+
+ prefs_after = self.services.user.GetUserPrefs(self.cnxn, 111)
+ self.assertEqual(0, len(prefs_after.prefs))
+
+ def testSetUserPrefs_Mine_Add(self):
+ """User can set a preference for the first time."""
+ request = users_pb2.SetUserPrefsRequest(
+ prefs=[user_objects_pb2.UserPrefValue(name='code_font', value='true')])
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.users_svcr.SetUserPrefs, mc, request)
+
+ prefs_after = self.services.user.GetUserPrefs(self.cnxn, 111)
+ self.assertEqual(1, len(prefs_after.prefs))
+ self.assertEqual('code_font', prefs_after.prefs[0].name)
+ self.assertEqual('true', prefs_after.prefs[0].value)
+
+ def testSetUserPrefs_Mine_Overwrite(self):
+ """User can change the value of a pref."""
+ self.services.user.SetUserPrefs(
+ self.cnxn, 111,
+ [user_pb2.UserPrefValue(name='code_font', value='true')])
+ request = users_pb2.SetUserPrefsRequest(
+ prefs=[user_objects_pb2.UserPrefValue(name='code_font', value='false')])
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+ self.CallWrapped(self.users_svcr.SetUserPrefs, mc, request)
+
+ prefs_after = self.services.user.GetUserPrefs(self.cnxn, 111)
+ self.assertEqual(1, len(prefs_after.prefs))
+ self.assertEqual('code_font', prefs_after.prefs[0].name)
+ self.assertEqual('false', prefs_after.prefs[0].value)
+
+ def testSetUserPrefs_Other_Allowed(self):
+ """A site admin can update another user's prefs."""
+ self.services.user.SetUserPrefs(
+ self.cnxn, 111,
+ [user_pb2.UserPrefValue(name='code_font', value='true')])
+ self.user_2.is_site_admin = True
+
+ request = users_pb2.SetUserPrefsRequest(
+ prefs=[user_objects_pb2.UserPrefValue(name='code_font', value='false')])
+ request.user_ref.display_name = 'owner@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='test2@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ self.CallWrapped(self.users_svcr.SetUserPrefs, mc, request)
+
+ prefs_after = self.services.user.GetUserPrefs(self.cnxn, 111)
+ self.assertEqual(1, len(prefs_after.prefs))
+ self.assertEqual('code_font', prefs_after.prefs[0].name)
+ self.assertEqual('false', prefs_after.prefs[0].value)
+
+ def testSetUserPrefs_Other_Denied(self):
+ """A non-admin cannot set another user's prefs."""
+ self.services.user.SetUserPrefs(
+ self.cnxn, 111,
+ [user_pb2.UserPrefValue(name='code_font', value='true')])
+ # user2 is not a site admin.
+
+ request = users_pb2.SetUserPrefsRequest(
+ prefs=[user_objects_pb2.UserPrefValue(name='code_font', value='false')])
+ request.user_ref.display_name = 'owner@example.com'
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='test2@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.users_svcr.SetUserPrefs, mc, request)
+
+ # Regardless of any exception, the preferences remain unchanged.
+ prefs_after = self.services.user.GetUserPrefs(self.cnxn, 111)
+ self.assertEqual(1, len(prefs_after.prefs))
+ self.assertEqual('code_font', prefs_after.prefs[0].name)
+ self.assertEqual('true', prefs_after.prefs[0].value)
+
+ def testInviteLinkedParent_NotFound(self):
+ """Reject attempt to invite a user that does not exist."""
+ self.services.user.TestAddUser('user@google.com', 333)
+ request = users_pb2.InviteLinkedParentRequest(
+ email='who@chromium.org') # Does not exist.
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='who@google.com')
+ with self.assertRaises(exceptions.NoSuchUserException):
+ self.CallWrapped(self.users_svcr.InviteLinkedParent, mc, request)
+
+ def testInviteLinkedParent_Normal(self):
+ """We can invite accounts to link when all criteria are met."""
+ self.services.user.TestAddUser('user@google.com', 333)
+ self.services.user.TestAddUser('user@chromium.org', 444)
+ request = users_pb2.InviteLinkedParentRequest(
+ email='user@google.com')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@chromium.org')
+ self.CallWrapped(self.users_svcr.InviteLinkedParent, mc, request)
+
+ (invite_as_parent, invite_as_child
+ ) = self.services.user.GetPendingLinkedInvites(self.cnxn, 333)
+ self.assertEqual([444], invite_as_parent)
+ self.assertEqual([], invite_as_child)
+ (invite_as_parent, invite_as_child
+ ) = self.services.user.GetPendingLinkedInvites(self.cnxn, 444)
+ self.assertEqual([], invite_as_parent)
+ self.assertEqual([333], invite_as_child)
+
+ def testAcceptLinkedChild_NotFound(self):
+ """Reject attempt to link a user that does not exist."""
+ self.services.user.TestAddUser('user@google.com', 333)
+ request = users_pb2.AcceptLinkedChildRequest(
+ email='who@chromium.org') # Does not exist.
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='who@google.com')
+ with self.assertRaises(exceptions.NoSuchUserException):
+ self.CallWrapped(self.users_svcr.AcceptLinkedChild, mc, request)
+
+ def testAcceptLinkedChild_NoInvite(self):
+ """Reject attempt to link accounts when there was no invite."""
+ self.services.user.TestAddUser('user@google.com', 333)
+ self.services.user.TestAddUser('user@chromium.org', 444)
+ request = users_pb2.AcceptLinkedChildRequest(
+ email='user@chromium.org')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@google.com')
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.users_svcr.AcceptLinkedChild, mc, request)
+
+ def testAcceptLinkedChild_Normal(self):
+ """We can linke accounts when all criteria are met."""
+ parent = self.services.user.TestAddUser('user@google.com', 333)
+ child = self.services.user.TestAddUser('user@chromium.org', 444)
+ self.services.user.InviteLinkedParent(
+ self.cnxn, parent.user_id, child.user_id)
+ request = users_pb2.AcceptLinkedChildRequest(
+ email='user@chromium.org')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='user@google.com')
+ self.CallWrapped(self.users_svcr.AcceptLinkedChild, mc, request)
+
+ self.assertEqual(parent.user_id, child.linked_parent_id)
+ self.assertIn(child.user_id, parent.linked_child_ids)
+
+ def testUnlinkAccounts_NotFound(self):
+ """Reject attempt to unlink a user that does not exist or unspecified."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ request = users_pb2.UnlinkAccountsRequest(
+ parent=common_pb2.UserRef(display_name='who@chromium.org'),
+ child=common_pb2.UserRef(display_name='owner@example.com'))
+ with self.assertRaises(exceptions.NoSuchUserException):
+ self.CallWrapped(self.users_svcr.UnlinkAccounts, mc, request)
+
+ request = users_pb2.UnlinkAccountsRequest(
+ parent=common_pb2.UserRef(display_name='owner@example.com'),
+ child=common_pb2.UserRef(display_name='who@google.com'))
+ with self.assertRaises(exceptions.NoSuchUserException):
+ self.CallWrapped(self.users_svcr.UnlinkAccounts, mc, request)
+
+ request = users_pb2.UnlinkAccountsRequest(
+ parent=common_pb2.UserRef(display_name='owner@example.com'))
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.users_svcr.UnlinkAccounts, mc, request)
+
+ request = users_pb2.UnlinkAccountsRequest(
+ child=common_pb2.UserRef(display_name='owner@example.com'))
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.users_svcr.UnlinkAccounts, mc, request)
+
+ def testUnlinkAccounts_Normal(self):
+ """Users can unlink their accounts."""
+ self.services.user.linked_account_rows = [(111, 222)]
+ request = users_pb2.UnlinkAccountsRequest(
+ parent=common_pb2.UserRef(display_name='owner@example.com'),
+ child=common_pb2.UserRef(display_name='test2@example.com'))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='owner@example.com')
+
+ self.CallWrapped(self.users_svcr.UnlinkAccounts, mc, request)
+
+ self.assertEqual([], self.services.user.linked_account_rows)
+
+ def AddUserProjects(self, user_id):
+ project_states = {
+ 'live': project_pb2.ProjectState.LIVE,
+ 'archived': project_pb2.ProjectState.ARCHIVED,
+ 'deletable': project_pb2.ProjectState.DELETABLE}
+
+ for name, state in project_states.items():
+ self.services.project.TestAddProject(
+ 'owner-%s-%s' % (name, user_id), state=state, owner_ids=[user_id])
+ self.services.project.TestAddProject(
+ 'committer-%s-%s' % (name, user_id), state=state,\
+ committer_ids=[user_id])
+ contributor = self.services.project.TestAddProject(
+ 'contributor-%s-%s' % (name, user_id), state=state)
+ contributor.contributor_ids = [user_id]
+
+ members_only = self.services.project.TestAddProject(
+ 'members-only-' + str(user_id), owner_ids=[user_id])
+ members_only.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+
+ def testGetUsersProjects(self):
+ self.user = self.services.user.TestAddUser('test3@example.com', 333)
+ self.services.project_star.SetStar(
+ self.cnxn, self.project.project_id, 222, True)
+ self.project.committer_ids.extend([222])
+
+ self.AddUserProjects(222)
+ self.AddUserProjects(333)
+
+ request = users_pb2.GetUsersProjectsRequest(user_refs=[
+ common_pb2.UserRef(display_name='test2@example.com'),
+ common_pb2.UserRef(display_name='test3@example.com')])
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='test2@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.users_svcr.GetUsersProjects, mc, request)
+
+ self.assertEqual([
+ user_objects_pb2.UserProjects(
+ user_ref=common_pb2.UserRef(display_name='test2@example.com'),
+ owner_of=['members-only-222', 'owner-live-222'],
+ member_of=['committer-live-222', 'proj'],
+ contributor_to=['contributor-live-222'],
+ starred_projects=['proj']),
+ user_objects_pb2.UserProjects(
+ user_ref=common_pb2.UserRef(display_name='test3@example.com'),
+ owner_of=['owner-live-333'],
+ member_of=['committer-live-333'],
+ contributor_to=['contributor-live-333'])],
+ list(response.users_projects))
+
+ def testGetUsersProjects_NoUserRefs(self):
+ request = users_pb2.GetUsersProjectsRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester='test2@example.com')
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.users_svcr.GetUsersProjects, mc, request)
+ self.assertEqual([], list(response.users_projects))
diff --git a/api/test_call b/api/test_call
new file mode 100755
index 0000000..5455241
--- /dev/null
+++ b/api/test_call
@@ -0,0 +1,157 @@
+#!/usr/bin/env python
+"""
+This is a helper script for making pRPC API calls during local development.
+
+Usage examples:
+
+To test an anonymous request to your own local monorail server:
+1. Run 'make serve' in another shell
+2. `./api/test_call monorail.Projects ListComponents
+ '{"project_name": "monorail", "include_admin_info": true}'`
+
+To test a signed in request to your own local monorail server:
+1. Run 'make serve' in another shell
+2. `./api/test_call monorail.Projects ListComponents
+ '{"project_name": "monorail", "include_admin_info": true}'
+ --test_account=test@example.com`
+Note that test account email address must always end in @example.com.
+
+To test an anonymous request to your monorail staging server:
+1. Deploy your staging server version, e.g., 12345-76697e9-tainted-jrobbins.
+2. Visit your staging server in a new incognito window and view source
+ to find the XSRF token for the anonymous user in JS var CS_env['token'].
+3. `./api/test_call monorail.Projects ListComponents
+ '{"project_name": "monorail", "include_admin_info": true}'
+ --host=12345-76697e9-tainted-jrobbins-dot-monorail-staging.appspot.com
+ --xsrf-token='THE_ANON_TOKEN'`
+
+To test a signed-in request to your monorail staging server using
+the client_id for monorail-staging and your own account:
+1. Make sure that you have a role in the monorail-staging project.
+2. Have your account allowlisted by email address.
+3. Download the monorail-staging app credientials via
+ `gcloud --project=monorail-staging auth login`.
+4. `./api/test_call monorail.Projects ListComponents
+ '{"project_name": "monorail", "include_admin_info": true}'
+ --host=12345-76697e9-tainted-jrobbins-dot-monorail-staging.appspot.com
+ --use-app-credentials`
+
+To test a signed-in request to your monorail staging server using
+a service account client secrets file that you download:
+(Note: This is not recommended for prod because downloading secrets
+is a bad practice.)
+1. Create a service account via the Cloud Console for any project.
+ Choose "IAM & Admin" > "Service accounts".
+ Press "+ Create Service Account".
+ Fill in the form and submit it to save a service account .json file
+ to your local disk. Keep this file private.
+2. File an issue on /p/monorail to allowlist your client_id and/or
+ client_email. Or, author a CL yourself to add it to the allowlist.
+3. `./api/test_call monorail.Projects ListComponents
+ '{"project_name": "monorail", "include_admin_info": true}'
+ --host=12345-76697e9-tainted-jrobbins-dot-monorail-staging.appspot.com
+ --service-account=FILENAME_OF_SERVICE_ACCOUNT_JSON_FILE`
+"""
+
+import argparse
+import errno
+import json
+import logging
+import os
+import sys
+
+
+monorail_dir = os.path.dirname(os.path.abspath(__file__ + '/..'))
+third_party_path = os.path.join(monorail_dir, 'third_party')
+if third_party_path not in sys.path:
+ sys.path.insert(0, third_party_path)
+
+import httplib2
+from oauth2client.client import GoogleCredentials
+
+
+URL_BASE = 'http://localhost:8080/prpc/'
+OAUTH_SCOPE = 'https://www.googleapis.com/auth/userinfo.email'
+
+def make_http(args):
+ """Return an httplib2.Http object, with or without oauth."""
+ http = httplib2.Http()
+ credentials = None
+ if args.use_app_credentials:
+ credentials = GoogleCredentials.get_application_default()
+ if args.service_account:
+ credentials = GoogleCredentials.from_stream(args.service_account)
+ logging.debug('Will request as user %r', credentials.service_account_email)
+
+ if credentials:
+ credentials = credentials.create_scoped([OAUTH_SCOPE])
+ logging.debug('Will request as client %r', credentials.client_id)
+ if not args.host:
+ print(('[ERROR] OAuth on localhost will always see user '
+ 'example@example.com, so we do not support that.\n'
+ 'Instead, add --server=YOUR_STAGING_SERVER, '
+ 'or use --test_account=USER@example.com.'))
+ sys.exit(1)
+
+ http = credentials.authorize(http)
+
+ return http
+
+def make_call(service, method, json_body, args):
+ """Call the server and print the response contents."""
+ body = json.loads(json_body)
+
+ url_base = URL_BASE
+ if args.host:
+ url_base = 'https://%s/prpc/' % args.host
+ url = '%s%s/%s' % (url_base, service, method)
+ logging.debug('Request URL: %s', url)
+
+ http = make_http(args)
+ headers = {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ }
+ if args.test_account:
+ headers['x-test-account'] = args.test_account
+ if args.xsrf_token:
+ headers['x-xsrf-token'] = args.xsrf_token
+ body = json.dumps(body)
+
+ logging.debug('Body: %r' % body)
+ try:
+ response, contents = http.request(
+ url, method='POST', body=body, headers=headers)
+ logging.info('Received response: %s', contents)
+ except httplib2.HttpLib2Error as e:
+ if hasattr(e.reason, 'errno') and e.reason.errno == errno.ECONNREFUSED:
+ print('[Error] Could not reach server. Is it running?')
+ else:
+ raise e
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description='Process some integers.')
+ parser.add_argument('service', help='pRPC service name.')
+ parser.add_argument('method', help='pRPC method name.')
+ parser.add_argument('json_body', help='pRPC HTTP body in valid JSON.')
+ parser.add_argument('--test-account',
+ help='Test account to use, in the form of an email.')
+ parser.add_argument('--xsrf-token', help='Custom XSRF token.')
+ parser.add_argument('--host', help='remote server FQDN.')
+ parser.add_argument(
+ '--use-app-credentials',
+ help='Use credentials of a GAE app that you are signed into via gcloud.',
+ action='store_true')
+ parser.add_argument(
+ '--service-account', help='Service account credentials JSON file name.')
+ parser.add_argument('-v', '--verbose', action='store_true')
+ args = parser.parse_args()
+
+ if args.verbose:
+ log_level = logging.DEBUG
+ else:
+ log_level = logging.INFO
+ logging.basicConfig(format='%(levelname)s: %(message)s', level=log_level)
+
+ make_call(args.service, args.method, args.json_body, args)
diff --git a/api/users_servicer.py b/api/users_servicer.py
new file mode 100644
index 0000000..d106868
--- /dev/null
+++ b/api/users_servicer.py
@@ -0,0 +1,217 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from api import converters
+from api import monorail_servicer
+from api import converters
+from api.api_proto import users_pb2
+from api.api_proto import users_prpc_pb2
+from api.api_proto import user_objects_pb2
+from businesslogic import work_env
+from framework import authdata
+from framework import framework_views
+from framework import permissions
+
+class UsersServicer(monorail_servicer.MonorailServicer):
+ """Handle API requests related to User objects.
+
+ Each API request is implemented with a method as defined in the
+ .proto file that does any request-specific validation, uses work_env
+ to safely operate on business objects, and returns a response proto.
+ """
+
+ DESCRIPTION = users_prpc_pb2.UsersServiceDescription
+
+ @monorail_servicer.PRPCMethod
+ def GetUser(self, mc, request):
+ """Return info about the specified user."""
+ with work_env.WorkEnv(mc, self.services) as we:
+ users, linked_user_ids = we.ListReferencedUsers(
+ [request.user_ref.display_name])
+ linked_user_views = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, linked_user_ids)
+
+ with mc.profiler.Phase('converting to response objects'):
+ response_users = converters.ConvertUsers(users, linked_user_views)
+
+ return response_users[0]
+
+ @monorail_servicer.PRPCMethod
+ def ListReferencedUsers(self, mc, request):
+ """Return the list of existing users in a response proto."""
+ emails = request.emails
+ if request.user_refs:
+ emails = [user_ref.display_name for user_ref in request.user_refs]
+ with work_env.WorkEnv(mc, self.services) as we:
+ users, linked_user_ids = we.ListReferencedUsers(emails)
+ linked_user_views = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, linked_user_ids)
+
+ with mc.profiler.Phase('converting to response objects'):
+ response_users = converters.ConvertUsers(users, linked_user_views)
+ response = users_pb2.ListReferencedUsersResponse(users=response_users)
+
+ return response
+
+ @monorail_servicer.PRPCMethod
+ def GetMemberships(self, mc, request):
+ """Return the user groups for the given user visible to the requester."""
+ user_id = converters.IngestUserRef(
+ mc.cnxn, request.user_ref, self.services.user)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ group_ids = we.GetMemberships(user_id)
+
+ with mc.profiler.Phase('converting to response objects'):
+ groups_by_id = framework_views.MakeAllUserViews(
+ mc.cnxn, self.services.user, group_ids)
+ group_refs = converters.ConvertUserRefs(
+ group_ids, [], groups_by_id, True)
+
+ return users_pb2.GetMembershipsResponse(group_refs=group_refs)
+
+ @monorail_servicer.PRPCMethod
+ def GetUserStarCount(self, mc, request):
+ """Return the star count for a given user."""
+ user_id = converters.IngestUserRef(
+ mc.cnxn, request.user_ref, self.services.user)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ star_count = we.GetUserStarCount(user_id)
+
+ result = users_pb2.GetUserStarCountResponse(star_count=star_count)
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def StarUser(self, mc, request):
+ """Star a given user."""
+ user_id = converters.IngestUserRef(
+ mc.cnxn, request.user_ref, self.services.user)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.StarUser(user_id, request.starred)
+ star_count = we.GetUserStarCount(user_id)
+
+ result = users_pb2.StarUserResponse(star_count=star_count)
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def SetExpandPermsPreference(self, mc, request):
+ """Set a users preference on whether to expand perms by default."""
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.UpdateUserSettings(
+ mc.auth.user_pb, keep_people_perms_open=request.expand_perms)
+
+ result = users_pb2.SetExpandPermsPreferenceResponse()
+ return result
+
+ def _SignedInOrSpecifiedUser(self, mc, request):
+ """If request specifies a user, return it. Otherwise signed-in user."""
+ user_id = mc.auth.user_id
+ if request.HasField('user_ref'):
+ user_id = converters.IngestUserRef(
+ mc.cnxn, request.user_ref, self.services.user)
+ return user_id
+
+ @monorail_servicer.PRPCMethod
+ def GetSavedQueries(self, mc, request):
+ """Get a user's saved queries."""
+ user_id = self._SignedInOrSpecifiedUser(mc, request)
+
+ # Only site admins can view other user's saved queries.
+ if user_id != mc.auth.user_id and not mc.auth.user_pb.is_site_admin:
+ raise permissions.PermissionException(
+ 'You are not allowed to view this user\'s saved queries')
+
+ saved_queries = self.services.features.GetSavedQueriesByUserID(
+ mc.cnxn, user_id)
+ return users_pb2.GetSavedQueriesResponse(
+ saved_queries=converters.IngestSavedQueries(mc.cnxn,
+ self.services.project, saved_queries))
+
+ @monorail_servicer.PRPCMethod
+ def GetUserPrefs(self, mc, request):
+ """Get a user's preferences."""
+ with work_env.WorkEnv(mc, self.services) as we:
+ userprefs = we.GetUserPrefs(self._SignedInOrSpecifiedUser(mc, request))
+
+ result = users_pb2.GetUserPrefsResponse(
+ prefs=converters.ConvertPrefValues(userprefs.prefs))
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def SetUserPrefs(self, mc, request):
+ """Add to or set a users preferences."""
+ with work_env.WorkEnv(mc, self.services) as we:
+ pref_values = converters.IngestPrefValues(request.prefs)
+ we.SetUserPrefs(self._SignedInOrSpecifiedUser(mc, request), pref_values)
+
+ result = users_pb2.SetUserPrefsResponse()
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def InviteLinkedParent(self, mc, request):
+ """Create a linked account invite."""
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.InviteLinkedParent(request.email)
+
+ result = users_pb2.InviteLinkedParentResponse()
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def AcceptLinkedChild(self, mc, request):
+ """Link a child account that has invited this account as parent."""
+ child_id = self.services.user.LookupUserID(mc.cnxn, request.email)
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.AcceptLinkedChild(child_id)
+
+ result = users_pb2.AcceptLinkedChildResponse()
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def UnlinkAccounts(self, mc, request):
+ """Unlink a specificed parent and child account."""
+ parent_id, child_id = converters.IngestUserRefs(
+ mc.cnxn, [request.parent, request.child], self.services.user)
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.UnlinkAccounts(parent_id, child_id)
+
+ result = users_pb2.UnlinkAccountsResponse()
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def GetUsersProjects(self, mc, request):
+ user_ids = converters.IngestUserRefs(
+ mc.cnxn, request.user_refs, self.services.user)
+ user_auths = [
+ authdata.AuthData.FromUserID(mc.cnxn, user_id, self.services)
+ for user_id in user_ids]
+
+ result = users_pb2.GetUsersProjectsResponse()
+ with work_env.WorkEnv(mc, self.services) as we:
+ for user_ref, auth in zip(request.user_refs, user_auths):
+ starred = we.ListStarredProjects(auth.user_id)
+ owner, _archived, member, contrib = we.GetUserProjects(
+ auth.effective_ids)
+ user_projects = result.users_projects.add()
+ user_projects.user_ref.CopyFrom(user_ref)
+ user_projects.owner_of.extend(p.project_name for p in owner)
+ user_projects.member_of.extend(p.project_name for p in member)
+ user_projects.contributor_to.extend(p.project_name for p in contrib)
+ user_projects.starred_projects.extend(p.project_name for p in starred)
+
+ return result
+
+ @monorail_servicer.PRPCMethod
+ def ExpungeUser(self, mc, request):
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.ExpungeUsers([request.email])
+
+ response = users_pb2.ExpungeUserResponse()
+ return response
diff --git a/api/v3/README.md b/api/v3/README.md
new file mode 100644
index 0000000..088cf32
--- /dev/null
+++ b/api/v3/README.md
@@ -0,0 +1,32 @@
+# Monorail v3.0 pRPC API
+
+This directory holds all the source for the Monorail pRPC API. This API is
+implemented using `.proto` files to describe a `gRPC` interface (services,
+methods, and request/response messages). It then uses a shim which
+converts the
+[`gRPC` server](http://www.grpc.io/docs/tutorials/basic/python.html)
+(which doesn't work on AppEngine, due to lack of support for HTTP/2) into a
+[`pRPC` server](https://godoc.org/github.com/luci/luci-go/grpc/prpc) which
+supports communication over HTTP/1.1, as well as text and JSON IO.
+
+- Resource name formats for each message are found in the message's resource annotation `pattern` field.
+- This v3.0 pRPC API is a resource-oriented API and aims to closely follow the principles at aip.dev.
+
+
+## API Documentation
+
+All resources, methods, request parameters, and responses are documented in
+[./api_proto](./api_proto).
+
+Resource name formats for each message are found in the message's resource annotation `pattern` field.
+
+## Development
+
+### Regenerating Python from Protocol Buffers
+
+In order to regenerate the python server and client stubs from the `.proto`
+files, run this command:
+
+```bash
+$ make prpc_proto_v3
+```
diff --git a/api/v3/__init__.py b/api/v3/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/api/v3/__init__.py
diff --git a/api/v3/api_constants.py b/api/v3/api_constants.py
new file mode 100644
index 0000000..9752242
--- /dev/null
+++ b/api/v3/api_constants.py
@@ -0,0 +1,28 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+"""Some constants used by Monorail's v3 API."""
+
+# Max comments per page in the ListComment API.
+MAX_COMMENTS_PER_PAGE = 100
+
+# Max issues per page in the SearchIssues API.
+MAX_ISSUES_PER_PAGE = 100
+
+# Max issues tp fetch in the BatchGetIssues API.
+MAX_BATCH_ISSUES = 100
+
+# Max issues to modify at once in the ModifyIssues API.
+MAX_MODIFY_ISSUES = 100
+
+# Max impacted issues allowed in a ModifyIssues API.
+MAX_MODIFY_IMPACTED_ISSUES = 50
+
+# Max approval values to modify at once in the ModifyIssueApprovalValues API.
+MAX_MODIFY_APPROVAL_VALUES = 100
+
+# Max users to fetch in the BatchGetUsers API.
+MAX_BATCH_USERS = 100
+
+# Max component defs to fetch in the ListComponentDefs API
+MAX_COMPONENTS_PER_PAGE = 100
diff --git a/api/v3/api_proto/__init__.py b/api/v3/api_proto/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/api/v3/api_proto/__init__.py
diff --git a/api/v3/api_proto/feature_objects.proto b/api/v3/api_proto/feature_objects.proto
new file mode 100644
index 0000000..716dc51
--- /dev/null
+++ b/api/v3/api_proto/feature_objects.proto
@@ -0,0 +1,88 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+// This file defines protobufs for features and related business
+// objects, e.g., hotlists.
+
+syntax = "proto3";
+
+package monorail.v3;
+
+option go_package = "api/v3/api_proto";
+
+import "google/api/field_behavior.proto";
+import "google/api/resource.proto";
+import "google/protobuf/timestamp.proto";
+import "api/v3/api_proto/issue_objects.proto";
+
+// A user-owned list of Issues.
+// Next available tag: 9
+message Hotlist {
+ option (google.api.resource) = {
+ type: "api.crbug.com/Hotlist"
+ pattern: "hotlists/{hotlist_id}"
+ };
+
+ // Resource name of the hotlist.
+ string name = 1;
+ // `display_name` must follow pattern found at `framework_bizobj.RE_HOTLIST_NAME_PATTERN`.
+ string display_name = 2 [ (google.api.field_behavior) = REQUIRED ];
+ // Resource name of the hotlist owner.
+ // Owners can update hotlist settings, editors, owner, and HotlistItems.
+ // TODO(monorail:7023): field_behavior may be changed in the future.
+ string owner = 3 [
+ (google.api.resource_reference) = {type: "api.crbug.com/User"},
+ (google.api.field_behavior) = REQUIRED ];
+ // Resource names of the hotlist editors.
+ // Editors can update hotlist HotlistItems.
+ repeated string editors = 4 [ (google.api.resource_reference) = {type: "api.crbug.com/User"} ];
+ // Summary of the hotlist.
+ string summary = 5 [ (google.api.field_behavior) = REQUIRED ];
+ // More detailed description of the purpose of the hotlist.
+ string description = 6 [ (google.api.field_behavior) = REQUIRED ];
+ // Ordered list of default columns shown on hotlist's issues list view.
+ repeated IssuesListColumn default_columns = 7;
+
+ // Privacy level of a Hotlist.
+ // Next available tag: 2
+ enum HotlistPrivacy {
+ // This value is unused.
+ HOTLIST_PRIVACY_UNSPECIFIED = 0;
+ // Only the owner and editors of the hotlist can view the hotlist.
+ PRIVATE = 1;
+ // Anyone on the web can view the hotlist.
+ PUBLIC = 2;
+ }
+ HotlistPrivacy hotlist_privacy = 8;
+}
+
+
+// Represents the the position of an Issue in a Hotlist.
+// Next available tag: 7
+message HotlistItem {
+ option (google.api.resource) = {
+ type: "api.crbug.com/HotlistItem"
+ pattern: "hotlists/{hotlist_id}/items/{item_id}"
+ };
+
+ // Resource name of the HotlistItem.
+ string name = 1;
+ // The Issue associated with this item.
+ string issue = 2 [
+ (google.api.resource_reference) = {type: "api.crbug.com/Issue"},
+ (google.api.field_behavior) = IMMUTABLE ];
+ // Represents the item's position in the Hotlist in decreasing priority order.
+ // Values will be from 1 to N (the size of the hotlist), each item having a unique rank.
+ // Changes to rank must be made in `RerankHotlistItems`.
+ uint32 rank = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
+ // Resource name of the adder of HotlistItem.
+ string adder = 4 [
+ (google.api.resource_reference) = {type: "api.crbug.com/User"},
+ (google.api.field_behavior) = OUTPUT_ONLY ];
+ // The time this HotlistItem was added to the hotlist.
+ google.protobuf.Timestamp create_time = 5 [ (google.api.field_behavior) = OUTPUT_ONLY ];
+ // User-provided additional details about this item.
+ string note = 6;
+}
\ No newline at end of file
diff --git a/api/v3/api_proto/feature_objects_pb2.py b/api/v3/api_proto/feature_objects_pb2.py
new file mode 100644
index 0000000..37a1d21
--- /dev/null
+++ b/api/v3/api_proto/feature_objects_pb2.py
@@ -0,0 +1,246 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/v3/api_proto/feature_objects.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()
+
+
+from google.api import field_behavior_pb2 as google_dot_api_dot_field__behavior__pb2
+from google.api import resource_pb2 as google_dot_api_dot_resource__pb2
+from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
+from api.v3.api_proto import issue_objects_pb2 as api_dot_v3_dot_api__proto_dot_issue__objects__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='api/v3/api_proto/feature_objects.proto',
+ package='monorail.v3',
+ syntax='proto3',
+ serialized_options=b'Z\020api/v3/api_proto',
+ create_key=_descriptor._internal_create_key,
+ serialized_pb=b'\n&api/v3/api_proto/feature_objects.proto\x12\x0bmonorail.v3\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$api/v3/api_proto/issue_objects.proto\"\xac\x03\n\x07Hotlist\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x19\n\x0c\x64isplay_name\x18\x02 \x01(\tB\x03\xe0\x41\x02\x12)\n\x05owner\x18\x03 \x01(\tB\x1a\xfa\x41\x14\n\x12\x61pi.crbug.com/User\xe0\x41\x02\x12(\n\x07\x65\x64itors\x18\x04 \x03(\tB\x17\xfa\x41\x14\n\x12\x61pi.crbug.com/User\x12\x14\n\x07summary\x18\x05 \x01(\tB\x03\xe0\x41\x02\x12\x18\n\x0b\x64\x65scription\x18\x06 \x01(\tB\x03\xe0\x41\x02\x12\x36\n\x0f\x64\x65\x66\x61ult_columns\x18\x07 \x03(\x0b\x32\x1d.monorail.v3.IssuesListColumn\x12<\n\x0fhotlist_privacy\x18\x08 \x01(\x0e\x32#.monorail.v3.Hotlist.HotlistPrivacy\"J\n\x0eHotlistPrivacy\x12\x1f\n\x1bHOTLIST_PRIVACY_UNSPECIFIED\x10\x00\x12\x0b\n\x07PRIVATE\x10\x01\x12\n\n\x06PUBLIC\x10\x02:1\xea\x41.\n\x15\x61pi.crbug.com/Hotlist\x12\x15hotlists/{hotlist_id}\"\x90\x02\n\x0bHotlistItem\x12\x0c\n\x04name\x18\x01 \x01(\t\x12*\n\x05issue\x18\x02 \x01(\tB\x1b\xfa\x41\x15\n\x13\x61pi.crbug.com/Issue\xe0\x41\x05\x12\x11\n\x04rank\x18\x03 \x01(\rB\x03\xe0\x41\x03\x12)\n\x05\x61\x64\x64\x65r\x18\x04 \x01(\tB\x1a\xfa\x41\x14\n\x12\x61pi.crbug.com/User\xe0\x41\x03\x12\x34\n\x0b\x63reate_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03\x12\x0c\n\x04note\x18\x06 \x01(\t:E\xea\x41\x42\n\x19\x61pi.crbug.com/HotlistItem\x12%hotlists/{hotlist_id}/items/{item_id}B\x12Z\x10\x61pi/v3/api_protob\x06proto3'
+ ,
+ dependencies=[google_dot_api_dot_field__behavior__pb2.DESCRIPTOR,google_dot_api_dot_resource__pb2.DESCRIPTOR,google_dot_protobuf_dot_timestamp__pb2.DESCRIPTOR,api_dot_v3_dot_api__proto_dot_issue__objects__pb2.DESCRIPTOR,])
+
+
+
+_HOTLIST_HOTLISTPRIVACY = _descriptor.EnumDescriptor(
+ name='HotlistPrivacy',
+ full_name='monorail.v3.Hotlist.HotlistPrivacy',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='HOTLIST_PRIVACY_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='PRIVATE', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='PUBLIC', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=490,
+ serialized_end=564,
+)
+_sym_db.RegisterEnumDescriptor(_HOTLIST_HOTLISTPRIVACY)
+
+
+_HOTLIST = _descriptor.Descriptor(
+ name='Hotlist',
+ full_name='monorail.v3.Hotlist',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.Hotlist.name', 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.v3.Hotlist.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=b'\340A\002', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='owner', full_name='monorail.v3.Hotlist.owner', 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=b'\372A\024\n\022api.crbug.com/User\340A\002', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='editors', full_name='monorail.v3.Hotlist.editors', index=3,
+ number=4, 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=b'\372A\024\n\022api.crbug.com/User', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='summary', full_name='monorail.v3.Hotlist.summary', index=4,
+ number=5, 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=b'\340A\002', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='description', full_name='monorail.v3.Hotlist.description', 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=b'\340A\002', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='default_columns', full_name='monorail.v3.Hotlist.default_columns', index=6,
+ number=7, 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='hotlist_privacy', full_name='monorail.v3.Hotlist.hotlist_privacy', index=7,
+ number=8, type=14, cpp_type=8, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ _HOTLIST_HOTLISTPRIVACY,
+ ],
+ serialized_options=b'\352A.\n\025api.crbug.com/Hotlist\022\025hotlists/{hotlist_id}',
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=187,
+ serialized_end=615,
+)
+
+
+_HOTLISTITEM = _descriptor.Descriptor(
+ name='HotlistItem',
+ full_name='monorail.v3.HotlistItem',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.HotlistItem.name', 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='issue', full_name='monorail.v3.HotlistItem.issue', 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=b'\372A\025\n\023api.crbug.com/Issue\340A\005', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='rank', full_name='monorail.v3.HotlistItem.rank', index=2,
+ number=3, type=13, cpp_type=3, 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=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='adder', full_name='monorail.v3.HotlistItem.adder', 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=b'\372A\024\n\022api.crbug.com/User\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='create_time', full_name='monorail.v3.HotlistItem.create_time', index=4,
+ number=5, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='note', full_name='monorail.v3.HotlistItem.note', 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=b'\352AB\n\031api.crbug.com/HotlistItem\022%hotlists/{hotlist_id}/items/{item_id}',
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=618,
+ serialized_end=890,
+)
+
+_HOTLIST.fields_by_name['default_columns'].message_type = api_dot_v3_dot_api__proto_dot_issue__objects__pb2._ISSUESLISTCOLUMN
+_HOTLIST.fields_by_name['hotlist_privacy'].enum_type = _HOTLIST_HOTLISTPRIVACY
+_HOTLIST_HOTLISTPRIVACY.containing_type = _HOTLIST
+_HOTLISTITEM.fields_by_name['create_time'].message_type = google_dot_protobuf_dot_timestamp__pb2._TIMESTAMP
+DESCRIPTOR.message_types_by_name['Hotlist'] = _HOTLIST
+DESCRIPTOR.message_types_by_name['HotlistItem'] = _HOTLISTITEM
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+Hotlist = _reflection.GeneratedProtocolMessageType('Hotlist', (_message.Message,), {
+ 'DESCRIPTOR' : _HOTLIST,
+ '__module__' : 'api.v3.api_proto.feature_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.Hotlist)
+ })
+_sym_db.RegisterMessage(Hotlist)
+
+HotlistItem = _reflection.GeneratedProtocolMessageType('HotlistItem', (_message.Message,), {
+ 'DESCRIPTOR' : _HOTLISTITEM,
+ '__module__' : 'api.v3.api_proto.feature_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.HotlistItem)
+ })
+_sym_db.RegisterMessage(HotlistItem)
+
+
+DESCRIPTOR._options = None
+_HOTLIST.fields_by_name['display_name']._options = None
+_HOTLIST.fields_by_name['owner']._options = None
+_HOTLIST.fields_by_name['editors']._options = None
+_HOTLIST.fields_by_name['summary']._options = None
+_HOTLIST.fields_by_name['description']._options = None
+_HOTLIST._options = None
+_HOTLISTITEM.fields_by_name['issue']._options = None
+_HOTLISTITEM.fields_by_name['rank']._options = None
+_HOTLISTITEM.fields_by_name['adder']._options = None
+_HOTLISTITEM.fields_by_name['create_time']._options = None
+_HOTLISTITEM._options = None
+# @@protoc_insertion_point(module_scope)
diff --git a/api/v3/api_proto/frontend.proto b/api/v3/api_proto/frontend.proto
new file mode 100644
index 0000000..b6e8564
--- /dev/null
+++ b/api/v3/api_proto/frontend.proto
@@ -0,0 +1,85 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+syntax = "proto3";
+
+package monorail.v3;
+
+option go_package = "api/v3/api_proto";
+
+import "google/api/field_behavior.proto";
+import "google/api/resource.proto";
+import "api/v3/api_proto/project_objects.proto";
+
+// ***DO NOT CALL rpcs IN THIS SERVICE.***
+// This service is for Monorail's frontend only.
+
+service Frontend {
+ // status: DO NOT USE
+ // Returns all project specific configurations needed for the SPA client.
+ //
+ // Raises:
+ // INVALID_ARGUMENT if the project resource name provided is invalid.
+ // NOT_FOUND if the parent project is not found.
+ // PERMISSION_DENIED if user is not allowed to view this project.
+ rpc GatherProjectEnvironment (GatherProjectEnvironmentRequest) returns (GatherProjectEnvironmentResponse) {};
+
+ // status: DO NOT USE
+ // Returns all of a given user's project memberships.
+ //
+ // Raises:
+ // NOT_FOUND if the user is not found.
+ // INVALID_ARGUMENT if the user resource name provided is invalid.
+ rpc GatherProjectMembershipsForUser (GatherProjectMembershipsForUserRequest)
+ returns (GatherProjectMembershipsForUserResponse) {}
+}
+
+
+// Request message for GatherProjectEnvironment
+// Next available tag: 2
+message GatherProjectEnvironmentRequest {
+ // The name of the project these config environments belong to.
+ string parent = 1 [
+ (google.api.resource_reference) = {type: "api.crbug.com/Project"},
+ (google.api.field_behavior) = REQUIRED ];
+}
+
+
+// Response message for GatherProjectEnvironment
+// Next available tag: 9
+message GatherProjectEnvironmentResponse {
+ // Project definitions such as display_name and summary.
+ Project project = 1;
+ // Configurations of this project such as default search term,
+ // default templates for members and non members.
+ ProjectConfig project_config = 2;
+ // List of statuses that belong to this project.
+ repeated StatusDef statuses = 3;
+ // List of well known labels that belong to this project.
+ repeated LabelDef well_known_labels = 4;
+ // List of components that belong to this project.
+ repeated ComponentDef components = 5;
+ // List of custom fields that belong to this project.
+ repeated FieldDef fields = 6;
+ // List of approval fields that belong to this project.
+ repeated ApprovalDef approval_fields = 7;
+ // Saved search queries that admins defined for this project.
+ repeated ProjectSavedQuery saved_queries = 8;
+}
+
+// The request message for Frontend.GatherProjectMembershipsForUser.
+// Next available tag: 2
+message GatherProjectMembershipsForUserRequest {
+ // The name of the user to request.
+ string user = 1 [
+ (google.api.resource_reference) = {type: "api.crbug.com/User"}];
+}
+
+// The response message for Frontend.GatherProjectMembershipsForUser.
+// Next available tag: 2
+message GatherProjectMembershipsForUserResponse {
+ // The projects that the user is a member of.
+ repeated ProjectMember project_memberships = 1;
+}
\ No newline at end of file
diff --git a/api/v3/api_proto/frontend_pb2.py b/api/v3/api_proto/frontend_pb2.py
new file mode 100644
index 0000000..980b6cd
--- /dev/null
+++ b/api/v3/api_proto/frontend_pb2.py
@@ -0,0 +1,291 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/v3/api_proto/frontend.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()
+
+
+from google.api import field_behavior_pb2 as google_dot_api_dot_field__behavior__pb2
+from google.api import resource_pb2 as google_dot_api_dot_resource__pb2
+from api.v3.api_proto import project_objects_pb2 as api_dot_v3_dot_api__proto_dot_project__objects__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='api/v3/api_proto/frontend.proto',
+ package='monorail.v3',
+ syntax='proto3',
+ serialized_options=b'Z\020api/v3/api_proto',
+ create_key=_descriptor._internal_create_key,
+ serialized_pb=b'\n\x1f\x61pi/v3/api_proto/frontend.proto\x12\x0bmonorail.v3\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a&api/v3/api_proto/project_objects.proto\"P\n\x1fGatherProjectEnvironmentRequest\x12-\n\x06parent\x18\x01 \x01(\tB\x1d\xfa\x41\x17\n\x15\x61pi.crbug.com/Project\xe0\x41\x02\"\x99\x03\n GatherProjectEnvironmentResponse\x12%\n\x07project\x18\x01 \x01(\x0b\x32\x14.monorail.v3.Project\x12\x32\n\x0eproject_config\x18\x02 \x01(\x0b\x32\x1a.monorail.v3.ProjectConfig\x12(\n\x08statuses\x18\x03 \x03(\x0b\x32\x16.monorail.v3.StatusDef\x12\x30\n\x11well_known_labels\x18\x04 \x03(\x0b\x32\x15.monorail.v3.LabelDef\x12-\n\ncomponents\x18\x05 \x03(\x0b\x32\x19.monorail.v3.ComponentDef\x12%\n\x06\x66ields\x18\x06 \x03(\x0b\x32\x15.monorail.v3.FieldDef\x12\x31\n\x0f\x61pproval_fields\x18\x07 \x03(\x0b\x32\x18.monorail.v3.ApprovalDef\x12\x35\n\rsaved_queries\x18\x08 \x03(\x0b\x32\x1e.monorail.v3.ProjectSavedQuery\"O\n&GatherProjectMembershipsForUserRequest\x12%\n\x04user\x18\x01 \x01(\tB\x17\xfa\x41\x14\n\x12\x61pi.crbug.com/User\"b\n\'GatherProjectMembershipsForUserResponse\x12\x37\n\x13project_memberships\x18\x01 \x03(\x0b\x32\x1a.monorail.v3.ProjectMember2\x96\x02\n\x08\x46rontend\x12y\n\x18GatherProjectEnvironment\x12,.monorail.v3.GatherProjectEnvironmentRequest\x1a-.monorail.v3.GatherProjectEnvironmentResponse\"\x00\x12\x8e\x01\n\x1fGatherProjectMembershipsForUser\x12\x33.monorail.v3.GatherProjectMembershipsForUserRequest\x1a\x34.monorail.v3.GatherProjectMembershipsForUserResponse\"\x00\x42\x12Z\x10\x61pi/v3/api_protob\x06proto3'
+ ,
+ dependencies=[google_dot_api_dot_field__behavior__pb2.DESCRIPTOR,google_dot_api_dot_resource__pb2.DESCRIPTOR,api_dot_v3_dot_api__proto_dot_project__objects__pb2.DESCRIPTOR,])
+
+
+
+
+_GATHERPROJECTENVIRONMENTREQUEST = _descriptor.Descriptor(
+ name='GatherProjectEnvironmentRequest',
+ full_name='monorail.v3.GatherProjectEnvironmentRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='parent', full_name='monorail.v3.GatherProjectEnvironmentRequest.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=b'\372A\027\n\025api.crbug.com/Project\340A\002', 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=148,
+ serialized_end=228,
+)
+
+
+_GATHERPROJECTENVIRONMENTRESPONSE = _descriptor.Descriptor(
+ name='GatherProjectEnvironmentResponse',
+ full_name='monorail.v3.GatherProjectEnvironmentResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project', full_name='monorail.v3.GatherProjectEnvironmentResponse.project', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ 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_config', full_name='monorail.v3.GatherProjectEnvironmentResponse.project_config', index=1,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ 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='statuses', full_name='monorail.v3.GatherProjectEnvironmentResponse.statuses', index=2,
+ number=3, 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='well_known_labels', full_name='monorail.v3.GatherProjectEnvironmentResponse.well_known_labels', index=3,
+ number=4, 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='components', full_name='monorail.v3.GatherProjectEnvironmentResponse.components', index=4,
+ 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='fields', full_name='monorail.v3.GatherProjectEnvironmentResponse.fields', index=5,
+ number=6, 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='approval_fields', full_name='monorail.v3.GatherProjectEnvironmentResponse.approval_fields', index=6,
+ number=7, 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='saved_queries', full_name='monorail.v3.GatherProjectEnvironmentResponse.saved_queries', index=7,
+ number=8, 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='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=231,
+ serialized_end=640,
+)
+
+
+_GATHERPROJECTMEMBERSHIPSFORUSERREQUEST = _descriptor.Descriptor(
+ name='GatherProjectMembershipsForUserRequest',
+ full_name='monorail.v3.GatherProjectMembershipsForUserRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='user', full_name='monorail.v3.GatherProjectMembershipsForUserRequest.user', 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=b'\372A\024\n\022api.crbug.com/User', 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=642,
+ serialized_end=721,
+)
+
+
+_GATHERPROJECTMEMBERSHIPSFORUSERRESPONSE = _descriptor.Descriptor(
+ name='GatherProjectMembershipsForUserResponse',
+ full_name='monorail.v3.GatherProjectMembershipsForUserResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_memberships', full_name='monorail.v3.GatherProjectMembershipsForUserResponse.project_memberships', 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='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=723,
+ serialized_end=821,
+)
+
+_GATHERPROJECTENVIRONMENTRESPONSE.fields_by_name['project'].message_type = api_dot_v3_dot_api__proto_dot_project__objects__pb2._PROJECT
+_GATHERPROJECTENVIRONMENTRESPONSE.fields_by_name['project_config'].message_type = api_dot_v3_dot_api__proto_dot_project__objects__pb2._PROJECTCONFIG
+_GATHERPROJECTENVIRONMENTRESPONSE.fields_by_name['statuses'].message_type = api_dot_v3_dot_api__proto_dot_project__objects__pb2._STATUSDEF
+_GATHERPROJECTENVIRONMENTRESPONSE.fields_by_name['well_known_labels'].message_type = api_dot_v3_dot_api__proto_dot_project__objects__pb2._LABELDEF
+_GATHERPROJECTENVIRONMENTRESPONSE.fields_by_name['components'].message_type = api_dot_v3_dot_api__proto_dot_project__objects__pb2._COMPONENTDEF
+_GATHERPROJECTENVIRONMENTRESPONSE.fields_by_name['fields'].message_type = api_dot_v3_dot_api__proto_dot_project__objects__pb2._FIELDDEF
+_GATHERPROJECTENVIRONMENTRESPONSE.fields_by_name['approval_fields'].message_type = api_dot_v3_dot_api__proto_dot_project__objects__pb2._APPROVALDEF
+_GATHERPROJECTENVIRONMENTRESPONSE.fields_by_name['saved_queries'].message_type = api_dot_v3_dot_api__proto_dot_project__objects__pb2._PROJECTSAVEDQUERY
+_GATHERPROJECTMEMBERSHIPSFORUSERRESPONSE.fields_by_name['project_memberships'].message_type = api_dot_v3_dot_api__proto_dot_project__objects__pb2._PROJECTMEMBER
+DESCRIPTOR.message_types_by_name['GatherProjectEnvironmentRequest'] = _GATHERPROJECTENVIRONMENTREQUEST
+DESCRIPTOR.message_types_by_name['GatherProjectEnvironmentResponse'] = _GATHERPROJECTENVIRONMENTRESPONSE
+DESCRIPTOR.message_types_by_name['GatherProjectMembershipsForUserRequest'] = _GATHERPROJECTMEMBERSHIPSFORUSERREQUEST
+DESCRIPTOR.message_types_by_name['GatherProjectMembershipsForUserResponse'] = _GATHERPROJECTMEMBERSHIPSFORUSERRESPONSE
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+GatherProjectEnvironmentRequest = _reflection.GeneratedProtocolMessageType('GatherProjectEnvironmentRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _GATHERPROJECTENVIRONMENTREQUEST,
+ '__module__' : 'api.v3.api_proto.frontend_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.GatherProjectEnvironmentRequest)
+ })
+_sym_db.RegisterMessage(GatherProjectEnvironmentRequest)
+
+GatherProjectEnvironmentResponse = _reflection.GeneratedProtocolMessageType('GatherProjectEnvironmentResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _GATHERPROJECTENVIRONMENTRESPONSE,
+ '__module__' : 'api.v3.api_proto.frontend_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.GatherProjectEnvironmentResponse)
+ })
+_sym_db.RegisterMessage(GatherProjectEnvironmentResponse)
+
+GatherProjectMembershipsForUserRequest = _reflection.GeneratedProtocolMessageType('GatherProjectMembershipsForUserRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _GATHERPROJECTMEMBERSHIPSFORUSERREQUEST,
+ '__module__' : 'api.v3.api_proto.frontend_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.GatherProjectMembershipsForUserRequest)
+ })
+_sym_db.RegisterMessage(GatherProjectMembershipsForUserRequest)
+
+GatherProjectMembershipsForUserResponse = _reflection.GeneratedProtocolMessageType('GatherProjectMembershipsForUserResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _GATHERPROJECTMEMBERSHIPSFORUSERRESPONSE,
+ '__module__' : 'api.v3.api_proto.frontend_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.GatherProjectMembershipsForUserResponse)
+ })
+_sym_db.RegisterMessage(GatherProjectMembershipsForUserResponse)
+
+
+DESCRIPTOR._options = None
+_GATHERPROJECTENVIRONMENTREQUEST.fields_by_name['parent']._options = None
+_GATHERPROJECTMEMBERSHIPSFORUSERREQUEST.fields_by_name['user']._options = None
+
+_FRONTEND = _descriptor.ServiceDescriptor(
+ name='Frontend',
+ full_name='monorail.v3.Frontend',
+ file=DESCRIPTOR,
+ index=0,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ serialized_start=824,
+ serialized_end=1102,
+ methods=[
+ _descriptor.MethodDescriptor(
+ name='GatherProjectEnvironment',
+ full_name='monorail.v3.Frontend.GatherProjectEnvironment',
+ index=0,
+ containing_service=None,
+ input_type=_GATHERPROJECTENVIRONMENTREQUEST,
+ output_type=_GATHERPROJECTENVIRONMENTRESPONSE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='GatherProjectMembershipsForUser',
+ full_name='monorail.v3.Frontend.GatherProjectMembershipsForUser',
+ index=1,
+ containing_service=None,
+ input_type=_GATHERPROJECTMEMBERSHIPSFORUSERREQUEST,
+ output_type=_GATHERPROJECTMEMBERSHIPSFORUSERRESPONSE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+])
+_sym_db.RegisterServiceDescriptor(_FRONTEND)
+
+DESCRIPTOR.services_by_name['Frontend'] = _FRONTEND
+
+# @@protoc_insertion_point(module_scope)
diff --git a/api/v3/api_proto/frontend_prpc_pb2.py b/api/v3/api_proto/frontend_prpc_pb2.py
new file mode 100644
index 0000000..5087c3a
--- /dev/null
+++ b/api/v3/api_proto/frontend_prpc_pb2.py
@@ -0,0 +1,864 @@
+# Generated by the pRPC protocol buffer compiler plugin. DO NOT EDIT!
+# source: api/v3/api_proto/frontend.proto
+
+import base64
+import zlib
+
+from google.protobuf import descriptor_pb2
+
+# Includes description of the api/v3/api_proto/frontend.proto and all of its transitive
+# dependencies. Includes source code info.
+FILE_DESCRIPTOR_SET = descriptor_pb2.FileDescriptorSet()
+FILE_DESCRIPTOR_SET.ParseFromString(zlib.decompress(base64.b64decode(
+ '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'
+ 'ohRl/GRnIs843p2JEztexpM4tmcSz8Sxk8nElifeYmfsJCMlfoodx+MZx4od+3Nsjz99o7x3fu'
+ 'fce+tWoQGScvK9P54+2Wr8quqec7dzz733LNnncrf7mzAmphcvLM/Z0jGdrnMb8UXPbxmnUb1c'
+ 'I0FXl/EOv5E308pkU/5I36ksPD02Y701a3Vc1pQX/YKJp7IDJRqLxcrDzP36Av9O7/BbsJjwIF'
+ 'eGrRGQPu43ssrJhq1tfXuuRQw/SgX5prPgr49xkd7lbx+fyE1Mjk8N5E9OTZwbyydMYVN+w+hY'
+ 'Hlavvt/UPzQ6TqiL38P5win67XWO+G1xYunQ32EVip/JUtt8fyA/Vsj306MBKTvXPzF4Nh+4x0'
+ '4/l8v7W+KdFnXKbXV0upp+ipNM/g0z1sc73+f6qSH4Pd9Yb8ba30u2/126/cVmeXes/TU18yPW'
+ '+kP++hiO1h/KncgPvah2yj+XO+FvjreTqe3+end+6iGucPBTGum77X5K+cjXb6TrnatrN9shNU'
+ 'Wk1TKxVtPksxiSUrLMjj5MXm0zOiUyiUuRWbExejiIZzyie/2m4swlUuppgnjE6Jbncx1+Ot5M'
+ 'k7XSYkG9lj7sN8Gtb6m2tZk+aOvbsQpv/E5BvZs+47cTddqvF+enZCHamuKGueX53HZ/W5xeTr'
+ '1J5aBybfrLMf4wfb+/HiH4p1R6udrWFjZ53lufkTy9ipqOq7cL6/Cx/it92l9XrixFZflc1p76'
+ 'ZQ1WlmJFtdKndknUeVFJrWuVNL60GC+JPjUlUQXheh4VtW6tCqJ34hXEx3ZhM0VO9K0KW79WYQ'
+ 'P0arwwfGwKO+g3l2bKCFG2tW3t4aLfy7zD8YNkH6RP+c3TF6uIQ09zB3bxPdfXedl+/qqgv87c'
+ 'TVKWf0bCyVlVOLmJWZa5329PdGl6u99Cw3wqKqqxkCLgLJeGh8VHpiIhiIfFR/hhZp/fnuhV8L'
+ 'RYmis9onniPzJf8Pwg2WnpV/rtpCGWZ69OEWtztE2p8UdtfUeur9ezI/z1hPq40FaJ/Z2e8Tcs'
+ 'Vueh5LGBObuIchXa+u64TgoF+r5gfV4IFhMISejWOVqUl2pT0HWUWPMFGiOEfVxKpRn1vEH5uA'
+ 'DB485hvy1eD8j8kdGJwZPnpiYKg6dO5QvjCZnf4jeO5M/mCyTu2/3W3Mi5qf7R4eH8yETgdl7w'
+ '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'
+ 'HdB8A=')))
+_INDEX = {
+ f.name: {
+ 'descriptor': f,
+ 'services': {s.name: s for s in f.service},
+ }
+ for f in FILE_DESCRIPTOR_SET.file
+}
+
+
+FrontendServiceDescription = {
+ 'file_descriptor_set': FILE_DESCRIPTOR_SET,
+ 'file_descriptor': _INDEX[u'api/v3/api_proto/frontend.proto']['descriptor'],
+ 'service_descriptor': _INDEX[u'api/v3/api_proto/frontend.proto']['services'][u'Frontend'],
+}
diff --git a/api/v3/api_proto/hotlists.proto b/api/v3/api_proto/hotlists.proto
new file mode 100644
index 0000000..1f668f7
--- /dev/null
+++ b/api/v3/api_proto/hotlists.proto
@@ -0,0 +1,276 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+syntax = "proto3";
+
+package monorail.v3;
+
+option go_package = "api/v3/api_proto";
+
+import "api/v3/api_proto/feature_objects.proto";
+import "google/protobuf/field_mask.proto";
+import "google/protobuf/empty.proto";
+import "google/api/field_behavior.proto";
+import "google/api/resource.proto";
+
+// ***ONLY CALL rpcs WITH `status: {ALPHA|STABLE}`***
+// rpcs without `status` are not ready.
+
+// Hotlists service includes all methods needed for managing Hotlists.
+service Hotlists {
+ // status: NOT READY
+ // Creates a new hotlist.
+ //
+ // Raises:
+ // NOT_FOUND if some given hotlist editors do not exist.
+ // ALREADY_EXISTS if a hotlist with the same name owned by the user
+ // already exists.
+ // INVALID_ARGUMENT if a `hotlist.owner` is given.
+ rpc CreateHotlist (CreateHotlistRequest) returns (Hotlist) {}
+
+ // status: NOT READY
+ // Returns the requested Hotlist.
+ //
+ // Raises:
+ // NOT_FOUND if the requested hotlist is not found.
+ // PERMISSION_DENIED if the requester is not allowed to view the hotlist.
+ // INVALID_ARGUMENT if the given resource name is not valid.
+ rpc GetHotlist (GetHotlistRequest) returns (Hotlist) {}
+
+ // status: NOT READY
+ // Updates a hotlist.
+ //
+ // Raises:
+ // NOT_FOUND if the hotlist is not found.
+ // PERMISSION_DENIED if the requester is not allowed to update the hotlist.
+ // INVALID_ARGUMENT if required fields are missing.
+ rpc UpdateHotlist (UpdateHotlistRequest) returns (Hotlist) {}
+
+ // status: NOT READY
+ // Deletes a hotlist.
+ //
+ // Raises:
+ // NOT_FOUND if the hotlist is not found.
+ // PERMISSION_DENIED if the requester is not allowed to delete the hotlist.
+ rpc DeleteHotlist (GetHotlistRequest) returns (google.protobuf.Empty) {}
+
+ // status: NOT READY
+ // Returns a list of all HotlistItems in the hotlist.
+ //
+ // Raises:
+ // NOT_FOUND if the parent hotlist is not found.
+ // PERMISSION_DENIED if the requester is not allowed to view the hotlist.
+ // INVALID_ARGUMENT if the page_token or given hotlist resource name is not
+ // valid.
+ rpc ListHotlistItems (ListHotlistItemsRequest) returns (ListHotlistItemsResponse) {}
+
+ // status: NOT READY
+ // Reranks a hotlist's items.
+ //
+ // Raises:
+ // NOT_FOUND if the hotlist or issues to rerank are not found.
+ // PERMISSION_DENIED if the requester is not allowed to rerank the hotlist
+ // or view issues they're trying to rerank.
+ // INVALID_ARGUMENT if the `target_position` is invalid or `hotlist_items`
+ // is empty or contains items not in the Hotlist.
+ rpc RerankHotlistItems (RerankHotlistItemsRequest) returns (google.protobuf.Empty) {}
+
+ // status: NOT READY
+ // Adds new items associated with given issues to a hotlist.
+ //
+ // Raises:
+ // NOT_FOUND if the parent hotlist or issues are not found.
+ // PERMISSION_DENIED if the requester is not allowed to edit the hotlist or
+ // view issues they are trying to add.
+ // INVALID_ARGUMENT if the `target_position` is invalid or `hotlist_items`
+ // is empty or contains items not in the Hotlist.
+ rpc AddHotlistItems (AddHotlistItemsRequest) returns (google.protobuf.Empty) {}
+
+ // status: NOT READY
+ // Removes items associated with given issues from a hotlist.
+ //
+ // Raises:
+ // NOT_FOUND if the parent hotlist or issues are not found.
+ // PERMISSION_DENIED if the requester is not allowed to edit the hotlist or
+ // view issues they are trying to remove.
+ // INVALID_ARGUMENT if the `target_position` is invalid or `hotlist_items`
+ // is empty or contains items not in the Hotlist.
+ rpc RemoveHotlistItems (RemoveHotlistItemsRequest) returns (google.protobuf.Empty) {}
+
+ // status: NOT READY
+ // Removes editors assigned to a hotlist.
+ //
+ // Raises:
+ // NOT_FOUND if the hotlist is not found.
+ // PERMISSION_DENIED if the requester is not allowed to remove all specified
+ // editors from the hotlist.
+ // INVALID_ARGUMENT if any specified editors are not in the hotlist.
+ rpc RemoveHotlistEditors (RemoveHotlistEditorsRequest) returns (google.protobuf.Empty) {}
+
+ // status: NOT READY
+ // Gathers all viewable hotlists that a user is a member of.
+ //
+ // Raises:
+ // NOT_FOUND if the user is not found.
+ // INVALID_ARGUMENT if the `user` is invalid.
+ rpc GatherHotlistsForUser (GatherHotlistsForUserRequest) returns (GatherHotlistsForUserResponse) {}
+}
+
+
+// Request message for CreateHotlist method.
+// Next available tag: 2
+message CreateHotlistRequest {
+ // The hotlist to create.
+ // `hotlist.owner` must be empty. The owner of the new hotlist will be
+ // set to the requester.
+ Hotlist hotlist = 1 [ (google.api.field_behavior) = REQUIRED ];
+}
+
+
+// Request message for GetHotlist method.
+// Next available tag: 2
+message GetHotlistRequest {
+ // The name of the hotlist to retrieve.
+ string name = 1 [
+ (google.api.field_behavior) = REQUIRED,
+ (google.api.resource_reference) = {type: "api.crbug.com/Hotlist"}];
+}
+
+
+// Request message for UpdateHotlist method.
+// Next available tag: 2
+message UpdateHotlistRequest {
+ // The hotlist's `name` field is used to identify the hotlist to be updated.
+ Hotlist hotlist = 1 [
+ (google.api.field_behavior) = REQUIRED,
+ (google.api.resource_reference) = {type: "api.crbug.com/Hotlist"} ];
+ // The list of fields to be updated.
+ google.protobuf.FieldMask update_mask = 2 [ (google.api.field_behavior) = REQUIRED ];
+}
+
+
+// Request message for ListHotlistItems method.
+// Next available tag: 5
+message ListHotlistItemsRequest {
+ // The parent hotlist, which owns this collection of items.
+ string parent = 1 [
+ (google.api.field_behavior) = REQUIRED,
+ (google.api.resource_reference) = {type: "api.crbug.com/Hotlist"} ];
+ // The maximum number of items to return. The service may return fewer than
+ // this value.
+ // If unspecified, at most 1000 items will be returned.
+ // The maximum value is 1000; values above 1000 will be coerced to 1000.
+ int32 page_size = 2;
+ // The string of comma separated field names used to order the items.
+ // Adding '-' before a field, reverses the sort order.
+ // E.g. 'stars,-status' sorts the items by number of stars low to high, then
+ // status high to low.
+ // If unspecified, items will be ordered by their rank in the parent.
+ string order_by = 3;
+ // A page token, received from a previous `ListHotlistItems` call.
+ // Provide this to retrieve the subsequent page.
+ //
+ // When paginating, all other parameters provided to `ListHotlistItems` must
+ // match the call that provided the page token.
+ string page_token = 4;
+}
+
+
+// Response to ListHotlistItems call.
+// Next available tag: 3
+message ListHotlistItemsResponse {
+ // The items from the specified hotlist.
+ repeated HotlistItem items = 1;
+ // A token, which can be sent as `page_token` to retrieve the next page.
+ // If this field is omitted, there are no subsequent pages.
+ string next_page_token = 2;
+}
+
+
+// The request used to rerank a Hotlist.
+// Next available tag: 4
+message RerankHotlistItemsRequest {
+ // Resource name of the Hotlist to rerank.
+ string name = 1 [
+ (google.api.resource_reference) = {type: "api.crbug.com/Hotlist"},
+ (google.api.field_behavior) = REQUIRED ];
+ // HotlistItems to be moved. The order of `hotlist_items` will
+ // determine the order of these items after they have been moved.
+ // E.g. With items [a, b, c, d, e], moving [d, c] to `target_position` 3, will
+ // result in items [a, b, e, d, c].
+ repeated string hotlist_items = 2 [
+ (google.api.resource_reference) = {type: "api.crbug.com/HotlistItem"},
+ (google.api.field_behavior) = REQUIRED ];
+ // Target starting position of the moved items.
+ // `target_position` must be between 0 and (# hotlist items - # items being moved).
+ uint32 target_position = 3 [ (google.api.field_behavior) = REQUIRED ];
+}
+
+
+// Request message for an AddHotlistItems call.
+// Next available tag: 4
+message AddHotlistItemsRequest {
+ // Resource name of the Hotlist to add new items to.
+ string parent = 1 [
+ (google.api.field_behavior) = REQUIRED,
+ (google.api.resource_reference) = {type: "api.crbug.com/Hotlist"} ];
+ // Resource names of Issues to associate with new HotlistItems added to `parent`.
+ repeated string issues = 2 [
+ (google.api.field_behavior) = REQUIRED,
+ (google.api.resource_reference) = {type: "api.crbug.com/Issue"} ];
+ // Target starting position of the new items.
+ // `target_position` must be between [0 and # of items that currently exist in
+ // `parent`]. The request will fail if a specified `target_position` is outside
+ // of this range.
+ // New HotlistItems added to a non-last position of the hotlist will
+ // cause ranks of existing HotlistItems below `target_position` to be adjusted.
+ // If no `target_position` is given, new items will be added to the end of
+ // `parent`.
+ uint32 target_position = 3;
+}
+
+
+// Request message for a RemoveHotlistItems call.
+// Next available tag: 3
+message RemoveHotlistItemsRequest {
+ // Resource name of the Hotlist to remove items from.
+ string parent = 1 [
+ (google.api.field_behavior) = REQUIRED,
+ (google.api.resource_reference) = {type: "api.crbug.com/Hotlist"} ];
+ // Resource names of Issues associated with HotlistItems that should be removed.
+ repeated string issues = 2 [
+ (google.api.field_behavior) = REQUIRED,
+ (google.api.resource_reference) = {type: "api.crbug.com/Issue"} ];
+}
+
+
+// Request message for a RemoveHotlistEditors call.
+// Next available tag: 3
+message RemoveHotlistEditorsRequest {
+ // Resource name of the Hotlist to remove editors from.
+ string name = 1 [
+ (google.api.field_behavior) = REQUIRED,
+ (google.api.resource_reference) = {type: "api.crbug.com/Hotlist"} ];
+ // Resource names of Users associated with the hotlist that should be removed.
+ repeated string editors = 2 [
+ (google.api.field_behavior) = REQUIRED,
+ (google.api.resource_reference) = {type: "api.crbug.com/User"} ];
+}
+
+
+// Request message for a GatherHotlistsForUser call.
+// Next available tag: 2
+message GatherHotlistsForUserRequest {
+ // Resource name of the user whose hotlists we want to fetch.
+ string user = 1 [ (google.api.field_behavior) = REQUIRED,
+ (google.api.resource_reference) = {type: "api.crbug.com/User"} ];
+}
+
+
+// Response message for a GatherHotlistsForUser call.
+// Next available tag: 2
+message GatherHotlistsForUserResponse {
+ repeated Hotlist hotlists = 1;
+}
\ No newline at end of file
diff --git a/api/v3/api_proto/hotlists_pb2.py b/api/v3/api_proto/hotlists_pb2.py
new file mode 100644
index 0000000..0258602
--- /dev/null
+++ b/api/v3/api_proto/hotlists_pb2.py
@@ -0,0 +1,690 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/v3/api_proto/hotlists.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()
+
+
+from api.v3.api_proto import feature_objects_pb2 as api_dot_v3_dot_api__proto_dot_feature__objects__pb2
+from google.protobuf import field_mask_pb2 as google_dot_protobuf_dot_field__mask__pb2
+from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
+from google.api import field_behavior_pb2 as google_dot_api_dot_field__behavior__pb2
+from google.api import resource_pb2 as google_dot_api_dot_resource__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='api/v3/api_proto/hotlists.proto',
+ package='monorail.v3',
+ syntax='proto3',
+ serialized_options=b'Z\020api/v3/api_proto',
+ create_key=_descriptor._internal_create_key,
+ serialized_pb=b'\n\x1f\x61pi/v3/api_proto/hotlists.proto\x12\x0bmonorail.v3\x1a&api/v3/api_proto/feature_objects.proto\x1a google/protobuf/field_mask.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\"B\n\x14\x43reateHotlistRequest\x12*\n\x07hotlist\x18\x01 \x01(\x0b\x32\x14.monorail.v3.HotlistB\x03\xe0\x41\x02\"@\n\x11GetHotlistRequest\x12+\n\x04name\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\n\x15\x61pi.crbug.com/Hotlist\"\x92\x01\n\x14UpdateHotlistRequest\x12\x44\n\x07hotlist\x18\x01 \x01(\x0b\x32\x14.monorail.v3.HotlistB\x1d\xe0\x41\x02\xfa\x41\x17\n\x15\x61pi.crbug.com/Hotlist\x12\x34\n\x0bupdate_mask\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.FieldMaskB\x03\xe0\x41\x02\"\x81\x01\n\x17ListHotlistItemsRequest\x12-\n\x06parent\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\n\x15\x61pi.crbug.com/Hotlist\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x10\n\x08order_by\x18\x03 \x01(\t\x12\x12\n\npage_token\x18\x04 \x01(\t\"\\\n\x18ListHotlistItemsResponse\x12\'\n\x05items\x18\x01 \x03(\x0b\x32\x18.monorail.v3.HotlistItem\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\"\xa0\x01\n\x19RerankHotlistItemsRequest\x12+\n\x04name\x18\x01 \x01(\tB\x1d\xfa\x41\x17\n\x15\x61pi.crbug.com/Hotlist\xe0\x41\x02\x12\x38\n\rhotlist_items\x18\x02 \x03(\tB!\xfa\x41\x1b\n\x19\x61pi.crbug.com/HotlistItem\xe0\x41\x02\x12\x1c\n\x0ftarget_position\x18\x03 \x01(\rB\x03\xe0\x41\x02\"\x8d\x01\n\x16\x41\x64\x64HotlistItemsRequest\x12-\n\x06parent\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\n\x15\x61pi.crbug.com/Hotlist\x12+\n\x06issues\x18\x02 \x03(\tB\x1b\xe0\x41\x02\xfa\x41\x15\n\x13\x61pi.crbug.com/Issue\x12\x17\n\x0ftarget_position\x18\x03 \x01(\r\"w\n\x19RemoveHotlistItemsRequest\x12-\n\x06parent\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\n\x15\x61pi.crbug.com/Hotlist\x12+\n\x06issues\x18\x02 \x03(\tB\x1b\xe0\x41\x02\xfa\x41\x15\n\x13\x61pi.crbug.com/Issue\"w\n\x1bRemoveHotlistEditorsRequest\x12+\n\x04name\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\n\x15\x61pi.crbug.com/Hotlist\x12+\n\x07\x65\x64itors\x18\x02 \x03(\tB\x1a\xe0\x41\x02\xfa\x41\x14\n\x12\x61pi.crbug.com/User\"H\n\x1cGatherHotlistsForUserRequest\x12(\n\x04user\x18\x01 \x01(\tB\x1a\xe0\x41\x02\xfa\x41\x14\n\x12\x61pi.crbug.com/User\"G\n\x1dGatherHotlistsForUserResponse\x12&\n\x08hotlists\x18\x01 \x03(\x0b\x32\x14.monorail.v3.Hotlist2\xe6\x06\n\x08Hotlists\x12J\n\rCreateHotlist\x12!.monorail.v3.CreateHotlistRequest\x1a\x14.monorail.v3.Hotlist\"\x00\x12\x44\n\nGetHotlist\x12\x1e.monorail.v3.GetHotlistRequest\x1a\x14.monorail.v3.Hotlist\"\x00\x12J\n\rUpdateHotlist\x12!.monorail.v3.UpdateHotlistRequest\x1a\x14.monorail.v3.Hotlist\"\x00\x12I\n\rDeleteHotlist\x12\x1e.monorail.v3.GetHotlistRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x61\n\x10ListHotlistItems\x12$.monorail.v3.ListHotlistItemsRequest\x1a%.monorail.v3.ListHotlistItemsResponse\"\x00\x12V\n\x12RerankHotlistItems\x12&.monorail.v3.RerankHotlistItemsRequest\x1a\x16.google.protobuf.Empty\"\x00\x12P\n\x0f\x41\x64\x64HotlistItems\x12#.monorail.v3.AddHotlistItemsRequest\x1a\x16.google.protobuf.Empty\"\x00\x12V\n\x12RemoveHotlistItems\x12&.monorail.v3.RemoveHotlistItemsRequest\x1a\x16.google.protobuf.Empty\"\x00\x12Z\n\x14RemoveHotlistEditors\x12(.monorail.v3.RemoveHotlistEditorsRequest\x1a\x16.google.protobuf.Empty\"\x00\x12p\n\x15GatherHotlistsForUser\x12).monorail.v3.GatherHotlistsForUserRequest\x1a*.monorail.v3.GatherHotlistsForUserResponse\"\x00\x42\x12Z\x10\x61pi/v3/api_protob\x06proto3'
+ ,
+ dependencies=[api_dot_v3_dot_api__proto_dot_feature__objects__pb2.DESCRIPTOR,google_dot_protobuf_dot_field__mask__pb2.DESCRIPTOR,google_dot_protobuf_dot_empty__pb2.DESCRIPTOR,google_dot_api_dot_field__behavior__pb2.DESCRIPTOR,google_dot_api_dot_resource__pb2.DESCRIPTOR,])
+
+
+
+
+_CREATEHOTLISTREQUEST = _descriptor.Descriptor(
+ name='CreateHotlistRequest',
+ full_name='monorail.v3.CreateHotlistRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='hotlist', full_name='monorail.v3.CreateHotlistRequest.hotlist', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\002', 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=211,
+ serialized_end=277,
+)
+
+
+_GETHOTLISTREQUEST = _descriptor.Descriptor(
+ name='GetHotlistRequest',
+ full_name='monorail.v3.GetHotlistRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.GetHotlistRequest.name', 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=b'\340A\002\372A\027\n\025api.crbug.com/Hotlist', 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=279,
+ serialized_end=343,
+)
+
+
+_UPDATEHOTLISTREQUEST = _descriptor.Descriptor(
+ name='UpdateHotlistRequest',
+ full_name='monorail.v3.UpdateHotlistRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='hotlist', full_name='monorail.v3.UpdateHotlistRequest.hotlist', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\002\372A\027\n\025api.crbug.com/Hotlist', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='update_mask', full_name='monorail.v3.UpdateHotlistRequest.update_mask', index=1,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\002', 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=346,
+ serialized_end=492,
+)
+
+
+_LISTHOTLISTITEMSREQUEST = _descriptor.Descriptor(
+ name='ListHotlistItemsRequest',
+ full_name='monorail.v3.ListHotlistItemsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='parent', full_name='monorail.v3.ListHotlistItemsRequest.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=b'\340A\002\372A\027\n\025api.crbug.com/Hotlist', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='page_size', full_name='monorail.v3.ListHotlistItemsRequest.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.v3.ListHotlistItemsRequest.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='page_token', full_name='monorail.v3.ListHotlistItemsRequest.page_token', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=495,
+ serialized_end=624,
+)
+
+
+_LISTHOTLISTITEMSRESPONSE = _descriptor.Descriptor(
+ name='ListHotlistItemsResponse',
+ full_name='monorail.v3.ListHotlistItemsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='items', full_name='monorail.v3.ListHotlistItemsResponse.items', 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),
+ _descriptor.FieldDescriptor(
+ name='next_page_token', full_name='monorail.v3.ListHotlistItemsResponse.next_page_token', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=626,
+ serialized_end=718,
+)
+
+
+_RERANKHOTLISTITEMSREQUEST = _descriptor.Descriptor(
+ name='RerankHotlistItemsRequest',
+ full_name='monorail.v3.RerankHotlistItemsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.RerankHotlistItemsRequest.name', 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=b'\372A\027\n\025api.crbug.com/Hotlist\340A\002', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='hotlist_items', full_name='monorail.v3.RerankHotlistItemsRequest.hotlist_items', index=1,
+ number=2, 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=b'\372A\033\n\031api.crbug.com/HotlistItem\340A\002', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='target_position', full_name='monorail.v3.RerankHotlistItemsRequest.target_position', index=2,
+ number=3, type=13, cpp_type=3, 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=b'\340A\002', 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=721,
+ serialized_end=881,
+)
+
+
+_ADDHOTLISTITEMSREQUEST = _descriptor.Descriptor(
+ name='AddHotlistItemsRequest',
+ full_name='monorail.v3.AddHotlistItemsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='parent', full_name='monorail.v3.AddHotlistItemsRequest.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=b'\340A\002\372A\027\n\025api.crbug.com/Hotlist', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='issues', full_name='monorail.v3.AddHotlistItemsRequest.issues', index=1,
+ number=2, 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=b'\340A\002\372A\025\n\023api.crbug.com/Issue', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='target_position', full_name='monorail.v3.AddHotlistItemsRequest.target_position', index=2,
+ number=3, type=13, cpp_type=3, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=884,
+ serialized_end=1025,
+)
+
+
+_REMOVEHOTLISTITEMSREQUEST = _descriptor.Descriptor(
+ name='RemoveHotlistItemsRequest',
+ full_name='monorail.v3.RemoveHotlistItemsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='parent', full_name='monorail.v3.RemoveHotlistItemsRequest.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=b'\340A\002\372A\027\n\025api.crbug.com/Hotlist', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='issues', full_name='monorail.v3.RemoveHotlistItemsRequest.issues', index=1,
+ number=2, 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=b'\340A\002\372A\025\n\023api.crbug.com/Issue', 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=1027,
+ serialized_end=1146,
+)
+
+
+_REMOVEHOTLISTEDITORSREQUEST = _descriptor.Descriptor(
+ name='RemoveHotlistEditorsRequest',
+ full_name='monorail.v3.RemoveHotlistEditorsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.RemoveHotlistEditorsRequest.name', 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=b'\340A\002\372A\027\n\025api.crbug.com/Hotlist', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='editors', full_name='monorail.v3.RemoveHotlistEditorsRequest.editors', index=1,
+ number=2, 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=b'\340A\002\372A\024\n\022api.crbug.com/User', 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=1148,
+ serialized_end=1267,
+)
+
+
+_GATHERHOTLISTSFORUSERREQUEST = _descriptor.Descriptor(
+ name='GatherHotlistsForUserRequest',
+ full_name='monorail.v3.GatherHotlistsForUserRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='user', full_name='monorail.v3.GatherHotlistsForUserRequest.user', 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=b'\340A\002\372A\024\n\022api.crbug.com/User', 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=1269,
+ serialized_end=1341,
+)
+
+
+_GATHERHOTLISTSFORUSERRESPONSE = _descriptor.Descriptor(
+ name='GatherHotlistsForUserResponse',
+ full_name='monorail.v3.GatherHotlistsForUserResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='hotlists', full_name='monorail.v3.GatherHotlistsForUserResponse.hotlists', 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='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1343,
+ serialized_end=1414,
+)
+
+_CREATEHOTLISTREQUEST.fields_by_name['hotlist'].message_type = api_dot_v3_dot_api__proto_dot_feature__objects__pb2._HOTLIST
+_UPDATEHOTLISTREQUEST.fields_by_name['hotlist'].message_type = api_dot_v3_dot_api__proto_dot_feature__objects__pb2._HOTLIST
+_UPDATEHOTLISTREQUEST.fields_by_name['update_mask'].message_type = google_dot_protobuf_dot_field__mask__pb2._FIELDMASK
+_LISTHOTLISTITEMSRESPONSE.fields_by_name['items'].message_type = api_dot_v3_dot_api__proto_dot_feature__objects__pb2._HOTLISTITEM
+_GATHERHOTLISTSFORUSERRESPONSE.fields_by_name['hotlists'].message_type = api_dot_v3_dot_api__proto_dot_feature__objects__pb2._HOTLIST
+DESCRIPTOR.message_types_by_name['CreateHotlistRequest'] = _CREATEHOTLISTREQUEST
+DESCRIPTOR.message_types_by_name['GetHotlistRequest'] = _GETHOTLISTREQUEST
+DESCRIPTOR.message_types_by_name['UpdateHotlistRequest'] = _UPDATEHOTLISTREQUEST
+DESCRIPTOR.message_types_by_name['ListHotlistItemsRequest'] = _LISTHOTLISTITEMSREQUEST
+DESCRIPTOR.message_types_by_name['ListHotlistItemsResponse'] = _LISTHOTLISTITEMSRESPONSE
+DESCRIPTOR.message_types_by_name['RerankHotlistItemsRequest'] = _RERANKHOTLISTITEMSREQUEST
+DESCRIPTOR.message_types_by_name['AddHotlistItemsRequest'] = _ADDHOTLISTITEMSREQUEST
+DESCRIPTOR.message_types_by_name['RemoveHotlistItemsRequest'] = _REMOVEHOTLISTITEMSREQUEST
+DESCRIPTOR.message_types_by_name['RemoveHotlistEditorsRequest'] = _REMOVEHOTLISTEDITORSREQUEST
+DESCRIPTOR.message_types_by_name['GatherHotlistsForUserRequest'] = _GATHERHOTLISTSFORUSERREQUEST
+DESCRIPTOR.message_types_by_name['GatherHotlistsForUserResponse'] = _GATHERHOTLISTSFORUSERRESPONSE
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+CreateHotlistRequest = _reflection.GeneratedProtocolMessageType('CreateHotlistRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _CREATEHOTLISTREQUEST,
+ '__module__' : 'api.v3.api_proto.hotlists_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.CreateHotlistRequest)
+ })
+_sym_db.RegisterMessage(CreateHotlistRequest)
+
+GetHotlistRequest = _reflection.GeneratedProtocolMessageType('GetHotlistRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _GETHOTLISTREQUEST,
+ '__module__' : 'api.v3.api_proto.hotlists_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.GetHotlistRequest)
+ })
+_sym_db.RegisterMessage(GetHotlistRequest)
+
+UpdateHotlistRequest = _reflection.GeneratedProtocolMessageType('UpdateHotlistRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _UPDATEHOTLISTREQUEST,
+ '__module__' : 'api.v3.api_proto.hotlists_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.UpdateHotlistRequest)
+ })
+_sym_db.RegisterMessage(UpdateHotlistRequest)
+
+ListHotlistItemsRequest = _reflection.GeneratedProtocolMessageType('ListHotlistItemsRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _LISTHOTLISTITEMSREQUEST,
+ '__module__' : 'api.v3.api_proto.hotlists_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ListHotlistItemsRequest)
+ })
+_sym_db.RegisterMessage(ListHotlistItemsRequest)
+
+ListHotlistItemsResponse = _reflection.GeneratedProtocolMessageType('ListHotlistItemsResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _LISTHOTLISTITEMSRESPONSE,
+ '__module__' : 'api.v3.api_proto.hotlists_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ListHotlistItemsResponse)
+ })
+_sym_db.RegisterMessage(ListHotlistItemsResponse)
+
+RerankHotlistItemsRequest = _reflection.GeneratedProtocolMessageType('RerankHotlistItemsRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _RERANKHOTLISTITEMSREQUEST,
+ '__module__' : 'api.v3.api_proto.hotlists_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.RerankHotlistItemsRequest)
+ })
+_sym_db.RegisterMessage(RerankHotlistItemsRequest)
+
+AddHotlistItemsRequest = _reflection.GeneratedProtocolMessageType('AddHotlistItemsRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _ADDHOTLISTITEMSREQUEST,
+ '__module__' : 'api.v3.api_proto.hotlists_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.AddHotlistItemsRequest)
+ })
+_sym_db.RegisterMessage(AddHotlistItemsRequest)
+
+RemoveHotlistItemsRequest = _reflection.GeneratedProtocolMessageType('RemoveHotlistItemsRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _REMOVEHOTLISTITEMSREQUEST,
+ '__module__' : 'api.v3.api_proto.hotlists_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.RemoveHotlistItemsRequest)
+ })
+_sym_db.RegisterMessage(RemoveHotlistItemsRequest)
+
+RemoveHotlistEditorsRequest = _reflection.GeneratedProtocolMessageType('RemoveHotlistEditorsRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _REMOVEHOTLISTEDITORSREQUEST,
+ '__module__' : 'api.v3.api_proto.hotlists_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.RemoveHotlistEditorsRequest)
+ })
+_sym_db.RegisterMessage(RemoveHotlistEditorsRequest)
+
+GatherHotlistsForUserRequest = _reflection.GeneratedProtocolMessageType('GatherHotlistsForUserRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _GATHERHOTLISTSFORUSERREQUEST,
+ '__module__' : 'api.v3.api_proto.hotlists_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.GatherHotlistsForUserRequest)
+ })
+_sym_db.RegisterMessage(GatherHotlistsForUserRequest)
+
+GatherHotlistsForUserResponse = _reflection.GeneratedProtocolMessageType('GatherHotlistsForUserResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _GATHERHOTLISTSFORUSERRESPONSE,
+ '__module__' : 'api.v3.api_proto.hotlists_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.GatherHotlistsForUserResponse)
+ })
+_sym_db.RegisterMessage(GatherHotlistsForUserResponse)
+
+
+DESCRIPTOR._options = None
+_CREATEHOTLISTREQUEST.fields_by_name['hotlist']._options = None
+_GETHOTLISTREQUEST.fields_by_name['name']._options = None
+_UPDATEHOTLISTREQUEST.fields_by_name['hotlist']._options = None
+_UPDATEHOTLISTREQUEST.fields_by_name['update_mask']._options = None
+_LISTHOTLISTITEMSREQUEST.fields_by_name['parent']._options = None
+_RERANKHOTLISTITEMSREQUEST.fields_by_name['name']._options = None
+_RERANKHOTLISTITEMSREQUEST.fields_by_name['hotlist_items']._options = None
+_RERANKHOTLISTITEMSREQUEST.fields_by_name['target_position']._options = None
+_ADDHOTLISTITEMSREQUEST.fields_by_name['parent']._options = None
+_ADDHOTLISTITEMSREQUEST.fields_by_name['issues']._options = None
+_REMOVEHOTLISTITEMSREQUEST.fields_by_name['parent']._options = None
+_REMOVEHOTLISTITEMSREQUEST.fields_by_name['issues']._options = None
+_REMOVEHOTLISTEDITORSREQUEST.fields_by_name['name']._options = None
+_REMOVEHOTLISTEDITORSREQUEST.fields_by_name['editors']._options = None
+_GATHERHOTLISTSFORUSERREQUEST.fields_by_name['user']._options = None
+
+_HOTLISTS = _descriptor.ServiceDescriptor(
+ name='Hotlists',
+ full_name='monorail.v3.Hotlists',
+ file=DESCRIPTOR,
+ index=0,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ serialized_start=1417,
+ serialized_end=2287,
+ methods=[
+ _descriptor.MethodDescriptor(
+ name='CreateHotlist',
+ full_name='monorail.v3.Hotlists.CreateHotlist',
+ index=0,
+ containing_service=None,
+ input_type=_CREATEHOTLISTREQUEST,
+ output_type=api_dot_v3_dot_api__proto_dot_feature__objects__pb2._HOTLIST,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='GetHotlist',
+ full_name='monorail.v3.Hotlists.GetHotlist',
+ index=1,
+ containing_service=None,
+ input_type=_GETHOTLISTREQUEST,
+ output_type=api_dot_v3_dot_api__proto_dot_feature__objects__pb2._HOTLIST,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='UpdateHotlist',
+ full_name='monorail.v3.Hotlists.UpdateHotlist',
+ index=2,
+ containing_service=None,
+ input_type=_UPDATEHOTLISTREQUEST,
+ output_type=api_dot_v3_dot_api__proto_dot_feature__objects__pb2._HOTLIST,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='DeleteHotlist',
+ full_name='monorail.v3.Hotlists.DeleteHotlist',
+ index=3,
+ containing_service=None,
+ input_type=_GETHOTLISTREQUEST,
+ output_type=google_dot_protobuf_dot_empty__pb2._EMPTY,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListHotlistItems',
+ full_name='monorail.v3.Hotlists.ListHotlistItems',
+ index=4,
+ containing_service=None,
+ input_type=_LISTHOTLISTITEMSREQUEST,
+ output_type=_LISTHOTLISTITEMSRESPONSE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='RerankHotlistItems',
+ full_name='monorail.v3.Hotlists.RerankHotlistItems',
+ index=5,
+ containing_service=None,
+ input_type=_RERANKHOTLISTITEMSREQUEST,
+ output_type=google_dot_protobuf_dot_empty__pb2._EMPTY,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='AddHotlistItems',
+ full_name='monorail.v3.Hotlists.AddHotlistItems',
+ index=6,
+ containing_service=None,
+ input_type=_ADDHOTLISTITEMSREQUEST,
+ output_type=google_dot_protobuf_dot_empty__pb2._EMPTY,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='RemoveHotlistItems',
+ full_name='monorail.v3.Hotlists.RemoveHotlistItems',
+ index=7,
+ containing_service=None,
+ input_type=_REMOVEHOTLISTITEMSREQUEST,
+ output_type=google_dot_protobuf_dot_empty__pb2._EMPTY,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='RemoveHotlistEditors',
+ full_name='monorail.v3.Hotlists.RemoveHotlistEditors',
+ index=8,
+ containing_service=None,
+ input_type=_REMOVEHOTLISTEDITORSREQUEST,
+ output_type=google_dot_protobuf_dot_empty__pb2._EMPTY,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='GatherHotlistsForUser',
+ full_name='monorail.v3.Hotlists.GatherHotlistsForUser',
+ index=9,
+ containing_service=None,
+ input_type=_GATHERHOTLISTSFORUSERREQUEST,
+ output_type=_GATHERHOTLISTSFORUSERRESPONSE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+])
+_sym_db.RegisterServiceDescriptor(_HOTLISTS)
+
+DESCRIPTOR.services_by_name['Hotlists'] = _HOTLISTS
+
+# @@protoc_insertion_point(module_scope)
diff --git a/api/v3/api_proto/hotlists_prpc_pb2.py b/api/v3/api_proto/hotlists_prpc_pb2.py
new file mode 100644
index 0000000..bce36ac
--- /dev/null
+++ b/api/v3/api_proto/hotlists_prpc_pb2.py
@@ -0,0 +1,788 @@
+# Generated by the pRPC protocol buffer compiler plugin. DO NOT EDIT!
+# source: api/v3/api_proto/hotlists.proto
+
+import base64
+import zlib
+
+from google.protobuf import descriptor_pb2
+
+# Includes description of the api/v3/api_proto/hotlists.proto and all of its transitive
+# dependencies. Includes source code info.
+FILE_DESCRIPTOR_SET = descriptor_pb2.FileDescriptorSet()
+FILE_DESCRIPTOR_SET.ParseFromString(zlib.decompress(base64.b64decode(
+ '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')))
+_INDEX = {
+ f.name: {
+ 'descriptor': f,
+ 'services': {s.name: s for s in f.service},
+ }
+ for f in FILE_DESCRIPTOR_SET.file
+}
+
+
+HotlistsServiceDescription = {
+ 'file_descriptor_set': FILE_DESCRIPTOR_SET,
+ 'file_descriptor': _INDEX[u'api/v3/api_proto/hotlists.proto']['descriptor'],
+ 'service_descriptor': _INDEX[u'api/v3/api_proto/hotlists.proto']['services'][u'Hotlists'],
+}
diff --git a/api/v3/api_proto/issue_objects.proto b/api/v3/api_proto/issue_objects.proto
new file mode 100644
index 0000000..2c5cf69
--- /dev/null
+++ b/api/v3/api_proto/issue_objects.proto
@@ -0,0 +1,349 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+// This file defines protobufs for issues and related business
+// objects, e.g., field values, comments, and attachments.
+
+syntax = "proto3";
+
+package monorail.v3;
+
+option go_package = "api/v3/api_proto";
+
+import "google/api/field_behavior.proto";
+import "google/api/resource.proto";
+import "google/protobuf/timestamp.proto";
+
+// Represents a comment and any associated changes to an Issue.
+//
+// Comments cannot be Created or Updated through standard methods. The
+// OUTPUT_ONLY annotations here indicate fields that would never be provided
+// by the user even if these methods were made available.
+// Next available tag: 11.
+message Comment {
+
+ // The type of comment.
+ // Next available tag: 9
+ enum Type {
+ // The default comment type. Used if type is omitted.
+ UNSPECIFIED = 0;
+ // A standard comment on an issue.
+ COMMENT = 1;
+ // A comment representing a new description for the issue.
+ DESCRIPTION = 2;
+ }
+
+ // A file attached to a comment.
+ // Next available tag: 8
+ message Attachment {
+ // The name of the attached file.
+ string filename = 1;
+ // It is possible for attachments to be deleted (and undeleted) by the
+ // uploader. The name of deleted attachments are still shown, but the
+ // content is not available.
+ IssueContentState state = 2;
+ // Size of the attached file in bytes.
+ uint64 size = 3;
+ // The type of content contained in the file, using the IANA's media type
+ // https://www.iana.org/assignments/media-types/media-types.xhtml.
+ string media_type = 4;
+ // The URI used for a preview of the attachment (when relelvant).
+ string thumbnail_uri = 5;
+ // The URI used to view the content of the attachment.
+ string view_uri = 6;
+ // The URI used to download the content of the attachment.
+ string download_uri = 7;
+ }
+
+ // This message is only suitable for displaying the amendment to users.
+ // We don't currently offer structured amendments that client code can
+ // reason about, field names can be ambiguous, and we don't have
+ // old_value for most changes.
+ // Next available tag: 4
+ message Amendment {
+ // This may be the name of a built-in or custom field, or relative to
+ // an approval field name.
+ string field_name = 1;
+ // This may be a new value that overwrote the old value, e.g., "Assigned",
+ // or it may be a space-separated list of changes, e.g., "Size-L -Size-S".
+ string new_or_delta_value = 2;
+ // old_value is only used when the user changes the summary.
+ string old_value = 3;
+ }
+
+ option (google.api.resource) = {
+ type: "api.crbug.com/Comment"
+ pattern: "projects/{project}/issues/{issue}/comments/{comment}"
+ };
+
+ // Resource name of the comment.
+ string name = 1;
+ // The state of the comment.
+ IssueContentState state = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
+ // The type of comment.
+ Type type = 3;
+ // The text of the comment.
+ string content = 4;
+ // Resource name of the author of the comment.
+ string commenter = 5 [
+ (google.api.resource_reference) = { type: "api.crbug.com/User" },
+ (google.api.field_behavior) = OUTPUT_ONLY
+ ];
+ // The time this comment was added to the Issue.
+ google.protobuf.Timestamp create_time = 6
+ [(google.api.field_behavior) = OUTPUT_ONLY];
+ // Optional string full text of an email that caused this comment to be added.
+ string inbound_message = 7 [(google.api.field_behavior) = OUTPUT_ONLY];
+ // The approval this comment is associated with, if applicable.
+ string approval = 8
+ [(google.api.resource_reference) = { type: "api.crbug.com/ApprovalValue" }];
+ // Any changes made to the issue in association with this comment.
+ repeated Amendment amendments = 9 [(google.api.field_behavior) = OUTPUT_ONLY];
+ // Any attachments uploaded in association with this comment.
+ repeated Attachment attachments = 10
+ [(google.api.field_behavior) = OUTPUT_ONLY];
+}
+
+
+// Many values on an issue can be set either explicitly or by a rule.
+//
+// Note: Though Derivations are used as OUTPUT_ONLY, values including them
+// will still be ingested even though the Derivation is ignored.
+//
+// Next available tag: 3
+enum Derivation {
+ // The default derivation. This value is used if the derivation is omitted.
+ DERIVATION_UNSPECIFIED = 0;
+ // The value was explicitly set on the issue.
+ EXPLICIT = 1;
+ // Value was auto-applied to the issue based on a project's rule. See
+ // monorail/doc/userguide/project-owners.md#how-to-configure-filter-rules
+ RULE = 2;
+}
+
+
+// A value of a custom field for an issue.
+// Next available tag: 5
+message FieldValue {
+ // The project-defined field associated with this value
+ string field = 1 [
+ (google.api.resource_reference) = { type: "api.crbug.com/FieldDef" }];
+ // The value associated with the field.
+ // Mapping of field types to string value:
+ // ENUM_TYPE(int) => str(value)
+ // INT_TYPE(int) => str(value)
+ // STR_TYPE(str) => value
+ // USER_TYPE(int) => the user's resource name
+ // DATE_TYPE(int) => str(int) representing time in seconds since epoch
+ // URL_TYPE(str) => value
+ string value = 2;
+ // How the value was derived.
+ Derivation derivation = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
+ // Issues with phase-specific fields can have values for each phase.
+ string phase = 4;
+}
+
+// Documents and tracks a bug, task, or feature request within a Project.
+// Next available tag: 23
+message Issue {
+ option (google.api.resource) = {
+ type: "api.crbug.com/Issue"
+ pattern: "projects/{project}/issues/{issue}"
+ };
+
+ // A possibly rule-derived component for the issue.
+ // Next available tag: 3
+ message ComponentValue {
+ // The component.
+ string component = 1 [
+ (google.api.resource_reference) = { type: "api.crbug.com/ComponentDef" }
+ ];
+ // How the component was derived.
+ Derivation derivation = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
+ }
+
+ // A possibly rule-derived label for an issue.
+ // Next available tag: 3
+ message LabelValue {
+ // The label.
+ string label = 1;
+ // How the label was derived.
+ Derivation derivation = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
+ }
+
+ // A possibly rule-derived status for an issue.
+ // Next available tag: 3
+ message StatusValue {
+ // The status of the issue. Note that in rare cases this can be a
+ // value not defined in the project's StatusDefs (e.g. if the issue
+ // was moved from another project).
+ string status = 1;
+ // How the status was derived.
+ Derivation derivation = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
+ }
+
+ // A possibly rule-derived user value on an issue.
+ // Next available tag: 3
+ message UserValue {
+ // The user.
+ string user = 1
+ [(google.api.resource_reference) = { type: "api.crbug.com/User" }];
+ // How the user value was derived.
+ Derivation derivation = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
+ }
+
+ // Resource name of the issue.
+ string name = 1;
+ // A brief summary of the issue. Generally displayed as a user-facing title.
+ // TODO(monorail:6988): The UI limits summary length while the backend does
+ // not. Resolve this discrepancy.
+ string summary = 2;
+ // The state of the issue.
+ IssueContentState state = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
+ // The current status of the issue.
+ StatusValue status = 4 [(google.api.field_behavior) = REQUIRED];
+ // The user who created the issue.
+ string reporter = 5 [
+ (google.api.resource_reference) = { type: "api.crbug.com/User" },
+ (google.api.field_behavior) = OUTPUT_ONLY
+ ];
+ // The user currently responsible for the issue. This user must be a member of
+ // the Project.
+ UserValue owner = 6;
+ // Additional users receiving notifications on the issue.
+ repeated UserValue cc_users = 7;
+ // Labels applied to the issue.
+ repeated LabelValue labels = 8;
+ // Components the issue is associated with.
+ repeated ComponentValue components = 9;
+ // Values for custom fields on the issue.
+ repeated FieldValue field_values = 10;
+ // An issue can be merged into another. If this value is set, the issue
+ // referred to should be considered the primary source for further updates.
+ IssueRef merged_into_issue_ref = 11;
+ // Issues preventing the completion of this issue.
+ repeated IssueRef blocked_on_issue_refs = 12;
+ // Issues for which this issue is blocking completion.
+ repeated IssueRef blocking_issue_refs = 13;
+ // The time the issue was reported.
+ google.protobuf.Timestamp create_time = 14
+ [(google.api.field_behavior) = OUTPUT_ONLY];
+ // The most recent time the issue was closed.
+ google.protobuf.Timestamp close_time = 15
+ [(google.api.field_behavior) = OUTPUT_ONLY];
+ // The most recent time the issue was modified.
+ google.protobuf.Timestamp modify_time = 16
+ [(google.api.field_behavior) = OUTPUT_ONLY];
+ // The most recent time a component value was modified.
+ google.protobuf.Timestamp component_modify_time = 17
+ [(google.api.field_behavior) = OUTPUT_ONLY];
+ // The most recent time the status value was modified.
+ google.protobuf.Timestamp status_modify_time = 18
+ [(google.api.field_behavior) = OUTPUT_ONLY];
+ // The most recent time the owner made a modification to the issue.
+ google.protobuf.Timestamp owner_modify_time = 19
+ [(google.api.field_behavior) = OUTPUT_ONLY];
+ // The number of attachments associated with the issue.
+ uint32 attachment_count = 20 [(google.api.field_behavior) = OUTPUT_ONLY];
+ // The number of users who have starred the issue.
+ uint32 star_count = 21 [(google.api.field_behavior) = OUTPUT_ONLY];
+ // Phases of a process the issue is tracking (if applicable).
+ // See monorail/doc/userguide/concepts.md#issue-approvals-and-gates
+ repeated string phases = 22 [
+ (google.api.field_behavior) = OUTPUT_ONLY];
+}
+
+// States that an issue or its comments can be in (aip.dev/216).
+// Next available tag: 4
+enum IssueContentState {
+ // The default value. This value is used if the state is omitted.
+ STATE_UNSPECIFIED = 0;
+ // The Issue or Comment is available.
+ ACTIVE = 1;
+ // The Issue or Comment has been deleted.
+ DELETED = 2;
+ // The Issue or Comment has been flagged as spam.
+ // Takes precedent over DELETED.
+ SPAM = 3;
+}
+
+// Specifies a column in an issues list view.
+// Next available tag: 2
+message IssuesListColumn {
+ // Column name shown in the column header.
+ string column = 1;
+}
+
+// Refers to an issue that may or may not be tracked in Monorail.
+// At least one of `issue` and `ext_identifier` MUST be set; they MUST NOT both
+// be set.
+// Next available tag: 3
+message IssueRef {
+ // Resource name of an issue tracked in Monorail
+ string issue = 1
+ [(google.api.resource_reference) = { type: "api.crbug.com/Issue" }];
+ // For referencing external issues, e.g. b/1234, or a dangling reference
+ // to an old 'codesite' issue.
+ // TODO(monorail:7208): add more documentation on dangling references.
+ string ext_identifier = 2;
+}
+
+// Documents and tracks an approval process.
+// See monorail/doc/userguide/concepts.md#issue-approvals-and-gates
+// Next available tag: 9
+message ApprovalValue {
+ option (google.api.resource) = {
+ type: "api.crbug.com/ApprovalValue"
+ pattern: "projects/{project}/issues/{issue}/approvalValues/{approval}"
+ };
+
+ // Potential states for an approval. Note that these statuses cause different
+ // sets of notifications. See monorail/doc/userguide/email.md
+ // Next available tag: 9
+ enum ApprovalStatus {
+ // The default approval status. This value is used if the status is omitted.
+ APPROVAL_STATUS_UNSPECIFIED = 0;
+ // No status has yet been set on this value.
+ NOT_SET = 1;
+ // This issue needs review from the approvers for this phase.
+ NEEDS_REVIEW = 2;
+ // This approval is not needed for this issue for this phase.
+ NA = 3;
+ // The issue is ready for the approvers to review.
+ REVIEW_REQUESTED = 4;
+ // The approvers have started reviewing this issue.
+ REVIEW_STARTED = 5;
+ // The approvers need more information.
+ NEED_INFO = 6;
+ // The approvers have approved this issue for this phase.
+ APPROVED = 7;
+ // The approvers have indicated this issue is not approved for this phase.
+ NOT_APPROVED = 8;
+ }
+
+ // The resource name.
+ string name = 1;
+ // The resource name of the ApprovalDef.
+ string approval_def = 2 [
+ (google.api.resource_reference) = { type: "api.crbug.com/ApprovalDef" },
+ (google.api.field_behavior) = OUTPUT_ONLY];
+ // The users able to grant this approval.
+ repeated string approvers = 3 [
+ (google.api.resource_reference) = { type: "api.crbug.com/User" }];
+ // The current status of the approval.
+ ApprovalStatus status = 4;
+ // The time `status` was last set.
+ google.protobuf.Timestamp set_time = 5 [
+ (google.api.field_behavior) = OUTPUT_ONLY];
+ // The user who most recently set `status`.
+ string setter = 6 [
+ (google.api.resource_reference) = { type: "api.crbug.com/User" },
+ (google.api.field_behavior) = OUTPUT_ONLY];
+ // The phase the approval is associated with (if applicable).
+ string phase = 7 [
+ (google.api.field_behavior) = OUTPUT_ONLY];
+ // FieldValues with `approval_def` as their parent.
+ repeated FieldValue field_values = 8;
+}
\ No newline at end of file
diff --git a/api/v3/api_proto/issue_objects_pb2.py b/api/v3/api_proto/issue_objects_pb2.py
new file mode 100644
index 0000000..1644b3c
--- /dev/null
+++ b/api/v3/api_proto/issue_objects_pb2.py
@@ -0,0 +1,1123 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/v3/api_proto/issue_objects.proto
+
+from google.protobuf.internal import enum_type_wrapper
+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()
+
+
+from google.api import field_behavior_pb2 as google_dot_api_dot_field__behavior__pb2
+from google.api import resource_pb2 as google_dot_api_dot_resource__pb2
+from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='api/v3/api_proto/issue_objects.proto',
+ package='monorail.v3',
+ syntax='proto3',
+ serialized_options=b'Z\020api/v3/api_proto',
+ create_key=_descriptor._internal_create_key,
+ serialized_pb=b'\n$api/v3/api_proto/issue_objects.proto\x12\x0bmonorail.v3\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xba\x06\n\x07\x43omment\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x32\n\x05state\x18\x02 \x01(\x0e\x32\x1e.monorail.v3.IssueContentStateB\x03\xe0\x41\x03\x12\'\n\x04type\x18\x03 \x01(\x0e\x32\x19.monorail.v3.Comment.Type\x12\x0f\n\x07\x63ontent\x18\x04 \x01(\t\x12-\n\tcommenter\x18\x05 \x01(\tB\x1a\xfa\x41\x14\n\x12\x61pi.crbug.com/User\xe0\x41\x03\x12\x34\n\x0b\x63reate_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03\x12\x1c\n\x0finbound_message\x18\x07 \x01(\tB\x03\xe0\x41\x03\x12\x32\n\x08\x61pproval\x18\x08 \x01(\tB \xfa\x41\x1d\n\x1b\x61pi.crbug.com/ApprovalValue\x12\x37\n\namendments\x18\t \x03(\x0b\x32\x1e.monorail.v3.Comment.AmendmentB\x03\xe0\x41\x03\x12\x39\n\x0b\x61ttachments\x18\n \x03(\x0b\x32\x1f.monorail.v3.Comment.AttachmentB\x03\xe0\x41\x03\x1a\xae\x01\n\nAttachment\x12\x10\n\x08\x66ilename\x18\x01 \x01(\t\x12-\n\x05state\x18\x02 \x01(\x0e\x32\x1e.monorail.v3.IssueContentState\x12\x0c\n\x04size\x18\x03 \x01(\x04\x12\x12\n\nmedia_type\x18\x04 \x01(\t\x12\x15\n\rthumbnail_uri\x18\x05 \x01(\t\x12\x10\n\x08view_uri\x18\x06 \x01(\t\x12\x14\n\x0c\x64ownload_uri\x18\x07 \x01(\t\x1aN\n\tAmendment\x12\x12\n\nfield_name\x18\x01 \x01(\t\x12\x1a\n\x12new_or_delta_value\x18\x02 \x01(\t\x12\x11\n\told_value\x18\x03 \x01(\t\"5\n\x04Type\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0b\n\x07\x43OMMENT\x10\x01\x12\x0f\n\x0b\x44\x45SCRIPTION\x10\x02:P\xea\x41M\n\x15\x61pi.crbug.com/Comment\x12\x34projects/{project}/issues/{issue}/comments/{comment}\"\x88\x01\n\nFieldValue\x12*\n\x05\x66ield\x18\x01 \x01(\tB\x1b\xfa\x41\x18\n\x16\x61pi.crbug.com/FieldDef\x12\r\n\x05value\x18\x02 \x01(\t\x12\x30\n\nderivation\x18\x03 \x01(\x0e\x32\x17.monorail.v3.DerivationB\x03\xe0\x41\x03\x12\r\n\x05phase\x18\x04 \x01(\t\"\xb1\x0b\n\x05Issue\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07summary\x18\x02 \x01(\t\x12\x32\n\x05state\x18\x03 \x01(\x0e\x32\x1e.monorail.v3.IssueContentStateB\x03\xe0\x41\x03\x12\x33\n\x06status\x18\x04 \x01(\x0b\x32\x1e.monorail.v3.Issue.StatusValueB\x03\xe0\x41\x02\x12,\n\x08reporter\x18\x05 \x01(\tB\x1a\xfa\x41\x14\n\x12\x61pi.crbug.com/User\xe0\x41\x03\x12+\n\x05owner\x18\x06 \x01(\x0b\x32\x1c.monorail.v3.Issue.UserValue\x12.\n\x08\x63\x63_users\x18\x07 \x03(\x0b\x32\x1c.monorail.v3.Issue.UserValue\x12-\n\x06labels\x18\x08 \x03(\x0b\x32\x1d.monorail.v3.Issue.LabelValue\x12\x35\n\ncomponents\x18\t \x03(\x0b\x32!.monorail.v3.Issue.ComponentValue\x12-\n\x0c\x66ield_values\x18\n \x03(\x0b\x32\x17.monorail.v3.FieldValue\x12\x34\n\x15merged_into_issue_ref\x18\x0b \x01(\x0b\x32\x15.monorail.v3.IssueRef\x12\x34\n\x15\x62locked_on_issue_refs\x18\x0c \x03(\x0b\x32\x15.monorail.v3.IssueRef\x12\x32\n\x13\x62locking_issue_refs\x18\r \x03(\x0b\x32\x15.monorail.v3.IssueRef\x12\x34\n\x0b\x63reate_time\x18\x0e \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03\x12\x33\n\nclose_time\x18\x0f \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03\x12\x34\n\x0bmodify_time\x18\x10 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03\x12>\n\x15\x63omponent_modify_time\x18\x11 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03\x12;\n\x12status_modify_time\x18\x12 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03\x12:\n\x11owner_modify_time\x18\x13 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03\x12\x1d\n\x10\x61ttachment_count\x18\x14 \x01(\rB\x03\xe0\x41\x03\x12\x17\n\nstar_count\x18\x15 \x01(\rB\x03\xe0\x41\x03\x12\x13\n\x06phases\x18\x16 \x03(\tB\x03\xe0\x41\x03\x1av\n\x0e\x43omponentValue\x12\x32\n\tcomponent\x18\x01 \x01(\tB\x1f\xfa\x41\x1c\n\x1a\x61pi.crbug.com/ComponentDef\x12\x30\n\nderivation\x18\x02 \x01(\x0e\x32\x17.monorail.v3.DerivationB\x03\xe0\x41\x03\x1aM\n\nLabelValue\x12\r\n\x05label\x18\x01 \x01(\t\x12\x30\n\nderivation\x18\x02 \x01(\x0e\x32\x17.monorail.v3.DerivationB\x03\xe0\x41\x03\x1aO\n\x0bStatusValue\x12\x0e\n\x06status\x18\x01 \x01(\t\x12\x30\n\nderivation\x18\x02 \x01(\x0e\x32\x17.monorail.v3.DerivationB\x03\xe0\x41\x03\x1a\x64\n\tUserValue\x12%\n\x04user\x18\x01 \x01(\tB\x17\xfa\x41\x14\n\x12\x61pi.crbug.com/User\x12\x30\n\nderivation\x18\x02 \x01(\x0e\x32\x17.monorail.v3.DerivationB\x03\xe0\x41\x03:;\xea\x41\x38\n\x13\x61pi.crbug.com/Issue\x12!projects/{project}/issues/{issue}\"\"\n\x10IssuesListColumn\x12\x0e\n\x06\x63olumn\x18\x01 \x01(\t\"K\n\x08IssueRef\x12\'\n\x05issue\x18\x01 \x01(\tB\x18\xfa\x41\x15\n\x13\x61pi.crbug.com/Issue\x12\x16\n\x0e\x65xt_identifier\x18\x02 \x01(\t\"\xf2\x04\n\rApprovalValue\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x37\n\x0c\x61pproval_def\x18\x02 \x01(\tB!\xfa\x41\x1b\n\x19\x61pi.crbug.com/ApprovalDef\xe0\x41\x03\x12*\n\tapprovers\x18\x03 \x03(\tB\x17\xfa\x41\x14\n\x12\x61pi.crbug.com/User\x12\x39\n\x06status\x18\x04 \x01(\x0e\x32).monorail.v3.ApprovalValue.ApprovalStatus\x12\x31\n\x08set_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03\x12*\n\x06setter\x18\x06 \x01(\tB\x1a\xfa\x41\x14\n\x12\x61pi.crbug.com/User\xe0\x41\x03\x12\x12\n\x05phase\x18\x07 \x01(\tB\x03\xe0\x41\x03\x12-\n\x0c\x66ield_values\x18\x08 \x03(\x0b\x32\x17.monorail.v3.FieldValue\"\xb1\x01\n\x0e\x41pprovalStatus\x12\x1f\n\x1b\x41PPROVAL_STATUS_UNSPECIFIED\x10\x00\x12\x0b\n\x07NOT_SET\x10\x01\x12\x10\n\x0cNEEDS_REVIEW\x10\x02\x12\x06\n\x02NA\x10\x03\x12\x14\n\x10REVIEW_REQUESTED\x10\x04\x12\x12\n\x0eREVIEW_STARTED\x10\x05\x12\r\n\tNEED_INFO\x10\x06\x12\x0c\n\x08\x41PPROVED\x10\x07\x12\x10\n\x0cNOT_APPROVED\x10\x08:]\xea\x41Z\n\x1b\x61pi.crbug.com/ApprovalValue\x12;projects/{project}/issues/{issue}/approvalValues/{approval}*@\n\nDerivation\x12\x1a\n\x16\x44\x45RIVATION_UNSPECIFIED\x10\x00\x12\x0c\n\x08\x45XPLICIT\x10\x01\x12\x08\n\x04RULE\x10\x02*M\n\x11IssueContentState\x12\x15\n\x11STATE_UNSPECIFIED\x10\x00\x12\n\n\x06\x41\x43TIVE\x10\x01\x12\x0b\n\x07\x44\x45LETED\x10\x02\x12\x08\n\x04SPAM\x10\x03\x42\x12Z\x10\x61pi/v3/api_protob\x06proto3'
+ ,
+ dependencies=[google_dot_api_dot_field__behavior__pb2.DESCRIPTOR,google_dot_api_dot_resource__pb2.DESCRIPTOR,google_dot_protobuf_dot_timestamp__pb2.DESCRIPTOR,])
+
+_DERIVATION = _descriptor.EnumDescriptor(
+ name='Derivation',
+ full_name='monorail.v3.Derivation',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='DERIVATION_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='EXPLICIT', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='RULE', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=3316,
+ serialized_end=3380,
+)
+_sym_db.RegisterEnumDescriptor(_DERIVATION)
+
+Derivation = enum_type_wrapper.EnumTypeWrapper(_DERIVATION)
+_ISSUECONTENTSTATE = _descriptor.EnumDescriptor(
+ name='IssueContentState',
+ full_name='monorail.v3.IssueContentState',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='STATE_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='ACTIVE', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='DELETED', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='SPAM', index=3, number=3,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=3382,
+ serialized_end=3459,
+)
+_sym_db.RegisterEnumDescriptor(_ISSUECONTENTSTATE)
+
+IssueContentState = enum_type_wrapper.EnumTypeWrapper(_ISSUECONTENTSTATE)
+DERIVATION_UNSPECIFIED = 0
+EXPLICIT = 1
+RULE = 2
+STATE_UNSPECIFIED = 0
+ACTIVE = 1
+DELETED = 2
+SPAM = 3
+
+
+_COMMENT_TYPE = _descriptor.EnumDescriptor(
+ name='Type',
+ full_name='monorail.v3.Comment.Type',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='COMMENT', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='DESCRIPTION', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=838,
+ serialized_end=891,
+)
+_sym_db.RegisterEnumDescriptor(_COMMENT_TYPE)
+
+_APPROVALVALUE_APPROVALSTATUS = _descriptor.EnumDescriptor(
+ name='ApprovalStatus',
+ full_name='monorail.v3.ApprovalValue.ApprovalStatus',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='APPROVAL_STATUS_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='NOT_SET', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='NEEDS_REVIEW', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='NA', index=3, number=3,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='REVIEW_REQUESTED', index=4, number=4,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='REVIEW_STARTED', index=5, number=5,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='NEED_INFO', index=6, number=6,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='APPROVED', index=7, number=7,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='NOT_APPROVED', index=8, number=8,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=3042,
+ serialized_end=3219,
+)
+_sym_db.RegisterEnumDescriptor(_APPROVALVALUE_APPROVALSTATUS)
+
+
+_COMMENT_ATTACHMENT = _descriptor.Descriptor(
+ name='Attachment',
+ full_name='monorail.v3.Comment.Attachment',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='filename', full_name='monorail.v3.Comment.Attachment.filename', 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='state', full_name='monorail.v3.Comment.Attachment.state', index=1,
+ number=2, type=14, cpp_type=8, 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='size', full_name='monorail.v3.Comment.Attachment.size', index=2,
+ number=3, type=4, cpp_type=4, 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='media_type', full_name='monorail.v3.Comment.Attachment.media_type', 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='thumbnail_uri', full_name='monorail.v3.Comment.Attachment.thumbnail_uri', index=4,
+ number=5, 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='view_uri', full_name='monorail.v3.Comment.Attachment.view_uri', 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),
+ _descriptor.FieldDescriptor(
+ name='download_uri', full_name='monorail.v3.Comment.Attachment.download_uri', index=6,
+ number=7, 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=582,
+ serialized_end=756,
+)
+
+_COMMENT_AMENDMENT = _descriptor.Descriptor(
+ name='Amendment',
+ full_name='monorail.v3.Comment.Amendment',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='field_name', full_name='monorail.v3.Comment.Amendment.field_name', 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='new_or_delta_value', full_name='monorail.v3.Comment.Amendment.new_or_delta_value', 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='old_value', full_name='monorail.v3.Comment.Amendment.old_value', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=758,
+ serialized_end=836,
+)
+
+_COMMENT = _descriptor.Descriptor(
+ name='Comment',
+ full_name='monorail.v3.Comment',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.Comment.name', 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='state', full_name='monorail.v3.Comment.state', index=1,
+ number=2, type=14, cpp_type=8, 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=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='type', full_name='monorail.v3.Comment.type', index=2,
+ number=3, type=14, cpp_type=8, 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='content', full_name='monorail.v3.Comment.content', 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='commenter', full_name='monorail.v3.Comment.commenter', index=4,
+ number=5, 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=b'\372A\024\n\022api.crbug.com/User\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='create_time', full_name='monorail.v3.Comment.create_time', index=5,
+ number=6, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='inbound_message', full_name='monorail.v3.Comment.inbound_message', index=6,
+ number=7, 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=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='approval', full_name='monorail.v3.Comment.approval', index=7,
+ number=8, 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=b'\372A\035\n\033api.crbug.com/ApprovalValue', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='amendments', full_name='monorail.v3.Comment.amendments', index=8,
+ number=9, 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=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='attachments', full_name='monorail.v3.Comment.attachments', index=9,
+ number=10, 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=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ ],
+ extensions=[
+ ],
+ nested_types=[_COMMENT_ATTACHMENT, _COMMENT_AMENDMENT, ],
+ enum_types=[
+ _COMMENT_TYPE,
+ ],
+ serialized_options=b'\352AM\n\025api.crbug.com/Comment\0224projects/{project}/issues/{issue}/comments/{comment}',
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=147,
+ serialized_end=973,
+)
+
+
+_FIELDVALUE = _descriptor.Descriptor(
+ name='FieldValue',
+ full_name='monorail.v3.FieldValue',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='field', full_name='monorail.v3.FieldValue.field', 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=b'\372A\030\n\026api.crbug.com/FieldDef', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='value', full_name='monorail.v3.FieldValue.value', 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='derivation', full_name='monorail.v3.FieldValue.derivation', index=2,
+ number=3, type=14, cpp_type=8, 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=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='phase', full_name='monorail.v3.FieldValue.phase', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=976,
+ serialized_end=1112,
+)
+
+
+_ISSUE_COMPONENTVALUE = _descriptor.Descriptor(
+ name='ComponentValue',
+ full_name='monorail.v3.Issue.ComponentValue',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='component', full_name='monorail.v3.Issue.ComponentValue.component', 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=b'\372A\034\n\032api.crbug.com/ComponentDef', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='derivation', full_name='monorail.v3.Issue.ComponentValue.derivation', index=1,
+ number=2, type=14, cpp_type=8, 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=b'\340A\003', 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=2131,
+ serialized_end=2249,
+)
+
+_ISSUE_LABELVALUE = _descriptor.Descriptor(
+ name='LabelValue',
+ full_name='monorail.v3.Issue.LabelValue',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='label', full_name='monorail.v3.Issue.LabelValue.label', 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='derivation', full_name='monorail.v3.Issue.LabelValue.derivation', index=1,
+ number=2, type=14, cpp_type=8, 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=b'\340A\003', 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=2251,
+ serialized_end=2328,
+)
+
+_ISSUE_STATUSVALUE = _descriptor.Descriptor(
+ name='StatusValue',
+ full_name='monorail.v3.Issue.StatusValue',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='status', full_name='monorail.v3.Issue.StatusValue.status', 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='derivation', full_name='monorail.v3.Issue.StatusValue.derivation', index=1,
+ number=2, type=14, cpp_type=8, 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=b'\340A\003', 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=2330,
+ serialized_end=2409,
+)
+
+_ISSUE_USERVALUE = _descriptor.Descriptor(
+ name='UserValue',
+ full_name='monorail.v3.Issue.UserValue',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='user', full_name='monorail.v3.Issue.UserValue.user', 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=b'\372A\024\n\022api.crbug.com/User', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='derivation', full_name='monorail.v3.Issue.UserValue.derivation', index=1,
+ number=2, type=14, cpp_type=8, 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=b'\340A\003', 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=2411,
+ serialized_end=2511,
+)
+
+_ISSUE = _descriptor.Descriptor(
+ name='Issue',
+ full_name='monorail.v3.Issue',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.Issue.name', 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='summary', full_name='monorail.v3.Issue.summary', 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='state', full_name='monorail.v3.Issue.state', index=2,
+ number=3, type=14, cpp_type=8, 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=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='status', full_name='monorail.v3.Issue.status', index=3,
+ number=4, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\002', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='reporter', full_name='monorail.v3.Issue.reporter', index=4,
+ number=5, 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=b'\372A\024\n\022api.crbug.com/User\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='owner', full_name='monorail.v3.Issue.owner', index=5,
+ number=6, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ 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='cc_users', full_name='monorail.v3.Issue.cc_users', index=6,
+ number=7, 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='labels', full_name='monorail.v3.Issue.labels', index=7,
+ number=8, 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='components', full_name='monorail.v3.Issue.components', index=8,
+ number=9, 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='field_values', full_name='monorail.v3.Issue.field_values', index=9,
+ number=10, 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='merged_into_issue_ref', full_name='monorail.v3.Issue.merged_into_issue_ref', index=10,
+ number=11, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ 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='blocked_on_issue_refs', full_name='monorail.v3.Issue.blocked_on_issue_refs', index=11,
+ number=12, 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='blocking_issue_refs', full_name='monorail.v3.Issue.blocking_issue_refs', index=12,
+ number=13, 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='create_time', full_name='monorail.v3.Issue.create_time', index=13,
+ number=14, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='close_time', full_name='monorail.v3.Issue.close_time', index=14,
+ number=15, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='modify_time', full_name='monorail.v3.Issue.modify_time', index=15,
+ number=16, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='component_modify_time', full_name='monorail.v3.Issue.component_modify_time', index=16,
+ number=17, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='status_modify_time', full_name='monorail.v3.Issue.status_modify_time', index=17,
+ number=18, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='owner_modify_time', full_name='monorail.v3.Issue.owner_modify_time', index=18,
+ number=19, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='attachment_count', full_name='monorail.v3.Issue.attachment_count', index=19,
+ number=20, type=13, cpp_type=3, 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=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='star_count', full_name='monorail.v3.Issue.star_count', index=20,
+ number=21, type=13, cpp_type=3, 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=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='phases', full_name='monorail.v3.Issue.phases', index=21,
+ number=22, 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=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ ],
+ extensions=[
+ ],
+ nested_types=[_ISSUE_COMPONENTVALUE, _ISSUE_LABELVALUE, _ISSUE_STATUSVALUE, _ISSUE_USERVALUE, ],
+ enum_types=[
+ ],
+ serialized_options=b'\352A8\n\023api.crbug.com/Issue\022!projects/{project}/issues/{issue}',
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1115,
+ serialized_end=2572,
+)
+
+
+_ISSUESLISTCOLUMN = _descriptor.Descriptor(
+ name='IssuesListColumn',
+ full_name='monorail.v3.IssuesListColumn',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='column', full_name='monorail.v3.IssuesListColumn.column', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2574,
+ serialized_end=2608,
+)
+
+
+_ISSUEREF = _descriptor.Descriptor(
+ name='IssueRef',
+ full_name='monorail.v3.IssueRef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue', full_name='monorail.v3.IssueRef.issue', 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=b'\372A\025\n\023api.crbug.com/Issue', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='ext_identifier', full_name='monorail.v3.IssueRef.ext_identifier', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2610,
+ serialized_end=2685,
+)
+
+
+_APPROVALVALUE = _descriptor.Descriptor(
+ name='ApprovalValue',
+ full_name='monorail.v3.ApprovalValue',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.ApprovalValue.name', 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='approval_def', full_name='monorail.v3.ApprovalValue.approval_def', 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=b'\372A\033\n\031api.crbug.com/ApprovalDef\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='approvers', full_name='monorail.v3.ApprovalValue.approvers', 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=b'\372A\024\n\022api.crbug.com/User', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='status', full_name='monorail.v3.ApprovalValue.status', index=3,
+ number=4, type=14, cpp_type=8, 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='set_time', full_name='monorail.v3.ApprovalValue.set_time', index=4,
+ number=5, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='setter', full_name='monorail.v3.ApprovalValue.setter', 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=b'\372A\024\n\022api.crbug.com/User\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='phase', full_name='monorail.v3.ApprovalValue.phase', index=6,
+ number=7, 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=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='field_values', full_name='monorail.v3.ApprovalValue.field_values', index=7,
+ number=8, 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=[
+ _APPROVALVALUE_APPROVALSTATUS,
+ ],
+ serialized_options=b'\352AZ\n\033api.crbug.com/ApprovalValue\022;projects/{project}/issues/{issue}/approvalValues/{approval}',
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2688,
+ serialized_end=3314,
+)
+
+_COMMENT_ATTACHMENT.fields_by_name['state'].enum_type = _ISSUECONTENTSTATE
+_COMMENT_ATTACHMENT.containing_type = _COMMENT
+_COMMENT_AMENDMENT.containing_type = _COMMENT
+_COMMENT.fields_by_name['state'].enum_type = _ISSUECONTENTSTATE
+_COMMENT.fields_by_name['type'].enum_type = _COMMENT_TYPE
+_COMMENT.fields_by_name['create_time'].message_type = google_dot_protobuf_dot_timestamp__pb2._TIMESTAMP
+_COMMENT.fields_by_name['amendments'].message_type = _COMMENT_AMENDMENT
+_COMMENT.fields_by_name['attachments'].message_type = _COMMENT_ATTACHMENT
+_COMMENT_TYPE.containing_type = _COMMENT
+_FIELDVALUE.fields_by_name['derivation'].enum_type = _DERIVATION
+_ISSUE_COMPONENTVALUE.fields_by_name['derivation'].enum_type = _DERIVATION
+_ISSUE_COMPONENTVALUE.containing_type = _ISSUE
+_ISSUE_LABELVALUE.fields_by_name['derivation'].enum_type = _DERIVATION
+_ISSUE_LABELVALUE.containing_type = _ISSUE
+_ISSUE_STATUSVALUE.fields_by_name['derivation'].enum_type = _DERIVATION
+_ISSUE_STATUSVALUE.containing_type = _ISSUE
+_ISSUE_USERVALUE.fields_by_name['derivation'].enum_type = _DERIVATION
+_ISSUE_USERVALUE.containing_type = _ISSUE
+_ISSUE.fields_by_name['state'].enum_type = _ISSUECONTENTSTATE
+_ISSUE.fields_by_name['status'].message_type = _ISSUE_STATUSVALUE
+_ISSUE.fields_by_name['owner'].message_type = _ISSUE_USERVALUE
+_ISSUE.fields_by_name['cc_users'].message_type = _ISSUE_USERVALUE
+_ISSUE.fields_by_name['labels'].message_type = _ISSUE_LABELVALUE
+_ISSUE.fields_by_name['components'].message_type = _ISSUE_COMPONENTVALUE
+_ISSUE.fields_by_name['field_values'].message_type = _FIELDVALUE
+_ISSUE.fields_by_name['merged_into_issue_ref'].message_type = _ISSUEREF
+_ISSUE.fields_by_name['blocked_on_issue_refs'].message_type = _ISSUEREF
+_ISSUE.fields_by_name['blocking_issue_refs'].message_type = _ISSUEREF
+_ISSUE.fields_by_name['create_time'].message_type = google_dot_protobuf_dot_timestamp__pb2._TIMESTAMP
+_ISSUE.fields_by_name['close_time'].message_type = google_dot_protobuf_dot_timestamp__pb2._TIMESTAMP
+_ISSUE.fields_by_name['modify_time'].message_type = google_dot_protobuf_dot_timestamp__pb2._TIMESTAMP
+_ISSUE.fields_by_name['component_modify_time'].message_type = google_dot_protobuf_dot_timestamp__pb2._TIMESTAMP
+_ISSUE.fields_by_name['status_modify_time'].message_type = google_dot_protobuf_dot_timestamp__pb2._TIMESTAMP
+_ISSUE.fields_by_name['owner_modify_time'].message_type = google_dot_protobuf_dot_timestamp__pb2._TIMESTAMP
+_APPROVALVALUE.fields_by_name['status'].enum_type = _APPROVALVALUE_APPROVALSTATUS
+_APPROVALVALUE.fields_by_name['set_time'].message_type = google_dot_protobuf_dot_timestamp__pb2._TIMESTAMP
+_APPROVALVALUE.fields_by_name['field_values'].message_type = _FIELDVALUE
+_APPROVALVALUE_APPROVALSTATUS.containing_type = _APPROVALVALUE
+DESCRIPTOR.message_types_by_name['Comment'] = _COMMENT
+DESCRIPTOR.message_types_by_name['FieldValue'] = _FIELDVALUE
+DESCRIPTOR.message_types_by_name['Issue'] = _ISSUE
+DESCRIPTOR.message_types_by_name['IssuesListColumn'] = _ISSUESLISTCOLUMN
+DESCRIPTOR.message_types_by_name['IssueRef'] = _ISSUEREF
+DESCRIPTOR.message_types_by_name['ApprovalValue'] = _APPROVALVALUE
+DESCRIPTOR.enum_types_by_name['Derivation'] = _DERIVATION
+DESCRIPTOR.enum_types_by_name['IssueContentState'] = _ISSUECONTENTSTATE
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+Comment = _reflection.GeneratedProtocolMessageType('Comment', (_message.Message,), {
+
+ 'Attachment' : _reflection.GeneratedProtocolMessageType('Attachment', (_message.Message,), {
+ 'DESCRIPTOR' : _COMMENT_ATTACHMENT,
+ '__module__' : 'api.v3.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.Comment.Attachment)
+ })
+ ,
+
+ 'Amendment' : _reflection.GeneratedProtocolMessageType('Amendment', (_message.Message,), {
+ 'DESCRIPTOR' : _COMMENT_AMENDMENT,
+ '__module__' : 'api.v3.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.Comment.Amendment)
+ })
+ ,
+ 'DESCRIPTOR' : _COMMENT,
+ '__module__' : 'api.v3.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.Comment)
+ })
+_sym_db.RegisterMessage(Comment)
+_sym_db.RegisterMessage(Comment.Attachment)
+_sym_db.RegisterMessage(Comment.Amendment)
+
+FieldValue = _reflection.GeneratedProtocolMessageType('FieldValue', (_message.Message,), {
+ 'DESCRIPTOR' : _FIELDVALUE,
+ '__module__' : 'api.v3.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.FieldValue)
+ })
+_sym_db.RegisterMessage(FieldValue)
+
+Issue = _reflection.GeneratedProtocolMessageType('Issue', (_message.Message,), {
+
+ 'ComponentValue' : _reflection.GeneratedProtocolMessageType('ComponentValue', (_message.Message,), {
+ 'DESCRIPTOR' : _ISSUE_COMPONENTVALUE,
+ '__module__' : 'api.v3.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.Issue.ComponentValue)
+ })
+ ,
+
+ 'LabelValue' : _reflection.GeneratedProtocolMessageType('LabelValue', (_message.Message,), {
+ 'DESCRIPTOR' : _ISSUE_LABELVALUE,
+ '__module__' : 'api.v3.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.Issue.LabelValue)
+ })
+ ,
+
+ 'StatusValue' : _reflection.GeneratedProtocolMessageType('StatusValue', (_message.Message,), {
+ 'DESCRIPTOR' : _ISSUE_STATUSVALUE,
+ '__module__' : 'api.v3.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.Issue.StatusValue)
+ })
+ ,
+
+ 'UserValue' : _reflection.GeneratedProtocolMessageType('UserValue', (_message.Message,), {
+ 'DESCRIPTOR' : _ISSUE_USERVALUE,
+ '__module__' : 'api.v3.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.Issue.UserValue)
+ })
+ ,
+ 'DESCRIPTOR' : _ISSUE,
+ '__module__' : 'api.v3.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.Issue)
+ })
+_sym_db.RegisterMessage(Issue)
+_sym_db.RegisterMessage(Issue.ComponentValue)
+_sym_db.RegisterMessage(Issue.LabelValue)
+_sym_db.RegisterMessage(Issue.StatusValue)
+_sym_db.RegisterMessage(Issue.UserValue)
+
+IssuesListColumn = _reflection.GeneratedProtocolMessageType('IssuesListColumn', (_message.Message,), {
+ 'DESCRIPTOR' : _ISSUESLISTCOLUMN,
+ '__module__' : 'api.v3.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.IssuesListColumn)
+ })
+_sym_db.RegisterMessage(IssuesListColumn)
+
+IssueRef = _reflection.GeneratedProtocolMessageType('IssueRef', (_message.Message,), {
+ 'DESCRIPTOR' : _ISSUEREF,
+ '__module__' : 'api.v3.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.IssueRef)
+ })
+_sym_db.RegisterMessage(IssueRef)
+
+ApprovalValue = _reflection.GeneratedProtocolMessageType('ApprovalValue', (_message.Message,), {
+ 'DESCRIPTOR' : _APPROVALVALUE,
+ '__module__' : 'api.v3.api_proto.issue_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ApprovalValue)
+ })
+_sym_db.RegisterMessage(ApprovalValue)
+
+
+DESCRIPTOR._options = None
+_COMMENT.fields_by_name['state']._options = None
+_COMMENT.fields_by_name['commenter']._options = None
+_COMMENT.fields_by_name['create_time']._options = None
+_COMMENT.fields_by_name['inbound_message']._options = None
+_COMMENT.fields_by_name['approval']._options = None
+_COMMENT.fields_by_name['amendments']._options = None
+_COMMENT.fields_by_name['attachments']._options = None
+_COMMENT._options = None
+_FIELDVALUE.fields_by_name['field']._options = None
+_FIELDVALUE.fields_by_name['derivation']._options = None
+_ISSUE_COMPONENTVALUE.fields_by_name['component']._options = None
+_ISSUE_COMPONENTVALUE.fields_by_name['derivation']._options = None
+_ISSUE_LABELVALUE.fields_by_name['derivation']._options = None
+_ISSUE_STATUSVALUE.fields_by_name['derivation']._options = None
+_ISSUE_USERVALUE.fields_by_name['user']._options = None
+_ISSUE_USERVALUE.fields_by_name['derivation']._options = None
+_ISSUE.fields_by_name['state']._options = None
+_ISSUE.fields_by_name['status']._options = None
+_ISSUE.fields_by_name['reporter']._options = None
+_ISSUE.fields_by_name['create_time']._options = None
+_ISSUE.fields_by_name['close_time']._options = None
+_ISSUE.fields_by_name['modify_time']._options = None
+_ISSUE.fields_by_name['component_modify_time']._options = None
+_ISSUE.fields_by_name['status_modify_time']._options = None
+_ISSUE.fields_by_name['owner_modify_time']._options = None
+_ISSUE.fields_by_name['attachment_count']._options = None
+_ISSUE.fields_by_name['star_count']._options = None
+_ISSUE.fields_by_name['phases']._options = None
+_ISSUE._options = None
+_ISSUEREF.fields_by_name['issue']._options = None
+_APPROVALVALUE.fields_by_name['approval_def']._options = None
+_APPROVALVALUE.fields_by_name['approvers']._options = None
+_APPROVALVALUE.fields_by_name['set_time']._options = None
+_APPROVALVALUE.fields_by_name['setter']._options = None
+_APPROVALVALUE.fields_by_name['phase']._options = None
+_APPROVALVALUE._options = None
+# @@protoc_insertion_point(module_scope)
diff --git a/api/v3/api_proto/issues.proto b/api/v3/api_proto/issues.proto
new file mode 100644
index 0000000..988f958
--- /dev/null
+++ b/api/v3/api_proto/issues.proto
@@ -0,0 +1,461 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+syntax = "proto3";
+
+package monorail.v3;
+
+option go_package = "api/v3/api_proto";
+
+import "google/protobuf/field_mask.proto";
+import "google/api/field_behavior.proto";
+import "google/api/resource.proto";
+import "api/v3/api_proto/issue_objects.proto";
+
+// ***ONLY CALL rpcs WITH `status: {ALPHA|STABLE}`***
+// rpcs without `status` are not ready.
+
+// Issues service includes all methods needed for managing Issues.
+service Issues {
+ // status: ALPHA
+ // Returns the requested Issue.
+ //
+ // Raises:
+ // INVALID_ARGUMENT if `name` is formatted incorrectly.
+ // NOT_FOUND if the issue does not exist.
+ // PERMISSION_DENIED if the requester is not allowed to view the issue.
+ rpc GetIssue (GetIssueRequest) returns (Issue) {}
+
+ // status: ALPHA
+ // Returns the requested Issues.
+ //
+ // Raises:
+ // INVALID_ARGUMENT if `names` is formatted incorrectly. Or if a parent
+ // collection in `names` does not match the value in `parent`.
+ // NOT_FOUND if any of the given issues do not exist.
+ // PERMISSION_DENIED if the requester does not have permission to view one
+ // (or more) of the given issues.
+ rpc BatchGetIssues(BatchGetIssuesRequest) returns (BatchGetIssuesResponse) {}
+
+ // status: ALPHA
+ // Searches over issues within the specified projects.
+ //
+ // Raises:
+ // INVALID_ARGUMENT if project names or search query are invalid.
+ rpc SearchIssues (SearchIssuesRequest) returns (SearchIssuesResponse) {}
+
+ // status: ALPHA
+ // Lists comments for an issue.
+ //
+ // Raises:
+ // INVALID_ARGUMENT if `parent` is formatted incorrectly or `page_size` < 0.
+ // NOT_FOUND if `parent` does not exist.
+ // PERMISSION_DENIED if the requester is not allowed to view `parent`.
+ rpc ListComments (ListCommentsRequest) returns (ListCommentsResponse) {}
+
+ // status: ALPHA
+ // Modifies Issues and creates a new Comment for each.
+ // Issues with NOOP changes and no comment_content will not be updated
+ // and will not be included in the response.
+ // We do not offer a standard UpdateIssue because every issue change
+ // must result in the side-effect of creating a new Comment, and may result in
+ // the side effect of sending a notification. We also want to allow for any
+ // combination of issue changes to be made at once in a monolithic method.
+ //
+ // Raises:
+ // INVALID_ARGUMENT required fields are missing or fields are formatted
+ // incorrectly.
+ // NOT_FOUND if any specified issues are not found.
+ // PERMISSION_DENIED if the requester is not allowed to make the
+ // requested change.
+ rpc ModifyIssues (ModifyIssuesRequest) returns (ModifyIssuesResponse) {}
+
+ // status: ALPHA
+ // Modifies ApprovalValues and creates a new Comment for each delta.
+ // We do not offer a standard UpdateApprovalValue because changes result
+ // in creating Comments on the parent Issue, and may have the side effect of
+ // sending notifications. We also want to allow for any combination of
+ // approval changes to be made at once in a monolithic method.
+ // To modify owner add 'owner' to update_mask, though 'owner.user' works too.
+ //
+ // Raises:
+ // INVALID_ARGUMENT required fields are missing or fields are formatted
+ // incorrectly.
+ // NOT_FOUND if any specified ApprovalValues are not found.
+ // PERMISSION_DENIED if the requester is not allowed to make any of the
+ // requested changes.
+ rpc ModifyIssueApprovalValues (ModifyIssueApprovalValuesRequest) returns
+ (ModifyIssueApprovalValuesResponse) {}
+
+ // status: ALPHA
+ // Lists approval values for an issue.
+ //
+ // Raises:
+ // INVALID_ARGUMENT if request `parent` is formatted incorrectly.
+ // NOT_FOUND if the parent issue does not exist.
+ // PERMISSION_DENIED if the requester is not allowed to view parent issue.
+ rpc ListApprovalValues (ListApprovalValuesRequest) returns
+ (ListApprovalValuesResponse) {}
+
+ // status: NOT READY
+ // Changes state for a comment. Supported state transitions:
+ // - ACTIVE -> DELETED
+ // - ACTIVE -> SPAM
+ // - DELETED -> ACTIVE
+ // - SPAM -> ACTIVE
+ //
+ // Raises:
+ // TODO(crbug/monorail/7867): Document errors when implemented
+ rpc ModifyCommentState (ModifyCommentStateRequest) returns
+ (ModifyCommentStateResponse) {}
+
+ // status: NOT READY
+ // Makes an issue from an IssueTemplate and deltas.
+ //
+ // Raises:
+ // TODO(crbug/monorail/7197): Document errors when implemented
+ rpc MakeIssueFromTemplate (MakeIssueFromTemplateRequest) returns (Issue) {}
+
+ // status: ALPHA
+ // Makes a basic issue, does not support phases, approvals, or approval
+ // fields.
+ // We do not offer a standard CreateIssue because Issue descriptions are
+ // required, but not included in the Issue proto.
+ //
+ // Raises:
+ // INVALID_ARGUMENT if any given names does not have a valid format, if any
+ // fields in the requested issue were invalid, or if proposed values
+ // violates filter rules that should error.
+ // NOT_FOUND if no project exists with the given name.
+ // PERMISSION_DENIED if user lacks sufficient permissions.
+ rpc MakeIssue (MakeIssueRequest) returns (Issue) {}
+}
+
+
+// The request message for Issues.GetIssue.
+// Next available tag: 2
+message GetIssueRequest {
+ // The name of the issue to request.
+ string name = 1 [
+ (google.api.resource_reference) = {type: "api.crbug.com/Issue"},
+ (google.api.field_behavior) = REQUIRED ];
+}
+
+// The request message for Issues.BatchGetIssues.
+// Next available tag: 3
+message BatchGetIssuesRequest {
+ // The project name from which to batch get issues. If included, the parent
+ // of all the issues specified in `names` must match this field.
+ string parent = 1 [
+ (google.api.resource_reference) = {type: "api.crbug.com/Project"} ];
+ // The issues to request. Maximum of 100 can be retrieved.
+ repeated string names = 2 [
+ (google.api.resource_reference) = {type: "api.crbug.com/Issue"} ];
+}
+
+// The response message for Issues.BatchGetIssues.
+// Next available tag: 2
+message BatchGetIssuesResponse {
+ // Issues matching the given request.
+ repeated Issue issues = 1;
+}
+
+// The request message for Issues.SearchIssues.
+// Next available tag: 6
+message SearchIssuesRequest {
+ // The names of Projects in which to search issues.
+ repeated string projects = 1 [
+ (google.api.resource_reference) = {type: "api.crbug.com/Project"},
+ (google.api.field_behavior) = REQUIRED ];
+ // The query string can contain any number of free text and
+ // field search expressions.
+ // Please see https://bugs.chromium.org/p/chromium/issues/searchtips for more
+ // details of how the query string works.
+ //
+ // Canned queries have been deprecated in v3 in favor of search scoping using
+ // parentheses support.
+ // For clients who previously used canned queries, we're providing the
+ // mapping of legacy canned query IDs to Monorail search syntax:
+ // - Format: (can_id, description, query_string)
+ // - (1, 'All issues', '')
+ // - (2, 'Open issues', 'is:open')
+ // - (3, 'Open and owned by me', 'is:open owner:me')
+ // - (4, 'Open and reported by me', 'is:open reporter:me')
+ // - (5, 'Open and starred by me', 'is:open is:starred')
+ // - (6, 'New issues', 'status:new')
+ // - (7, 'Issues to verify', 'status=fixed,done')
+ // - (8, 'Open with comment by me', 'is:open commentby:me')
+ string query = 2;
+ // The maximum number of items to return. The service may return fewer than
+ // this value.
+ // If unspecified, at most 100 items will be returned.
+ // The maximum value is 100; values above 100 will be coerced to 100.
+ int32 page_size = 3;
+ // A page token, received from a previous `SearchIssues` call.
+ // Provide this to retrieve the subsequent page.
+ //
+ // When paginating, all other parameters provided to `SearchIssues` must match
+ // the call that provided the page token.
+ string page_token = 4;
+ // The string of comma separated field names used to order the items.
+ // Adding '-' before a field, reverses the sort order.
+ // E.g. 'stars,-status' sorts the items by number of stars, high to low,
+ // then by status, low to high.
+ string order_by = 5;
+}
+
+// The response message for Issues.SearchIssues.
+// Next available tag: 3
+message SearchIssuesResponse {
+ // Issues matching the given request.
+ repeated Issue issues = 1;
+ // A token, which can be sent as `page_token` to retrieve the next page.
+ // If this field is omitted, there are no subsequent pages.
+ string next_page_token = 2;
+}
+
+// The request message for Issues.ListComments.
+// Next available tag: 5
+message ListCommentsRequest {
+ // The name of the issue for which to list comments.
+ string parent = 1 [
+ (google.api.resource_reference) = {type: "api.crbug.com/Issue"},
+ (google.api.field_behavior) = REQUIRED ];
+ // The maximum number of items to return. The service may return fewer than
+ // this value.
+ // If unspecified, at most 100 items will be returned.
+ // The maximum value is 100; values above 100 will be coerced to 100.
+ int32 page_size = 2;
+ // A page token, received from a previous `ListComments` call.
+ // Provide this to retrieve the subsequent page.
+ //
+ // When paginating, all other parameters provided to `ListComments` must
+ // match the call that provided the page token.
+ string page_token = 3;
+ // For our initial release this filter only supports filtering to comments
+ // related to a specific approval.
+ // For example `approval = "projects/monorail/approvalDefs/1"`,
+ // Note that no further logical or comparison operators are supported
+ string filter = 4;
+}
+
+// The response message for Issues.ListComments
+// Next available tag: 3
+message ListCommentsResponse {
+ // The comments from the specified issue.
+ repeated Comment comments = 1;
+ // A token, which can be sent as `page_token` to retrieve the next page.
+ // If this field is omitted, there are no subsequent pages.
+ string next_page_token = 2;
+}
+
+// An attachment to upload to a comment or description.
+// Next available tag: 3
+message AttachmentUpload {
+ string filename = 1 [ (google.api.field_behavior) = REQUIRED ];
+ bytes content = 2 [ (google.api.field_behavior) = REQUIRED ];
+}
+
+// Holds changes to one issue, used in ModifyIssuesRequest.
+// Next available tag: 9
+message IssueDelta {
+ // The issue's `name` field is used to identify the issue to be
+ // updated. `issue.name` must always be filled.
+ //
+ // Values with rule-based Derivation within `issue` and in `field_vals_remove`
+ // will be ignored.
+ Issue issue = 1 [
+ (google.api.field_behavior) = REQUIRED ];
+ // The list of fields in `issue` to be updated.
+ //
+ // Repeated fields set on `issue` will be appended to.
+ //
+ // Non-repeated fields (e.g. `owner`) can be set with `issue.owner` set and
+ // either 'owner' or 'owner.user' added to `update_mask`.
+ // To unset non-repeated fields back to their default value, `issue.owner`
+ // must contain the default value and `update_mask` must include 'owner.user'
+ // NOT 'owner'.
+ //
+ // Its `field_values`, however, are a special case. Fields can be specified as
+ // single-value or multi-value in their FieldDef.
+ //
+ // Single-value Field: if there is preexisting FieldValue with the same
+ // `field` and `phase`, it will be REPLACED.
+ //
+ // Multi-value Field: a new value will be appended, unless the same `field`,
+ // `phase`, `value` combination already exists. In that case, the FieldValue
+ // will be ignored. In other words, duplicate values are ignored.
+ // (With the exception of crbug.com/monorail/8137 until it is fixed).
+ google.protobuf.FieldMask update_mask = 2 [
+ (google.api.field_behavior) = REQUIRED ];
+
+ // Values to remove from the repeated fields of the issue.
+
+ // Cc's to remove.
+ repeated string ccs_remove = 3 [
+ (google.api.resource_reference) = {type: "api.crbug.com/User"}];
+ // Blocked_on issues to remove.
+ repeated IssueRef blocked_on_issues_remove = 4;
+ // Blocking issues to remove.
+ repeated IssueRef blocking_issues_remove = 5;
+ // Components to remove.
+ repeated string components_remove = 6 [
+ (google.api.resource_reference) = {type: "api.crbug.com/ComponentDef"}];
+ // Labels to remove.
+ repeated string labels_remove = 7;
+ // FieldValues to remove. Any values that did not already exist will be
+ // ignored e.g. if you append a FieldValue in issue and remove it here, it
+ // will still be added.
+ repeated FieldValue field_vals_remove = 8;
+
+ // TODO(crbug.com/monorail/8019): add Attachment uploading and removing.
+}
+
+// Changes to make to an ApprovalValue. Used to ModifyIssueApprovalValues or
+// to MakeIssueFromTemplate.
+//
+// NOTE: The same handling of FieldValues discussed in IssueDelta applies here.
+// Next available tag: 6
+message ApprovalDelta {
+ // The ApprovalValue we want to update. `approval_value.name` must always be
+ // set.
+ ApprovalValue approval_value = 1;
+ // Repeated fields found in `update_mask` will be appended to.
+ google.protobuf.FieldMask update_mask = 2 [
+ (google.api.field_behavior) = REQUIRED ];
+ // Resource names of the approvers we want to remove.
+ repeated string approvers_remove = 3 [
+ (google.api.resource_reference) = { type: "api.crbug.com/User" }
+ ];
+ // FieldValues that do not belong to `approval_value` will trigger error.
+ repeated FieldValue field_vals_remove = 5;
+ // TODO(crbug.com/monorail/8019): add Attachment uploading and removing.
+}
+
+
+// The type of notification a change should trigger.
+// See monorail/doc/userguide/email.md
+// Next available tag: 2
+enum NotifyType {
+ // The default value. This value is unused.
+ NOTIFY_TYPE_UNSPECIFIED = 0;
+ // An email notification should be sent.
+ EMAIL = 1;
+ // No notifcation should be triggered at all.
+ NO_NOTIFICATION = 2;
+}
+
+
+// The request message for Issues.ModifyIssues.
+// Next available tag: 5
+message ModifyIssuesRequest {
+ // The issue changes to make. A maximum of 100 issue changes can be requested.
+ // There is also a constraint of 50 additional 'impacted issues' per
+ // ModifyIssuesRequest. 'Impacted issues' are issues that are adding/removing
+ // `blocked_on`, `blocking`, or `merge`
+ // If you encounter this error, consider significantly smaller batches.
+ repeated IssueDelta deltas = 1;
+ // The type of notification the modifications should trigger.
+ NotifyType notify_type = 2;
+ // The comment text that should be added to each issue in delta.
+ // Max length is 51200 characters.
+ string comment_content = 3;
+ // The attachment that will be to each comment for each issue in delta.
+ repeated AttachmentUpload uploads = 4;
+}
+
+
+// The response message for Issues.ModifyIssues.
+// Next available tag: 2
+message ModifyIssuesResponse {
+ // The updated issues.
+ repeated Issue issues = 1;
+}
+
+// The request message for Issues.ModifyIssueApprovalValues.
+// Next available tag: 4
+message ModifyIssueApprovalValuesRequest {
+ // The ApprovalValue changes to make. Maximum of 100 deltas can be requested.
+ repeated ApprovalDelta deltas = 1;
+ // The type of notification the modifications should trigger.
+ NotifyType notify_type = 2;
+ // The `content` of the Comment created for each change in `deltas`.
+ // Max length is 51200 characters.
+ string comment_content = 3;
+}
+
+// The response message for Issues.ModifyIssueApprovalValuesRequest.
+// Next available tag: 2
+message ModifyIssueApprovalValuesResponse {
+ // The updated ApprovalValues.
+ repeated ApprovalValue approval_values = 1;
+}
+
+// The request message for Issue.ListApprovalValues.
+// Next available tag: 2
+message ListApprovalValuesRequest {
+ // The name of the issue for which to list approval values.
+ string parent = 1 [
+ (google.api.resource_reference) = {type: "api.crbug.com/Issue"},
+ (google.api.field_behavior) = REQUIRED ];
+}
+
+// The response message for Issues.ListApprovalValues.
+// Next available tag: 2
+message ListApprovalValuesResponse {
+ // The approval values from the specified issue.
+ repeated ApprovalValue approval_values = 1;
+}
+
+// The request message for Issues.ModifyCommentState.
+// Next available tag: 3
+message ModifyCommentStateRequest {
+ // Resource name of the comment to modify state.
+ string name = 1 [
+ (google.api.resource_reference) = {type: "api.crbug.com/Comment"},
+ (google.api.field_behavior) = REQUIRED ];
+ // Requested state.
+ IssueContentState state = 2;
+}
+
+// The response message for Issues.ModifyCommentState.
+// Next available tag: 2
+message ModifyCommentStateResponse {
+ // The updated comment after modifying state.
+ Comment comment = 1;
+}
+
+// The request message for MakeIssueFromTemplate.
+// Next available tag: 5
+message MakeIssueFromTemplateRequest {
+ // Resource name of the template to use for filling in default values
+ // and adding approvals and phases.
+ string template = 1 [
+ (google.api.resource_reference) = { type: "api.crbug.com/Template" }
+ ];
+ // The issue differences relative to the `template.issue` default.
+ IssueDelta template_issue_delta = 2;
+ // Changes to fields belonging to approvals relative to template default.
+ // While ApprovalDelta can hold additional information, this method only
+ // allows adding and removing field values, all other deltas will be ignored.
+ repeated ApprovalDelta template_approval_deltas = 3;
+ // The issue description, will be saved as the first comment.
+ string description = 4;
+}
+
+// The request message for MakeIssue.
+// Next available tag: 5
+message MakeIssueRequest {
+ // The name of the project the issue should belong to.
+ string parent = 1 [
+ (google.api.resource_reference) = {type: "api.crbug.com/Project"},
+ (google.api.field_behavior) = REQUIRED ];
+ // The issue to be created.
+ Issue issue = 2;
+ // The issue description.
+ string description = 3;
+ // The type of notification the creation should trigger.
+ NotifyType notify_type = 4;
+}
diff --git a/api/v3/api_proto/issues_pb2.py b/api/v3/api_proto/issues_pb2.py
new file mode 100644
index 0000000..7b8abd3
--- /dev/null
+++ b/api/v3/api_proto/issues_pb2.py
@@ -0,0 +1,1261 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/v3/api_proto/issues.proto
+
+from google.protobuf.internal import enum_type_wrapper
+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()
+
+
+from google.protobuf import field_mask_pb2 as google_dot_protobuf_dot_field__mask__pb2
+from google.api import field_behavior_pb2 as google_dot_api_dot_field__behavior__pb2
+from google.api import resource_pb2 as google_dot_api_dot_resource__pb2
+from api.v3.api_proto import issue_objects_pb2 as api_dot_v3_dot_api__proto_dot_issue__objects__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='api/v3/api_proto/issues.proto',
+ package='monorail.v3',
+ syntax='proto3',
+ serialized_options=b'Z\020api/v3/api_proto',
+ create_key=_descriptor._internal_create_key,
+ serialized_pb=b'\n\x1d\x61pi/v3/api_proto/issues.proto\x12\x0bmonorail.v3\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a$api/v3/api_proto/issue_objects.proto\"<\n\x0fGetIssueRequest\x12)\n\x04name\x18\x01 \x01(\tB\x1b\xfa\x41\x15\n\x13\x61pi.crbug.com/Issue\xe0\x41\x02\"l\n\x15\x42\x61tchGetIssuesRequest\x12*\n\x06parent\x18\x01 \x01(\tB\x1a\xfa\x41\x17\n\x15\x61pi.crbug.com/Project\x12\'\n\x05names\x18\x02 \x03(\tB\x18\xfa\x41\x15\n\x13\x61pi.crbug.com/Issue\"<\n\x16\x42\x61tchGetIssuesResponse\x12\"\n\x06issues\x18\x01 \x03(\x0b\x32\x12.monorail.v3.Issue\"\x8e\x01\n\x13SearchIssuesRequest\x12/\n\x08projects\x18\x01 \x03(\tB\x1d\xfa\x41\x17\n\x15\x61pi.crbug.com/Project\xe0\x41\x02\x12\r\n\x05query\x18\x02 \x01(\t\x12\x11\n\tpage_size\x18\x03 \x01(\x05\x12\x12\n\npage_token\x18\x04 \x01(\t\x12\x10\n\x08order_by\x18\x05 \x01(\t\"S\n\x14SearchIssuesResponse\x12\"\n\x06issues\x18\x01 \x03(\x0b\x32\x12.monorail.v3.Issue\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\"y\n\x13ListCommentsRequest\x12+\n\x06parent\x18\x01 \x01(\tB\x1b\xfa\x41\x15\n\x13\x61pi.crbug.com/Issue\xe0\x41\x02\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x12\n\npage_token\x18\x03 \x01(\t\x12\x0e\n\x06\x66ilter\x18\x04 \x01(\t\"W\n\x14ListCommentsResponse\x12&\n\x08\x63omments\x18\x01 \x03(\x0b\x32\x14.monorail.v3.Comment\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\"?\n\x10\x41ttachmentUpload\x12\x15\n\x08\x66ilename\x18\x01 \x01(\tB\x03\xe0\x41\x02\x12\x14\n\x07\x63ontent\x18\x02 \x01(\x0c\x42\x03\xe0\x41\x02\"\x8e\x03\n\nIssueDelta\x12&\n\x05issue\x18\x01 \x01(\x0b\x32\x12.monorail.v3.IssueB\x03\xe0\x41\x02\x12\x34\n\x0bupdate_mask\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.FieldMaskB\x03\xe0\x41\x02\x12+\n\nccs_remove\x18\x03 \x03(\tB\x17\xfa\x41\x14\n\x12\x61pi.crbug.com/User\x12\x37\n\x18\x62locked_on_issues_remove\x18\x04 \x03(\x0b\x32\x15.monorail.v3.IssueRef\x12\x35\n\x16\x62locking_issues_remove\x18\x05 \x03(\x0b\x32\x15.monorail.v3.IssueRef\x12:\n\x11\x63omponents_remove\x18\x06 \x03(\tB\x1f\xfa\x41\x1c\n\x1a\x61pi.crbug.com/ComponentDef\x12\x15\n\rlabels_remove\x18\x07 \x03(\t\x12\x32\n\x11\x66ield_vals_remove\x18\x08 \x03(\x0b\x32\x17.monorail.v3.FieldValue\"\xe0\x01\n\rApprovalDelta\x12\x32\n\x0e\x61pproval_value\x18\x01 \x01(\x0b\x32\x1a.monorail.v3.ApprovalValue\x12\x34\n\x0bupdate_mask\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.FieldMaskB\x03\xe0\x41\x02\x12\x31\n\x10\x61pprovers_remove\x18\x03 \x03(\tB\x17\xfa\x41\x14\n\x12\x61pi.crbug.com/User\x12\x32\n\x11\x66ield_vals_remove\x18\x05 \x03(\x0b\x32\x17.monorail.v3.FieldValue\"\xb5\x01\n\x13ModifyIssuesRequest\x12\'\n\x06\x64\x65ltas\x18\x01 \x03(\x0b\x32\x17.monorail.v3.IssueDelta\x12,\n\x0bnotify_type\x18\x02 \x01(\x0e\x32\x17.monorail.v3.NotifyType\x12\x17\n\x0f\x63omment_content\x18\x03 \x01(\t\x12.\n\x07uploads\x18\x04 \x03(\x0b\x32\x1d.monorail.v3.AttachmentUpload\":\n\x14ModifyIssuesResponse\x12\"\n\x06issues\x18\x01 \x03(\x0b\x32\x12.monorail.v3.Issue\"\x95\x01\n ModifyIssueApprovalValuesRequest\x12*\n\x06\x64\x65ltas\x18\x01 \x03(\x0b\x32\x1a.monorail.v3.ApprovalDelta\x12,\n\x0bnotify_type\x18\x02 \x01(\x0e\x32\x17.monorail.v3.NotifyType\x12\x17\n\x0f\x63omment_content\x18\x03 \x01(\t\"X\n!ModifyIssueApprovalValuesResponse\x12\x33\n\x0f\x61pproval_values\x18\x01 \x03(\x0b\x32\x1a.monorail.v3.ApprovalValue\"H\n\x19ListApprovalValuesRequest\x12+\n\x06parent\x18\x01 \x01(\tB\x1b\xfa\x41\x15\n\x13\x61pi.crbug.com/Issue\xe0\x41\x02\"Q\n\x1aListApprovalValuesResponse\x12\x33\n\x0f\x61pproval_values\x18\x01 \x03(\x0b\x32\x1a.monorail.v3.ApprovalValue\"w\n\x19ModifyCommentStateRequest\x12+\n\x04name\x18\x01 \x01(\tB\x1d\xfa\x41\x17\n\x15\x61pi.crbug.com/Comment\xe0\x41\x02\x12-\n\x05state\x18\x02 \x01(\x0e\x32\x1e.monorail.v3.IssueContentState\"C\n\x1aModifyCommentStateResponse\x12%\n\x07\x63omment\x18\x01 \x01(\x0b\x32\x14.monorail.v3.Comment\"\xd7\x01\n\x1cMakeIssueFromTemplateRequest\x12-\n\x08template\x18\x01 \x01(\tB\x1b\xfa\x41\x18\n\x16\x61pi.crbug.com/Template\x12\x35\n\x14template_issue_delta\x18\x02 \x01(\x0b\x32\x17.monorail.v3.IssueDelta\x12<\n\x18template_approval_deltas\x18\x03 \x03(\x0b\x32\x1a.monorail.v3.ApprovalDelta\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\"\xa7\x01\n\x10MakeIssueRequest\x12-\n\x06parent\x18\x01 \x01(\tB\x1d\xfa\x41\x17\n\x15\x61pi.crbug.com/Project\xe0\x41\x02\x12!\n\x05issue\x18\x02 \x01(\x0b\x32\x12.monorail.v3.Issue\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12,\n\x0bnotify_type\x18\x04 \x01(\x0e\x32\x17.monorail.v3.NotifyType*I\n\nNotifyType\x12\x1b\n\x17NOTIFY_TYPE_UNSPECIFIED\x10\x00\x12\t\n\x05\x45MAIL\x10\x01\x12\x13\n\x0fNO_NOTIFICATION\x10\x02\x32\x96\x07\n\x06Issues\x12>\n\x08GetIssue\x12\x1c.monorail.v3.GetIssueRequest\x1a\x12.monorail.v3.Issue\"\x00\x12[\n\x0e\x42\x61tchGetIssues\x12\".monorail.v3.BatchGetIssuesRequest\x1a#.monorail.v3.BatchGetIssuesResponse\"\x00\x12U\n\x0cSearchIssues\x12 .monorail.v3.SearchIssuesRequest\x1a!.monorail.v3.SearchIssuesResponse\"\x00\x12U\n\x0cListComments\x12 .monorail.v3.ListCommentsRequest\x1a!.monorail.v3.ListCommentsResponse\"\x00\x12U\n\x0cModifyIssues\x12 .monorail.v3.ModifyIssuesRequest\x1a!.monorail.v3.ModifyIssuesResponse\"\x00\x12|\n\x19ModifyIssueApprovalValues\x12-.monorail.v3.ModifyIssueApprovalValuesRequest\x1a..monorail.v3.ModifyIssueApprovalValuesResponse\"\x00\x12g\n\x12ListApprovalValues\x12&.monorail.v3.ListApprovalValuesRequest\x1a\'.monorail.v3.ListApprovalValuesResponse\"\x00\x12g\n\x12ModifyCommentState\x12&.monorail.v3.ModifyCommentStateRequest\x1a\'.monorail.v3.ModifyCommentStateResponse\"\x00\x12X\n\x15MakeIssueFromTemplate\x12).monorail.v3.MakeIssueFromTemplateRequest\x1a\x12.monorail.v3.Issue\"\x00\x12@\n\tMakeIssue\x12\x1d.monorail.v3.MakeIssueRequest\x1a\x12.monorail.v3.Issue\"\x00\x42\x12Z\x10\x61pi/v3/api_protob\x06proto3'
+ ,
+ dependencies=[google_dot_protobuf_dot_field__mask__pb2.DESCRIPTOR,google_dot_api_dot_field__behavior__pb2.DESCRIPTOR,google_dot_api_dot_resource__pb2.DESCRIPTOR,api_dot_v3_dot_api__proto_dot_issue__objects__pb2.DESCRIPTOR,])
+
+_NOTIFYTYPE = _descriptor.EnumDescriptor(
+ name='NotifyType',
+ full_name='monorail.v3.NotifyType',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='NOTIFY_TYPE_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='EMAIL', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='NO_NOTIFICATION', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=2768,
+ serialized_end=2841,
+)
+_sym_db.RegisterEnumDescriptor(_NOTIFYTYPE)
+
+NotifyType = enum_type_wrapper.EnumTypeWrapper(_NOTIFYTYPE)
+NOTIFY_TYPE_UNSPECIFIED = 0
+EMAIL = 1
+NO_NOTIFICATION = 2
+
+
+
+_GETISSUEREQUEST = _descriptor.Descriptor(
+ name='GetIssueRequest',
+ full_name='monorail.v3.GetIssueRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.GetIssueRequest.name', 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=b'\372A\025\n\023api.crbug.com/Issue\340A\002', 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=178,
+ serialized_end=238,
+)
+
+
+_BATCHGETISSUESREQUEST = _descriptor.Descriptor(
+ name='BatchGetIssuesRequest',
+ full_name='monorail.v3.BatchGetIssuesRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='parent', full_name='monorail.v3.BatchGetIssuesRequest.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=b'\372A\027\n\025api.crbug.com/Project', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='names', full_name='monorail.v3.BatchGetIssuesRequest.names', index=1,
+ number=2, 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=b'\372A\025\n\023api.crbug.com/Issue', 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=240,
+ serialized_end=348,
+)
+
+
+_BATCHGETISSUESRESPONSE = _descriptor.Descriptor(
+ name='BatchGetIssuesResponse',
+ full_name='monorail.v3.BatchGetIssuesResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issues', full_name='monorail.v3.BatchGetIssuesResponse.issues', 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='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=350,
+ serialized_end=410,
+)
+
+
+_SEARCHISSUESREQUEST = _descriptor.Descriptor(
+ name='SearchIssuesRequest',
+ full_name='monorail.v3.SearchIssuesRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='projects', full_name='monorail.v3.SearchIssuesRequest.projects', index=0,
+ number=1, 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=b'\372A\027\n\025api.crbug.com/Project\340A\002', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='query', full_name='monorail.v3.SearchIssuesRequest.query', 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='page_size', full_name='monorail.v3.SearchIssuesRequest.page_size', index=2,
+ number=3, 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='page_token', full_name='monorail.v3.SearchIssuesRequest.page_token', 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='order_by', full_name='monorail.v3.SearchIssuesRequest.order_by', index=4,
+ number=5, 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=413,
+ serialized_end=555,
+)
+
+
+_SEARCHISSUESRESPONSE = _descriptor.Descriptor(
+ name='SearchIssuesResponse',
+ full_name='monorail.v3.SearchIssuesResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issues', full_name='monorail.v3.SearchIssuesResponse.issues', 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),
+ _descriptor.FieldDescriptor(
+ name='next_page_token', full_name='monorail.v3.SearchIssuesResponse.next_page_token', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=557,
+ serialized_end=640,
+)
+
+
+_LISTCOMMENTSREQUEST = _descriptor.Descriptor(
+ name='ListCommentsRequest',
+ full_name='monorail.v3.ListCommentsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='parent', full_name='monorail.v3.ListCommentsRequest.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=b'\372A\025\n\023api.crbug.com/Issue\340A\002', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='page_size', full_name='monorail.v3.ListCommentsRequest.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='page_token', full_name='monorail.v3.ListCommentsRequest.page_token', 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='filter', full_name='monorail.v3.ListCommentsRequest.filter', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=642,
+ serialized_end=763,
+)
+
+
+_LISTCOMMENTSRESPONSE = _descriptor.Descriptor(
+ name='ListCommentsResponse',
+ full_name='monorail.v3.ListCommentsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='comments', full_name='monorail.v3.ListCommentsResponse.comments', 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),
+ _descriptor.FieldDescriptor(
+ name='next_page_token', full_name='monorail.v3.ListCommentsResponse.next_page_token', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=765,
+ serialized_end=852,
+)
+
+
+_ATTACHMENTUPLOAD = _descriptor.Descriptor(
+ name='AttachmentUpload',
+ full_name='monorail.v3.AttachmentUpload',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='filename', full_name='monorail.v3.AttachmentUpload.filename', 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=b'\340A\002', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='content', full_name='monorail.v3.AttachmentUpload.content', 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=b'\340A\002', 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=854,
+ serialized_end=917,
+)
+
+
+_ISSUEDELTA = _descriptor.Descriptor(
+ name='IssueDelta',
+ full_name='monorail.v3.IssueDelta',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issue', full_name='monorail.v3.IssueDelta.issue', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\002', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='update_mask', full_name='monorail.v3.IssueDelta.update_mask', index=1,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\002', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='ccs_remove', full_name='monorail.v3.IssueDelta.ccs_remove', 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=b'\372A\024\n\022api.crbug.com/User', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='blocked_on_issues_remove', full_name='monorail.v3.IssueDelta.blocked_on_issues_remove', index=3,
+ number=4, 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='blocking_issues_remove', full_name='monorail.v3.IssueDelta.blocking_issues_remove', index=4,
+ 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='components_remove', full_name='monorail.v3.IssueDelta.components_remove', index=5,
+ number=6, 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=b'\372A\034\n\032api.crbug.com/ComponentDef', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='labels_remove', full_name='monorail.v3.IssueDelta.labels_remove', index=6,
+ number=7, 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='field_vals_remove', full_name='monorail.v3.IssueDelta.field_vals_remove', index=7,
+ number=8, 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='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=920,
+ serialized_end=1318,
+)
+
+
+_APPROVALDELTA = _descriptor.Descriptor(
+ name='ApprovalDelta',
+ full_name='monorail.v3.ApprovalDelta',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='approval_value', full_name='monorail.v3.ApprovalDelta.approval_value', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ 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='update_mask', full_name='monorail.v3.ApprovalDelta.update_mask', index=1,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\002', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='approvers_remove', full_name='monorail.v3.ApprovalDelta.approvers_remove', 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=b'\372A\024\n\022api.crbug.com/User', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='field_vals_remove', full_name='monorail.v3.ApprovalDelta.field_vals_remove', index=3,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1321,
+ serialized_end=1545,
+)
+
+
+_MODIFYISSUESREQUEST = _descriptor.Descriptor(
+ name='ModifyIssuesRequest',
+ full_name='monorail.v3.ModifyIssuesRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='deltas', full_name='monorail.v3.ModifyIssuesRequest.deltas', 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),
+ _descriptor.FieldDescriptor(
+ name='notify_type', full_name='monorail.v3.ModifyIssuesRequest.notify_type', index=1,
+ number=2, type=14, cpp_type=8, 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='comment_content', full_name='monorail.v3.ModifyIssuesRequest.comment_content', 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='uploads', full_name='monorail.v3.ModifyIssuesRequest.uploads', index=3,
+ number=4, 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='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1548,
+ serialized_end=1729,
+)
+
+
+_MODIFYISSUESRESPONSE = _descriptor.Descriptor(
+ name='ModifyIssuesResponse',
+ full_name='monorail.v3.ModifyIssuesResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='issues', full_name='monorail.v3.ModifyIssuesResponse.issues', 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='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1731,
+ serialized_end=1789,
+)
+
+
+_MODIFYISSUEAPPROVALVALUESREQUEST = _descriptor.Descriptor(
+ name='ModifyIssueApprovalValuesRequest',
+ full_name='monorail.v3.ModifyIssueApprovalValuesRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='deltas', full_name='monorail.v3.ModifyIssueApprovalValuesRequest.deltas', 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),
+ _descriptor.FieldDescriptor(
+ name='notify_type', full_name='monorail.v3.ModifyIssueApprovalValuesRequest.notify_type', index=1,
+ number=2, type=14, cpp_type=8, 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='comment_content', full_name='monorail.v3.ModifyIssueApprovalValuesRequest.comment_content', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1792,
+ serialized_end=1941,
+)
+
+
+_MODIFYISSUEAPPROVALVALUESRESPONSE = _descriptor.Descriptor(
+ name='ModifyIssueApprovalValuesResponse',
+ full_name='monorail.v3.ModifyIssueApprovalValuesResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='approval_values', full_name='monorail.v3.ModifyIssueApprovalValuesResponse.approval_values', 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='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1943,
+ serialized_end=2031,
+)
+
+
+_LISTAPPROVALVALUESREQUEST = _descriptor.Descriptor(
+ name='ListApprovalValuesRequest',
+ full_name='monorail.v3.ListApprovalValuesRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='parent', full_name='monorail.v3.ListApprovalValuesRequest.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=b'\372A\025\n\023api.crbug.com/Issue\340A\002', 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=2033,
+ serialized_end=2105,
+)
+
+
+_LISTAPPROVALVALUESRESPONSE = _descriptor.Descriptor(
+ name='ListApprovalValuesResponse',
+ full_name='monorail.v3.ListApprovalValuesResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='approval_values', full_name='monorail.v3.ListApprovalValuesResponse.approval_values', 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='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2107,
+ serialized_end=2188,
+)
+
+
+_MODIFYCOMMENTSTATEREQUEST = _descriptor.Descriptor(
+ name='ModifyCommentStateRequest',
+ full_name='monorail.v3.ModifyCommentStateRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.ModifyCommentStateRequest.name', 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=b'\372A\027\n\025api.crbug.com/Comment\340A\002', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='state', full_name='monorail.v3.ModifyCommentStateRequest.state', index=1,
+ number=2, type=14, cpp_type=8, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2190,
+ serialized_end=2309,
+)
+
+
+_MODIFYCOMMENTSTATERESPONSE = _descriptor.Descriptor(
+ name='ModifyCommentStateResponse',
+ full_name='monorail.v3.ModifyCommentStateResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='comment', full_name='monorail.v3.ModifyCommentStateResponse.comment', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ 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=2311,
+ serialized_end=2378,
+)
+
+
+_MAKEISSUEFROMTEMPLATEREQUEST = _descriptor.Descriptor(
+ name='MakeIssueFromTemplateRequest',
+ full_name='monorail.v3.MakeIssueFromTemplateRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='template', full_name='monorail.v3.MakeIssueFromTemplateRequest.template', 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=b'\372A\030\n\026api.crbug.com/Template', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='template_issue_delta', full_name='monorail.v3.MakeIssueFromTemplateRequest.template_issue_delta', index=1,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ 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='template_approval_deltas', full_name='monorail.v3.MakeIssueFromTemplateRequest.template_approval_deltas', index=2,
+ number=3, 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='description', full_name='monorail.v3.MakeIssueFromTemplateRequest.description', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2381,
+ serialized_end=2596,
+)
+
+
+_MAKEISSUEREQUEST = _descriptor.Descriptor(
+ name='MakeIssueRequest',
+ full_name='monorail.v3.MakeIssueRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='parent', full_name='monorail.v3.MakeIssueRequest.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=b'\372A\027\n\025api.crbug.com/Project\340A\002', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='issue', full_name='monorail.v3.MakeIssueRequest.issue', index=1,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ 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.v3.MakeIssueRequest.description', 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='notify_type', full_name='monorail.v3.MakeIssueRequest.notify_type', index=3,
+ number=4, type=14, cpp_type=8, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2599,
+ serialized_end=2766,
+)
+
+_BATCHGETISSUESRESPONSE.fields_by_name['issues'].message_type = api_dot_v3_dot_api__proto_dot_issue__objects__pb2._ISSUE
+_SEARCHISSUESRESPONSE.fields_by_name['issues'].message_type = api_dot_v3_dot_api__proto_dot_issue__objects__pb2._ISSUE
+_LISTCOMMENTSRESPONSE.fields_by_name['comments'].message_type = api_dot_v3_dot_api__proto_dot_issue__objects__pb2._COMMENT
+_ISSUEDELTA.fields_by_name['issue'].message_type = api_dot_v3_dot_api__proto_dot_issue__objects__pb2._ISSUE
+_ISSUEDELTA.fields_by_name['update_mask'].message_type = google_dot_protobuf_dot_field__mask__pb2._FIELDMASK
+_ISSUEDELTA.fields_by_name['blocked_on_issues_remove'].message_type = api_dot_v3_dot_api__proto_dot_issue__objects__pb2._ISSUEREF
+_ISSUEDELTA.fields_by_name['blocking_issues_remove'].message_type = api_dot_v3_dot_api__proto_dot_issue__objects__pb2._ISSUEREF
+_ISSUEDELTA.fields_by_name['field_vals_remove'].message_type = api_dot_v3_dot_api__proto_dot_issue__objects__pb2._FIELDVALUE
+_APPROVALDELTA.fields_by_name['approval_value'].message_type = api_dot_v3_dot_api__proto_dot_issue__objects__pb2._APPROVALVALUE
+_APPROVALDELTA.fields_by_name['update_mask'].message_type = google_dot_protobuf_dot_field__mask__pb2._FIELDMASK
+_APPROVALDELTA.fields_by_name['field_vals_remove'].message_type = api_dot_v3_dot_api__proto_dot_issue__objects__pb2._FIELDVALUE
+_MODIFYISSUESREQUEST.fields_by_name['deltas'].message_type = _ISSUEDELTA
+_MODIFYISSUESREQUEST.fields_by_name['notify_type'].enum_type = _NOTIFYTYPE
+_MODIFYISSUESREQUEST.fields_by_name['uploads'].message_type = _ATTACHMENTUPLOAD
+_MODIFYISSUESRESPONSE.fields_by_name['issues'].message_type = api_dot_v3_dot_api__proto_dot_issue__objects__pb2._ISSUE
+_MODIFYISSUEAPPROVALVALUESREQUEST.fields_by_name['deltas'].message_type = _APPROVALDELTA
+_MODIFYISSUEAPPROVALVALUESREQUEST.fields_by_name['notify_type'].enum_type = _NOTIFYTYPE
+_MODIFYISSUEAPPROVALVALUESRESPONSE.fields_by_name['approval_values'].message_type = api_dot_v3_dot_api__proto_dot_issue__objects__pb2._APPROVALVALUE
+_LISTAPPROVALVALUESRESPONSE.fields_by_name['approval_values'].message_type = api_dot_v3_dot_api__proto_dot_issue__objects__pb2._APPROVALVALUE
+_MODIFYCOMMENTSTATEREQUEST.fields_by_name['state'].enum_type = api_dot_v3_dot_api__proto_dot_issue__objects__pb2._ISSUECONTENTSTATE
+_MODIFYCOMMENTSTATERESPONSE.fields_by_name['comment'].message_type = api_dot_v3_dot_api__proto_dot_issue__objects__pb2._COMMENT
+_MAKEISSUEFROMTEMPLATEREQUEST.fields_by_name['template_issue_delta'].message_type = _ISSUEDELTA
+_MAKEISSUEFROMTEMPLATEREQUEST.fields_by_name['template_approval_deltas'].message_type = _APPROVALDELTA
+_MAKEISSUEREQUEST.fields_by_name['issue'].message_type = api_dot_v3_dot_api__proto_dot_issue__objects__pb2._ISSUE
+_MAKEISSUEREQUEST.fields_by_name['notify_type'].enum_type = _NOTIFYTYPE
+DESCRIPTOR.message_types_by_name['GetIssueRequest'] = _GETISSUEREQUEST
+DESCRIPTOR.message_types_by_name['BatchGetIssuesRequest'] = _BATCHGETISSUESREQUEST
+DESCRIPTOR.message_types_by_name['BatchGetIssuesResponse'] = _BATCHGETISSUESRESPONSE
+DESCRIPTOR.message_types_by_name['SearchIssuesRequest'] = _SEARCHISSUESREQUEST
+DESCRIPTOR.message_types_by_name['SearchIssuesResponse'] = _SEARCHISSUESRESPONSE
+DESCRIPTOR.message_types_by_name['ListCommentsRequest'] = _LISTCOMMENTSREQUEST
+DESCRIPTOR.message_types_by_name['ListCommentsResponse'] = _LISTCOMMENTSRESPONSE
+DESCRIPTOR.message_types_by_name['AttachmentUpload'] = _ATTACHMENTUPLOAD
+DESCRIPTOR.message_types_by_name['IssueDelta'] = _ISSUEDELTA
+DESCRIPTOR.message_types_by_name['ApprovalDelta'] = _APPROVALDELTA
+DESCRIPTOR.message_types_by_name['ModifyIssuesRequest'] = _MODIFYISSUESREQUEST
+DESCRIPTOR.message_types_by_name['ModifyIssuesResponse'] = _MODIFYISSUESRESPONSE
+DESCRIPTOR.message_types_by_name['ModifyIssueApprovalValuesRequest'] = _MODIFYISSUEAPPROVALVALUESREQUEST
+DESCRIPTOR.message_types_by_name['ModifyIssueApprovalValuesResponse'] = _MODIFYISSUEAPPROVALVALUESRESPONSE
+DESCRIPTOR.message_types_by_name['ListApprovalValuesRequest'] = _LISTAPPROVALVALUESREQUEST
+DESCRIPTOR.message_types_by_name['ListApprovalValuesResponse'] = _LISTAPPROVALVALUESRESPONSE
+DESCRIPTOR.message_types_by_name['ModifyCommentStateRequest'] = _MODIFYCOMMENTSTATEREQUEST
+DESCRIPTOR.message_types_by_name['ModifyCommentStateResponse'] = _MODIFYCOMMENTSTATERESPONSE
+DESCRIPTOR.message_types_by_name['MakeIssueFromTemplateRequest'] = _MAKEISSUEFROMTEMPLATEREQUEST
+DESCRIPTOR.message_types_by_name['MakeIssueRequest'] = _MAKEISSUEREQUEST
+DESCRIPTOR.enum_types_by_name['NotifyType'] = _NOTIFYTYPE
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+GetIssueRequest = _reflection.GeneratedProtocolMessageType('GetIssueRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _GETISSUEREQUEST,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.GetIssueRequest)
+ })
+_sym_db.RegisterMessage(GetIssueRequest)
+
+BatchGetIssuesRequest = _reflection.GeneratedProtocolMessageType('BatchGetIssuesRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _BATCHGETISSUESREQUEST,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.BatchGetIssuesRequest)
+ })
+_sym_db.RegisterMessage(BatchGetIssuesRequest)
+
+BatchGetIssuesResponse = _reflection.GeneratedProtocolMessageType('BatchGetIssuesResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _BATCHGETISSUESRESPONSE,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.BatchGetIssuesResponse)
+ })
+_sym_db.RegisterMessage(BatchGetIssuesResponse)
+
+SearchIssuesRequest = _reflection.GeneratedProtocolMessageType('SearchIssuesRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _SEARCHISSUESREQUEST,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.SearchIssuesRequest)
+ })
+_sym_db.RegisterMessage(SearchIssuesRequest)
+
+SearchIssuesResponse = _reflection.GeneratedProtocolMessageType('SearchIssuesResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _SEARCHISSUESRESPONSE,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.SearchIssuesResponse)
+ })
+_sym_db.RegisterMessage(SearchIssuesResponse)
+
+ListCommentsRequest = _reflection.GeneratedProtocolMessageType('ListCommentsRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _LISTCOMMENTSREQUEST,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ListCommentsRequest)
+ })
+_sym_db.RegisterMessage(ListCommentsRequest)
+
+ListCommentsResponse = _reflection.GeneratedProtocolMessageType('ListCommentsResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _LISTCOMMENTSRESPONSE,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ListCommentsResponse)
+ })
+_sym_db.RegisterMessage(ListCommentsResponse)
+
+AttachmentUpload = _reflection.GeneratedProtocolMessageType('AttachmentUpload', (_message.Message,), {
+ 'DESCRIPTOR' : _ATTACHMENTUPLOAD,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.AttachmentUpload)
+ })
+_sym_db.RegisterMessage(AttachmentUpload)
+
+IssueDelta = _reflection.GeneratedProtocolMessageType('IssueDelta', (_message.Message,), {
+ 'DESCRIPTOR' : _ISSUEDELTA,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.IssueDelta)
+ })
+_sym_db.RegisterMessage(IssueDelta)
+
+ApprovalDelta = _reflection.GeneratedProtocolMessageType('ApprovalDelta', (_message.Message,), {
+ 'DESCRIPTOR' : _APPROVALDELTA,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ApprovalDelta)
+ })
+_sym_db.RegisterMessage(ApprovalDelta)
+
+ModifyIssuesRequest = _reflection.GeneratedProtocolMessageType('ModifyIssuesRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _MODIFYISSUESREQUEST,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ModifyIssuesRequest)
+ })
+_sym_db.RegisterMessage(ModifyIssuesRequest)
+
+ModifyIssuesResponse = _reflection.GeneratedProtocolMessageType('ModifyIssuesResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _MODIFYISSUESRESPONSE,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ModifyIssuesResponse)
+ })
+_sym_db.RegisterMessage(ModifyIssuesResponse)
+
+ModifyIssueApprovalValuesRequest = _reflection.GeneratedProtocolMessageType('ModifyIssueApprovalValuesRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _MODIFYISSUEAPPROVALVALUESREQUEST,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ModifyIssueApprovalValuesRequest)
+ })
+_sym_db.RegisterMessage(ModifyIssueApprovalValuesRequest)
+
+ModifyIssueApprovalValuesResponse = _reflection.GeneratedProtocolMessageType('ModifyIssueApprovalValuesResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _MODIFYISSUEAPPROVALVALUESRESPONSE,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ModifyIssueApprovalValuesResponse)
+ })
+_sym_db.RegisterMessage(ModifyIssueApprovalValuesResponse)
+
+ListApprovalValuesRequest = _reflection.GeneratedProtocolMessageType('ListApprovalValuesRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _LISTAPPROVALVALUESREQUEST,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ListApprovalValuesRequest)
+ })
+_sym_db.RegisterMessage(ListApprovalValuesRequest)
+
+ListApprovalValuesResponse = _reflection.GeneratedProtocolMessageType('ListApprovalValuesResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _LISTAPPROVALVALUESRESPONSE,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ListApprovalValuesResponse)
+ })
+_sym_db.RegisterMessage(ListApprovalValuesResponse)
+
+ModifyCommentStateRequest = _reflection.GeneratedProtocolMessageType('ModifyCommentStateRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _MODIFYCOMMENTSTATEREQUEST,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ModifyCommentStateRequest)
+ })
+_sym_db.RegisterMessage(ModifyCommentStateRequest)
+
+ModifyCommentStateResponse = _reflection.GeneratedProtocolMessageType('ModifyCommentStateResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _MODIFYCOMMENTSTATERESPONSE,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ModifyCommentStateResponse)
+ })
+_sym_db.RegisterMessage(ModifyCommentStateResponse)
+
+MakeIssueFromTemplateRequest = _reflection.GeneratedProtocolMessageType('MakeIssueFromTemplateRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _MAKEISSUEFROMTEMPLATEREQUEST,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.MakeIssueFromTemplateRequest)
+ })
+_sym_db.RegisterMessage(MakeIssueFromTemplateRequest)
+
+MakeIssueRequest = _reflection.GeneratedProtocolMessageType('MakeIssueRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _MAKEISSUEREQUEST,
+ '__module__' : 'api.v3.api_proto.issues_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.MakeIssueRequest)
+ })
+_sym_db.RegisterMessage(MakeIssueRequest)
+
+
+DESCRIPTOR._options = None
+_GETISSUEREQUEST.fields_by_name['name']._options = None
+_BATCHGETISSUESREQUEST.fields_by_name['parent']._options = None
+_BATCHGETISSUESREQUEST.fields_by_name['names']._options = None
+_SEARCHISSUESREQUEST.fields_by_name['projects']._options = None
+_LISTCOMMENTSREQUEST.fields_by_name['parent']._options = None
+_ATTACHMENTUPLOAD.fields_by_name['filename']._options = None
+_ATTACHMENTUPLOAD.fields_by_name['content']._options = None
+_ISSUEDELTA.fields_by_name['issue']._options = None
+_ISSUEDELTA.fields_by_name['update_mask']._options = None
+_ISSUEDELTA.fields_by_name['ccs_remove']._options = None
+_ISSUEDELTA.fields_by_name['components_remove']._options = None
+_APPROVALDELTA.fields_by_name['update_mask']._options = None
+_APPROVALDELTA.fields_by_name['approvers_remove']._options = None
+_LISTAPPROVALVALUESREQUEST.fields_by_name['parent']._options = None
+_MODIFYCOMMENTSTATEREQUEST.fields_by_name['name']._options = None
+_MAKEISSUEFROMTEMPLATEREQUEST.fields_by_name['template']._options = None
+_MAKEISSUEREQUEST.fields_by_name['parent']._options = None
+
+_ISSUES = _descriptor.ServiceDescriptor(
+ name='Issues',
+ full_name='monorail.v3.Issues',
+ file=DESCRIPTOR,
+ index=0,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ serialized_start=2844,
+ serialized_end=3762,
+ methods=[
+ _descriptor.MethodDescriptor(
+ name='GetIssue',
+ full_name='monorail.v3.Issues.GetIssue',
+ index=0,
+ containing_service=None,
+ input_type=_GETISSUEREQUEST,
+ output_type=api_dot_v3_dot_api__proto_dot_issue__objects__pb2._ISSUE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='BatchGetIssues',
+ full_name='monorail.v3.Issues.BatchGetIssues',
+ index=1,
+ containing_service=None,
+ input_type=_BATCHGETISSUESREQUEST,
+ output_type=_BATCHGETISSUESRESPONSE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='SearchIssues',
+ full_name='monorail.v3.Issues.SearchIssues',
+ index=2,
+ containing_service=None,
+ input_type=_SEARCHISSUESREQUEST,
+ output_type=_SEARCHISSUESRESPONSE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListComments',
+ full_name='monorail.v3.Issues.ListComments',
+ index=3,
+ containing_service=None,
+ input_type=_LISTCOMMENTSREQUEST,
+ output_type=_LISTCOMMENTSRESPONSE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ModifyIssues',
+ full_name='monorail.v3.Issues.ModifyIssues',
+ index=4,
+ containing_service=None,
+ input_type=_MODIFYISSUESREQUEST,
+ output_type=_MODIFYISSUESRESPONSE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ModifyIssueApprovalValues',
+ full_name='monorail.v3.Issues.ModifyIssueApprovalValues',
+ index=5,
+ containing_service=None,
+ input_type=_MODIFYISSUEAPPROVALVALUESREQUEST,
+ output_type=_MODIFYISSUEAPPROVALVALUESRESPONSE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListApprovalValues',
+ full_name='monorail.v3.Issues.ListApprovalValues',
+ index=6,
+ containing_service=None,
+ input_type=_LISTAPPROVALVALUESREQUEST,
+ output_type=_LISTAPPROVALVALUESRESPONSE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ModifyCommentState',
+ full_name='monorail.v3.Issues.ModifyCommentState',
+ index=7,
+ containing_service=None,
+ input_type=_MODIFYCOMMENTSTATEREQUEST,
+ output_type=_MODIFYCOMMENTSTATERESPONSE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='MakeIssueFromTemplate',
+ full_name='monorail.v3.Issues.MakeIssueFromTemplate',
+ index=8,
+ containing_service=None,
+ input_type=_MAKEISSUEFROMTEMPLATEREQUEST,
+ output_type=api_dot_v3_dot_api__proto_dot_issue__objects__pb2._ISSUE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='MakeIssue',
+ full_name='monorail.v3.Issues.MakeIssue',
+ index=9,
+ containing_service=None,
+ input_type=_MAKEISSUEREQUEST,
+ output_type=api_dot_v3_dot_api__proto_dot_issue__objects__pb2._ISSUE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+])
+_sym_db.RegisterServiceDescriptor(_ISSUES)
+
+DESCRIPTOR.services_by_name['Issues'] = _ISSUES
+
+# @@protoc_insertion_point(module_scope)
diff --git a/api/v3/api_proto/issues_prpc_pb2.py b/api/v3/api_proto/issues_prpc_pb2.py
new file mode 100644
index 0000000..8bf1235
--- /dev/null
+++ b/api/v3/api_proto/issues_prpc_pb2.py
@@ -0,0 +1,822 @@
+# Generated by the pRPC protocol buffer compiler plugin. DO NOT EDIT!
+# source: api/v3/api_proto/issues.proto
+
+import base64
+import zlib
+
+from google.protobuf import descriptor_pb2
+
+# Includes description of the api/v3/api_proto/issues.proto and all of its transitive
+# dependencies. Includes source code info.
+FILE_DESCRIPTOR_SET = descriptor_pb2.FileDescriptorSet()
+FILE_DESCRIPTOR_SET.ParseFromString(zlib.decompress(base64.b64decode(
+ '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'
+ 'wX+52ZC4D/Bpwj87Y=')))
+_INDEX = {
+ f.name: {
+ 'descriptor': f,
+ 'services': {s.name: s for s in f.service},
+ }
+ for f in FILE_DESCRIPTOR_SET.file
+}
+
+
+IssuesServiceDescription = {
+ 'file_descriptor_set': FILE_DESCRIPTOR_SET,
+ 'file_descriptor': _INDEX[u'api/v3/api_proto/issues.proto']['descriptor'],
+ 'service_descriptor': _INDEX[u'api/v3/api_proto/issues.proto']['services'][u'Issues'],
+}
diff --git a/api/v3/api_proto/permission_objects.proto b/api/v3/api_proto/permission_objects.proto
new file mode 100644
index 0000000..60f8169
--- /dev/null
+++ b/api/v3/api_proto/permission_objects.proto
@@ -0,0 +1,43 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+// This file defines protobufs for features and related business
+// objects, e.g., hotlists.
+
+syntax = "proto3";
+
+package monorail.v3;
+
+option go_package = "api/v3/api_proto";
+
+// All possible permissions on the Monorail site.
+// Next available tag: 6
+enum Permission {
+ // Default value. This value is unused.
+ PERMISSION_UNSPECIFIED = 0;
+ // The permission needed to add and remove issues from a hotlist.
+ HOTLIST_EDIT = 1;
+ // The permission needed to delete a hotlist or change hotlist
+ // settings/members.
+ HOTLIST_ADMINISTER = 2;
+ // The permission needed to edit an issue.
+ ISSUE_EDIT = 3;
+ // The permission needed to edit a custom field definition.
+ FIELD_DEF_EDIT = 4;
+ // The permission needed to edit the value of a custom field.
+ // More permissions will be required in the specific issue
+ // where the user plans to edit that value, e.g. ISSUE_EDIT.
+ FIELD_DEF_VALUE_EDIT = 5;
+}
+
+
+// The set of a user's permissions for a single resource.
+// Next available tag: 3
+message PermissionSet {
+ // The name of the resource `permissions` applies to.
+ string resource = 1;
+ // All the permissions a user has for `resource`.
+ repeated Permission permissions = 2;
+}
diff --git a/api/v3/api_proto/permission_objects_pb2.py b/api/v3/api_proto/permission_objects_pb2.py
new file mode 100644
index 0000000..2ea90db
--- /dev/null
+++ b/api/v3/api_proto/permission_objects_pb2.py
@@ -0,0 +1,133 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/v3/api_proto/permission_objects.proto
+
+from google.protobuf.internal import enum_type_wrapper
+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/v3/api_proto/permission_objects.proto',
+ package='monorail.v3',
+ syntax='proto3',
+ serialized_options=b'Z\020api/v3/api_proto',
+ create_key=_descriptor._internal_create_key,
+ serialized_pb=b'\n)api/v3/api_proto/permission_objects.proto\x12\x0bmonorail.v3\"O\n\rPermissionSet\x12\x10\n\x08resource\x18\x01 \x01(\t\x12,\n\x0bpermissions\x18\x02 \x03(\x0e\x32\x17.monorail.v3.Permission*\x90\x01\n\nPermission\x12\x1a\n\x16PERMISSION_UNSPECIFIED\x10\x00\x12\x10\n\x0cHOTLIST_EDIT\x10\x01\x12\x16\n\x12HOTLIST_ADMINISTER\x10\x02\x12\x0e\n\nISSUE_EDIT\x10\x03\x12\x12\n\x0e\x46IELD_DEF_EDIT\x10\x04\x12\x18\n\x14\x46IELD_DEF_VALUE_EDIT\x10\x05\x42\x12Z\x10\x61pi/v3/api_protob\x06proto3'
+)
+
+_PERMISSION = _descriptor.EnumDescriptor(
+ name='Permission',
+ full_name='monorail.v3.Permission',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='PERMISSION_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='HOTLIST_EDIT', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='HOTLIST_ADMINISTER', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='ISSUE_EDIT', index=3, number=3,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='FIELD_DEF_EDIT', index=4, number=4,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='FIELD_DEF_VALUE_EDIT', index=5, number=5,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=140,
+ serialized_end=284,
+)
+_sym_db.RegisterEnumDescriptor(_PERMISSION)
+
+Permission = enum_type_wrapper.EnumTypeWrapper(_PERMISSION)
+PERMISSION_UNSPECIFIED = 0
+HOTLIST_EDIT = 1
+HOTLIST_ADMINISTER = 2
+ISSUE_EDIT = 3
+FIELD_DEF_EDIT = 4
+FIELD_DEF_VALUE_EDIT = 5
+
+
+
+_PERMISSIONSET = _descriptor.Descriptor(
+ name='PermissionSet',
+ full_name='monorail.v3.PermissionSet',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='resource', full_name='monorail.v3.PermissionSet.resource', 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='permissions', full_name='monorail.v3.PermissionSet.permissions', index=1,
+ number=2, type=14, cpp_type=8, 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='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=58,
+ serialized_end=137,
+)
+
+_PERMISSIONSET.fields_by_name['permissions'].enum_type = _PERMISSION
+DESCRIPTOR.message_types_by_name['PermissionSet'] = _PERMISSIONSET
+DESCRIPTOR.enum_types_by_name['Permission'] = _PERMISSION
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+PermissionSet = _reflection.GeneratedProtocolMessageType('PermissionSet', (_message.Message,), {
+ 'DESCRIPTOR' : _PERMISSIONSET,
+ '__module__' : 'api.v3.api_proto.permission_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.PermissionSet)
+ })
+_sym_db.RegisterMessage(PermissionSet)
+
+
+DESCRIPTOR._options = None
+# @@protoc_insertion_point(module_scope)
diff --git a/api/v3/api_proto/permissions.proto b/api/v3/api_proto/permissions.proto
new file mode 100644
index 0000000..9efba1f
--- /dev/null
+++ b/api/v3/api_proto/permissions.proto
@@ -0,0 +1,61 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+syntax = "proto3";
+
+package monorail.v3;
+
+option go_package = "api/v3/api_proto";
+
+import "google/api/field_behavior.proto";
+import "api/v3/api_proto/permission_objects.proto";
+
+// ***DO NOT CALL rpcs IN THIS SERVICE.***
+// This service is for Monorail's frontend only.
+
+// Permissions service includes all methods needed for fetching permissions.
+service Permissions {
+ // status: DO NOT USE
+ // Returns the requester's permissions for the given resource.
+ //
+ // Raises:
+ // PERMISSION_DENIED if the given resource does not exist and/or the
+ // requester does not have permission to view the resource's name space.
+ // NOT_FOUND if the given resource does not exist.
+ rpc GetPermissionSet (GetPermissionSetRequest) returns (PermissionSet) {}
+
+ // status: DO NOT USE
+ // Returns the requester's permissions for all the given resources.
+ //
+ // Raises:
+ // PERMISSION_DENIED if any of the given resources do not exist and/or the
+ // requester does not have permission to view one of the resource's
+ // name space.
+ // NOT_FOUND if one of the given resources do not exist.
+ rpc BatchGetPermissionSets (BatchGetPermissionSetsRequest) returns (BatchGetPermissionSetsResponse) {}
+}
+
+
+// Request message for the GetPermissionSet emthod.
+// Next available tag: 2
+message GetPermissionSetRequest {
+ // The resource name of the resource permissions to retrieve.
+ string name = 1 [ (google.api.field_behavior) = REQUIRED ];
+}
+
+
+// Request message for the BatchGetPermissionSets method.
+// Next available tag: 2
+message BatchGetPermissionSetsRequest {
+ // The resource names of the resource permissions to retrieve.
+ repeated string names = 1 [ (google.api.field_behavior) = REQUIRED ];
+}
+
+
+// Response message for the BatchGetPermissionSets method.
+// Next available tag: 2
+message BatchGetPermissionSetsResponse {
+ // The Permissions, one for each of the given resources.
+ repeated PermissionSet permission_sets = 1;
+}
diff --git a/api/v3/api_proto/permissions_pb2.py b/api/v3/api_proto/permissions_pb2.py
new file mode 100644
index 0000000..c22e0ab
--- /dev/null
+++ b/api/v3/api_proto/permissions_pb2.py
@@ -0,0 +1,193 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/v3/api_proto/permissions.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()
+
+
+from google.api import field_behavior_pb2 as google_dot_api_dot_field__behavior__pb2
+from api.v3.api_proto import permission_objects_pb2 as api_dot_v3_dot_api__proto_dot_permission__objects__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='api/v3/api_proto/permissions.proto',
+ package='monorail.v3',
+ syntax='proto3',
+ serialized_options=b'Z\020api/v3/api_proto',
+ create_key=_descriptor._internal_create_key,
+ serialized_pb=b'\n\"api/v3/api_proto/permissions.proto\x12\x0bmonorail.v3\x1a\x1fgoogle/api/field_behavior.proto\x1a)api/v3/api_proto/permission_objects.proto\",\n\x17GetPermissionSetRequest\x12\x11\n\x04name\x18\x01 \x01(\tB\x03\xe0\x41\x02\"3\n\x1d\x42\x61tchGetPermissionSetsRequest\x12\x12\n\x05names\x18\x01 \x03(\tB\x03\xe0\x41\x02\"U\n\x1e\x42\x61tchGetPermissionSetsResponse\x12\x33\n\x0fpermission_sets\x18\x01 \x03(\x0b\x32\x1a.monorail.v3.PermissionSet2\xda\x01\n\x0bPermissions\x12V\n\x10GetPermissionSet\x12$.monorail.v3.GetPermissionSetRequest\x1a\x1a.monorail.v3.PermissionSet\"\x00\x12s\n\x16\x42\x61tchGetPermissionSets\x12*.monorail.v3.BatchGetPermissionSetsRequest\x1a+.monorail.v3.BatchGetPermissionSetsResponse\"\x00\x42\x12Z\x10\x61pi/v3/api_protob\x06proto3'
+ ,
+ dependencies=[google_dot_api_dot_field__behavior__pb2.DESCRIPTOR,api_dot_v3_dot_api__proto_dot_permission__objects__pb2.DESCRIPTOR,])
+
+
+
+
+_GETPERMISSIONSETREQUEST = _descriptor.Descriptor(
+ name='GetPermissionSetRequest',
+ full_name='monorail.v3.GetPermissionSetRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.GetPermissionSetRequest.name', 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=b'\340A\002', 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=127,
+ serialized_end=171,
+)
+
+
+_BATCHGETPERMISSIONSETSREQUEST = _descriptor.Descriptor(
+ name='BatchGetPermissionSetsRequest',
+ full_name='monorail.v3.BatchGetPermissionSetsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='names', full_name='monorail.v3.BatchGetPermissionSetsRequest.names', index=0,
+ number=1, 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=b'\340A\002', 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=173,
+ serialized_end=224,
+)
+
+
+_BATCHGETPERMISSIONSETSRESPONSE = _descriptor.Descriptor(
+ name='BatchGetPermissionSetsResponse',
+ full_name='monorail.v3.BatchGetPermissionSetsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='permission_sets', full_name='monorail.v3.BatchGetPermissionSetsResponse.permission_sets', 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='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=226,
+ serialized_end=311,
+)
+
+_BATCHGETPERMISSIONSETSRESPONSE.fields_by_name['permission_sets'].message_type = api_dot_v3_dot_api__proto_dot_permission__objects__pb2._PERMISSIONSET
+DESCRIPTOR.message_types_by_name['GetPermissionSetRequest'] = _GETPERMISSIONSETREQUEST
+DESCRIPTOR.message_types_by_name['BatchGetPermissionSetsRequest'] = _BATCHGETPERMISSIONSETSREQUEST
+DESCRIPTOR.message_types_by_name['BatchGetPermissionSetsResponse'] = _BATCHGETPERMISSIONSETSRESPONSE
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+GetPermissionSetRequest = _reflection.GeneratedProtocolMessageType('GetPermissionSetRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _GETPERMISSIONSETREQUEST,
+ '__module__' : 'api.v3.api_proto.permissions_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.GetPermissionSetRequest)
+ })
+_sym_db.RegisterMessage(GetPermissionSetRequest)
+
+BatchGetPermissionSetsRequest = _reflection.GeneratedProtocolMessageType('BatchGetPermissionSetsRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _BATCHGETPERMISSIONSETSREQUEST,
+ '__module__' : 'api.v3.api_proto.permissions_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.BatchGetPermissionSetsRequest)
+ })
+_sym_db.RegisterMessage(BatchGetPermissionSetsRequest)
+
+BatchGetPermissionSetsResponse = _reflection.GeneratedProtocolMessageType('BatchGetPermissionSetsResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _BATCHGETPERMISSIONSETSRESPONSE,
+ '__module__' : 'api.v3.api_proto.permissions_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.BatchGetPermissionSetsResponse)
+ })
+_sym_db.RegisterMessage(BatchGetPermissionSetsResponse)
+
+
+DESCRIPTOR._options = None
+_GETPERMISSIONSETREQUEST.fields_by_name['name']._options = None
+_BATCHGETPERMISSIONSETSREQUEST.fields_by_name['names']._options = None
+
+_PERMISSIONS = _descriptor.ServiceDescriptor(
+ name='Permissions',
+ full_name='monorail.v3.Permissions',
+ file=DESCRIPTOR,
+ index=0,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ serialized_start=314,
+ serialized_end=532,
+ methods=[
+ _descriptor.MethodDescriptor(
+ name='GetPermissionSet',
+ full_name='monorail.v3.Permissions.GetPermissionSet',
+ index=0,
+ containing_service=None,
+ input_type=_GETPERMISSIONSETREQUEST,
+ output_type=api_dot_v3_dot_api__proto_dot_permission__objects__pb2._PERMISSIONSET,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='BatchGetPermissionSets',
+ full_name='monorail.v3.Permissions.BatchGetPermissionSets',
+ index=1,
+ containing_service=None,
+ input_type=_BATCHGETPERMISSIONSETSREQUEST,
+ output_type=_BATCHGETPERMISSIONSETSRESPONSE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+])
+_sym_db.RegisterServiceDescriptor(_PERMISSIONS)
+
+DESCRIPTOR.services_by_name['Permissions'] = _PERMISSIONS
+
+# @@protoc_insertion_point(module_scope)
diff --git a/api/v3/api_proto/permissions_prpc_pb2.py b/api/v3/api_proto/permissions_prpc_pb2.py
new file mode 100644
index 0000000..3afdc60
--- /dev/null
+++ b/api/v3/api_proto/permissions_prpc_pb2.py
@@ -0,0 +1,432 @@
+# Generated by the pRPC protocol buffer compiler plugin. DO NOT EDIT!
+# source: api/v3/api_proto/permissions.proto
+
+import base64
+import zlib
+
+from google.protobuf import descriptor_pb2
+
+# Includes description of the api/v3/api_proto/permissions.proto and all of its transitive
+# dependencies. Includes source code info.
+FILE_DESCRIPTOR_SET = descriptor_pb2.FileDescriptorSet()
+FILE_DESCRIPTOR_SET.ParseFromString(zlib.decompress(base64.b64decode(
+ 'eJzlvX10ZFdxL+rTrZZaRxppq2fsGbdtfNz+GGksaTwzxsYzGEcjacYyGklXHzYmD8tHrSOp7V'
+ 'a36NMaWQ6skJtLIOTjhQAG5/GR8GXiBBICJHmwuLnckLsWuQlJ3kvIug8nl5XgBEJiPnwX4ZKQ'
+ '9+pXu/Y++3RrxgOxc/94XixGXWefOnvXrl27qnZVbf8jB/xSuFU5ev7EUfpneatRb9aPbkWNzU'
+ 'ocV+q1eJQhhZ7Neq3eCCvV0fMniteu1+vr1QgvHF2rRNXV5ZVoIzxfqTd06+LQRTAu11ceispN'
+ 'QVw67h88GzXn7OOFqDkfvXo7ipuFg35HLdyMDnmBN9h9Ovulscw8A0on/WtOh83yRuuLsXnzSj'
+ '+HhjG9mjWvakgp8l90oXfjLRpvVBj3+53exvSI0fQcL446RBhNd7lvK4Xs+P/j+T1Ji7hwr69a'
+ 'v1i4IYXvAlQoXuSrpcsKsX/F3sMpHEm9d1F6FW++pLaaPqXLThdeqVon+J6/7PfzqkNdpl6qPP'
+ '9JL9/LPwrH3+MF4/Wt3UZlfaMZHL/l+C3B4kYUjG806puV7c1gbLu5UW/Eo8FYtRpwozhoRHHU'
+ 'OB+tjvrBUhwF9bWguVGJg7i+3ShHQbm+GgX0c71+PmrUotVgZTcIg9MLEyNxc7ca+UG1Uo6on/'
+ 'RS2AzKYS1YiYK1+nZtNajUCBgF01PjkzMLk8FapRqN+n4+n1Gd1NcD9Fde5emvwwDme+zf2fxl'
+ 'yqe/h/hvT/XQ3yf8j3n5TnowQD9uUF7x5YEz3wFGQP2gT5ar26tRHIQ0vs2IBrsaB7UoWqWOr9'
+ 'UbwVpEtK7U1gN3yfnHXxEcOXJkYjaYmV0Mxsemp4PGVjkOpmaCxbunFoKFyfl7aQyj1MYnclac'
+ 'z8WM9ZzM5mH62ajXmhENvl6r7tJoe9FpGsMAjW6//5EM/8zQOK5UGfXy4rsyQdwMm9vxyUA+v7'
+ 'Qw6QfzUXO7QcMC+RqaaaIGYXd6zR/G8/XK+aiGSeT5ok8G82EljuKTfhDMTc6fm1pYmJqdWZ6Y'
+ 'nJmanAgqa3u8FKzWiWS1ejOIHqnEzSCsrR7V2AkH/rN9SFqSAIqc/gTNenC+Eu1IlzVe6jGkQB'
+ 'BvhehZgAEun5ldmrm0ftBYlKEXkfBKYpsrHEiGIIfUUQeSJchJddb/r4bMnrqeyLxR/OS/jsxg'
+ 'pvbexpdA67C2qxdU28s01ueJ4vVaZL6REF7QpMifor7z1sV65syBx+TsVFc7kAxBXqROOZAsQc'
+ '6oVf++fAdx+RCt1mFarVOByD5alHEcrkeWfVsFXxBtYtWiu9EjRJjztK7ClSrJl3D9ZHAcK6qD'
+ '2WGIVtS1/jT/woK6mWb6zuJLWd5ZhuLxtxAnNb1ExEbUbFSi8xEPVbDlc4wv70A8gnSrfQ4kSx'
+ 'ClBhxIniAF9VL/8gSSf2eeO3dAnfRfRWBPHSOa3Eo0mb0gTfbeEUSeXZwyHn8gr270z/EvUOYE'
+ 'jeRs8c52ysTfJ2kYXb6DEbqQHEF6hBCeEOsEEeKgA8kSpKiuciB5glytzjCxPJdYJ4ilJvwHCJ'
+ 'xRd+gtrjgXmA3x+aQWJMQdRK2b/LP8C9Q6RYMbKd7O1HK2mGFeMfhmFJY3LrB0hE4ZodMpSycN'
+ '6SRIjzrkQDyCXKkGHUiWIDer4ZVO3udP+K8r+s+l/xV83WCUGhQDacxPVrbXjtJuWG5Utpqm9Z'
+ 'Ef8fedAZbTgqTwIr94ZmpyemL59OTdY/dOzc4vL80szE2OTxF0Ql1W6PXzs3OLJNjGppWHX/OT'
+ '/25pap6eZQr9fs/s0uLc0uLy7Mz0/Spb6PP9qRn7u6Owz++eOnduaXHs9PSkyp180O9LD6Fwza'
+ 'j03vR4lHs3u9UE2Q+9M0/KYN/xK0eTMY6muj+/b839eXrL7yvXN53mpwup9nP4zJz3yjFpsV6v'
+ 'hrX10Xpj/eh6VNPqs35E78ZM9LBG8jDk/pxy/n5vpuPs2NzUPX92Beli/cSoM6SL/ccO0sX6WR'
+ 'f7WEdKFzt2R3CW8QbT0+PYO6a15rQakLIUaV4eI1ENlUk/GQ7ujRos64+P3hIMokFJHpWGTvnB'
+ 'bn072Ax3WVpvswoGhYTULJLd5WirCQ2MaLFVrYQ1Wtw7leaG1sg0Dlob9wuG+kozpMYhNd+yO5'
+ 'Y0C8Kmr7eTjWZz6+TRozs7O0RXdJSpJgpgfFQUvRHqLL2wVKvSQuWNrNIQvXGLulLmVVgNaeOi'
+ 'nXW9QcoZRA19fadRaZJuNkyq51pzJ2zQXrhKW1CjsrLdTFHJdIxG6zYgOpH6WRpbCKYWSsHpsY'
+ 'WphWE/uG9q8W5i0eC+sfn5sZnFqcmFYHY+GJ+dmZgCT9OvM8HYzP3By6dmJoaDiGhEX4ke2Wqg'
+ '99TFCujH2vFCFKU+bwRQvBWVK2uVcgBG2oZo0spyi56JTR7q8mZF+Kd9RMQVrB9Dyd0vmvB++v'
+ 'sI68qX01+vEF1Z/w3oFfRXiaG+/A3oQdaZ8af5G38dstq2J38DeqXFcIP9u0tdBQ2cGPqL2XwX'
+ 'dWOQZNnJ4uezwVhAEqWyXuMxgFXChAC8EgOztINBM/kkPLebW9tN1oqJyM3y6JCPKTdr3gh11q'
+ 'kmHwmJ5tCqwHaYXyIk7+R3BseCHx50JEFalgxRAyObXnUKLy8QqSPW/S7pZUeU6fdbRdPEdkOP'
+ 'u9msAqFeF8+BNZGHeyNdrNDomzRmsB2Ra7lZ0WO9JOxOn4dF63uO7hhxTL2hXbCLVemrVJe6Xn'
+ '518GSbZ530q8c+8+jXDeq4/MrSrxerO/y/z+RzxCPHtAws/kUmGKvR9K7SYqd9x8gTyxfMNHrr'
+ '1BwzyPuqnvZhbUmG+pGPRW5lSKVmFGPsxw3BEcaGvUgOBMSadbQlaKVGwKFRMdqOHDGq9JEjQX'
+ 'mD1mqU7pbhx3K9SpjW1kj2BpVmHFXXTtG/zLskkSJCEMXpN2llsxgN6aUyaer1nYA2BhJr9Sp4'
+ 'l761WpV3eFTg8hlCc1L3LKqRaX7kCGGgnjWickTDIqt1JzgfVmmwxpBe2yZzJWKjMsdTcUzlVM'
+ 'E/zb+Mondd8TjtOjWiC/g0rFKH18LtalOrLvQhMv4n6qktg7WWnFVzgeVqB5IhyLUq8N/vCchT'
+ 'L6Em/cW3eMGCrHyykHYtaWTqeF62dC9oDu7bwMYEU4qfxnuSm4S+fWdY72eg64oVshFxBI9kc2'
+ 'sjjOkxjKwtwrPVqNAid4biST9dSIYg+1Sf/7tmKGQy8FB+wwsm2ntv+M5wkHA0zySxqJ1OzN12'
+ 'DLaijlJfzldWNQNuhY1momMz49IaBbeskTpKk4mdj8ytuE6bM1GmHGJOaBOLGg3Iz+14myn74N'
+ 'TMvWPTUxPLY/Nnl85Nziw+OOSMFDNyZ2qkemAY6esyAsqqcWpSKH59z5E64vk5B0vP7Bh5QWq9'
+ 'nFYkLz/2wIDr2ZiVV6BaCAGsQct+GFZq2P/UCDQRZQFjxfGGyVBetYSxQSvLkieMGWsM7hYi8+'
+ 'dgwcOpRTqPSya4kkCDfQ4kQxCYcL9kGKJD3U1NBoo/tyeZWKD8gFQyYguzz6Qp1xuadkwt+xZW'
+ 'prix+E09Nc44YEOik70OJEOQfqX8D5px5NQ5aqKKb9t7HJub203oYc85DLP6IgybHWzOTBLzlh'
+ 'sR9tbQt0aQZgOjltK7WtTyoKnxWjNqOIPJ0WDQ0x4HkiFIn+q3JtDbfynrP6dZU+hv2VZL5/yB'
+ 'MyRyJmxDOIFf4ndAQRYX8w172B7uG2wrzPMbpS93+Pv3eFoouG5z7TEvHPK7SD9+mBSaQxkGm5'
+ '9ka/mr0VZEil+tvHsoC3/5vAMp3OwPbG2vkJa87DTzqVluXukHE0njw37/ThQ+7Dbt4aZ9ADsN'
+ 'x/1eUbCWm7tb0aEOHn3QNvrWkffIW4v0UmHM78b+oTHkLkC/SWrRiiWP1wRFl7hOD3UygsNtCB'
+ 'b081Yc5j0aSjfZ86Qs0x5xqIuR3Li3BdmKInmvcJvfVRf7Mk/z03P86j0ZQWzQedO4MOUrzeTL'
+ 'cIsvV2pr9UPdjODa9oFww3FqN0XN5vvi1O/CFX5nvFtrho8c6mUOkV+l/7PT778UFjvl53iFEo'
+ 'N9HzTQ76SJ2PkDEnHM76nBRbmqOSJ7iTzl65faWarjB2KpV/j9tkvLDUga4c2jz9WT0Unz3jxe'
+ 'm++LUr8LE75fr0X1NVpe5eqh/AWoNIsmbVSqa2i5WrgjYbWuC3DKOb3I2rhtye8zZzMysm7uxO'
+ 'hzjmxeXtMD29dwfxau9y1gmdnKZynUa4AzBCs+6velyVM44OfISGk0mQtz8/pHQflZEjIs5XLz'
+ '+LPwQ8mAszzgm9pnNIW5ddzF2/19qQFc6qdLr/Ev3xM1McmBbTLHaeshxQAcqz916G+7LsBzS2'
+ '5rjWV+/3Y78Eh3/itd6nX0X6b0mU7/wF5rZs/lS8ufOHglajCRcvPyi1ZErhquRFVaDd5g3/Gb'
+ 'L2lVjk7jlXn9ZuFlfoeIaGA4cmkYsJbm+b3CVX43/tW80cl9zgMAvigU/Twvk9XIbG32NxhLjI'
+ '1lNlyY4YmxBHgvYIVr/R69qkjliB5h6Zmb1wttChB8/qGY1rKwJn8CAP787a2C++Lew2Qt0Vap'
+ 'tYllY14cGiAE+fk+DZ4VaOk3M34HC5Z+v2fx/rnJ5YnZJbguPXg2GXBmenZsUWXs76mZxdtuVV'
+ 'n7wpIGdLgNThxXOWLYXo1g6hWTE9SiMw2hNl1wlzLk9OzstMpbnAuL81MzZ1W3xXl2fnZpTvkW'
+ 'w7nJhYWxs5Oqx7Y4ff/i5ILqTXWLPrHPfmJyZon0rMKAv09/wnSivwVEPVVJRzSWgRSAWhRK43'
+ '6O2ZDYvW967PTk9LLjNLYwx3XswOYmxxYJli2V/QN7CdQ9l5DDC5kL8ALjauWF0l9n/P17bCp7'
+ 'fuQuP6d5WW+zQ3vuTszZbVstv+eqGtkLqBpA0cawr2oT/np/vO1S9keGfX+bQG6PTeCUP9CG6J'
+ 'KF8Y97/qELEec5RGImJRJPtVLwugtPQttcP+H5V+ytUu7Zh5f5nfrcSOa7fe86x49bJ1vecnf7'
+ '7IX0Qt2btp7+ZMa/fE/ke3b0Gt9nY1SrTloSdzOEhRekLNuNRjfDc1+DuMFLko52cEdfdIGRtj'
+ 'HmLb4qVytRrbkcN8me2yTjlbea/MncWliNo/l+/XjBPMUb2sJ33uhMvaEf2zdKb+72exwFvHCd'
+ '3/tQeD5cNkaVpkQPYHNiWN3iH+AmNEb6ULkaxjETLc9NC3g2i0fj5knhxf5+fmOT9qbKVjVahp'
+ 'kX85ZjezaAFuekAXoUk1p4Db+2HtWiBtm+y2QMU9tlsuuXN8J449ABIDidOeTNX4mGZ6XdJDcb'
+ 'q63eTY0KJ/0rGIv2bi+XN6Lyw8vbzbWXHLrK/T73cIHbjKPJErUoLPi9mIzNyqPU53qD99C+PU'
+ 'STQ8HRWXnhHNkfJ3MLc5OTE/M9BssZHMP5/nrdErhHM9R63ZCXiFUu6zGTbSrGWHxIpYhVLp/V'
+ 'DYTHY1oPlyfEcl8caBtl66v0xa3d9hcLqS9u7ba+drt/YGtjq/29I+57BWrS+uKNbJk3IrhBVg'
+ '8ddJs7DwqjxP7l5agG78ly2KA/4kPXcuOOZmObrIhyeZIfjvGzwhF/oL7yUFlz5DKhWas8cugG'
+ 'Jm8/HjA/zjG4MES4442wscUimcM2Dt2om2r4jAFjRcQ7lbWmwXhYrwiGCbZBX4ESqQ8PcrM+gr'
+ 'vfpc0ALZOPDmnFjYDJF2/1r0AjEnThatgMndbD3BpkPycPU/1sbK/sWsYa0f0EzLDWC6acl076'
+ 'vS7fF7p9zfmkkJASND47AfXllZOki5AaNT21OLk8vzSzOHVuUmUdxf6ejvxN6nDpTzJ+X9pSK7'
+ 'zUP2jcKnHUXN7B2Q0tyM1Qb46Wfw5Iq4WoeR+1OcNNCtP+tbU6CQASHGFjdTlxaC2HZWLIuK43'
+ 'Qovl6lp9QRonO8SYNG1h3+yF2Je0681wi/i32dhl/Tw/nyfAJH7/m5hJRM286qb/71Z+6a+yfq'
+ '+rr8P8KfOO5bFMu/6i2v3oOLayk51aOZ7Xb0KNALNFWhnJz8uvwlm/86GYcXcy7r18fw7uexYY'
+ 'efc9C8szs/Pnxqbn5fXClX5HNXx0N73pMehSJ4EwwEGX3moY9AIuhqN+julV8H2hmLqskPc7xm'
+ 'fnsSBoBWjo8tzU5DitidKL/U5NBCwWSwZ6Sf8UHJ55unTu9OS8yqSnukPlSjGtQkcP/7cxxv+T'
+ '5/c4ejUUorBare8sh9VKGAtr+AwaA+RSp+7faInkVGfp3Z6vWhXblm56/yu7WXqn5/eltdmW7l'
+ '33v7R7X8r4+1I67KX27tX+QGU12tyqN+E8X65G56PqoRILjXanYuoLo1PJe9N47eT+qYnJc3Oz'
+ 'i5Mz4/cvL828fGb2vpl5VWlp9gIu+zlftXaqcNDfq1u0svf7/TOztCfSxjh55szk+OKC9nvY1o'
+ 'upBV76hay/f4+ekBjXFos2okYupfej0BnmyJQUA4d0IaJSrYkz34b4k7QZ05/AtUtp2C9s1eNK'
+ 's3IeLnnjfIJZ0zGvzJOpWtO2rkXrYUtrCPPsvDJPbGvSX1br29D1dDvsHd58j4bZJqLFJ16vXl'
+ 'LFGKabHPb7w/X1BpAbRNou6bNgbli8x88bOmCrBiWWt7SxnYEjrGYe0kcr8XLixM/Q8/x8TyW2'
+ 'DtDSE6SwpA8hyHbJV+tljmCRE7DB5zi3GJ2W9vP2zeLnPT9vwLTddmyFzQ1GlzudUd48/wacNM'
+ 'Aas4DA8RvzWo3CVTZ66pubNJOxmVeBjwsYZ2FNhNWn2nZwW2Ue2MYn/SsN3lXSQsmgWk1e6mTn'
+ 'xkFpMCHPzbulP/H8AWOmrVpinfP9JNhPyNXOym3vjY7Zl+YdBMVN30+eXJBstE/JCRMfU2rD3t'
+ 'cg2HNwv6xE65Wa+I31D+N+6bDul9P/u0cWWxIHafp7WrV4F+K7vVe+bL3S3NheGaX2R3U4ZHLO'
+ 'qkM0RsieGlmvO6eup5I/v+N5781kz86dfjJT1BGOo3OGPPPRWjUqY8j3/PHTGb9bHVaXqZ/uQt'
+ 'JKf76XfxWOf6Y3mDORIKclEmREgiUPxwEsioAFhkTtaB3bT0VX3vISE105VSuPBhdIcEEEY3zy'
+ 'KI2D5GB9iz5kCISxm3CUEQlHOYoA/8iGGFY4wHCV42YqNZMgA8hKpRY2drlf8bCOs6w3+N/6Nv'
+ 'Vzs77KQTLAMMwxLhwW2ETQog0SSA7e69BKEBNQRmyADhXkaEiyvU5KKOaRlo5xKLebssNREyRf'
+ 'QwkeClfq5yMO72Sq+Didr5QjCbKpIgmBMLhf1HEKTnfoe2RMVjZxgH+BTtDHHFqYTtAYV7fLUd'
+ 'IPP+nIv6ofvgmNWq2Xt7GWQzNJyKaocygncQrZ+bTJJ6Q2gbBOfCl2IDOoGQkCBWKTQODyVq2e'
+ 'PGO6V5oxRlTTqOoNG7S0HevgVlqQBI3AFNSJTdqLA00T4s5V6t15pCfRA9+kXun4V8NBSYDlVg'
+ 'PhZgiUJSnvBJcioEznKs2eWbxvbH4yoL/n5mfvpZ17Ijh9Pz2cDMZn5+6fnzp792Jw9+z0xOT8'
+ 'QjA2M4FIWNLkTy8tzs4v+DZ6Fk8QFTv5irn5yQUOmZ06NzeNtJYkkHY4mJoZn16aIDtgOCAMyD'
+ 'Dxg+mpc2RKTwSLs8P82fb3EHJ7bnJ+/G76OXZ6iizv+/mDZ6YWZ/CxM7PzfjAWzI3NL06NL02P'
+ 'zQdzS/NzswuTAUY2MbUwPj1GVvrEKFK0ZmaDyXtJMwkW7kbmVmqgfkB6zeS8BPzaYQanJ6mXCI'
+ 'HEp3icE1PzpO5gQMlf40Q86uD0sB9wODz9RfQgTYh6dP+wIF2Y/HdL1IoeBhNj58bO0ugGn4sq'
+ 'NDHjS/OTCOQCKRaWTi8sTi0uLU4GZ2dnJ5jYknO2cCqYnl1ggi0tTFJHJsYWx/jThIPIRc/p79'
+ 'NLC1NMuKmZxcn5+SU+dxmiWb6PKEO9HKN3J5jCszMYLXhlcnb+fqAFHXgGhoP77p4k+DyIytQa'
+ 'Axlg5o0vus3og0REGlIyzmBm8uz01FnSJifxeBZo7ptamByiCZtaQIMp/jDxwP0IWMWHMVGchc'
+ 'V/O6w7zPMZTJ0JxibunULPpTVxwMKUsAuTbfxuofmof/yLGUluPBk8TIKgXvuhRLAHgy9nUHBv'
+ '2FgNh2idnw5jHTBeJyFUQZhk2wako5yDlV1qvhDWHqIVfXYj2gx3wuZwcE+0thZMRGFNx3OxpO'
+ 'HYZWSNmFhmLZxMSL7eL1e0FFyN1io1EXA2WVJv0tyacNECIE2xsuqCTX4laT+1uAqtgyQYmS3N'
+ '6i7ETBjsEaDkWynCyWgs2RC4gi0UwnIwGl0ftW0aWkWCSEPke73RjIckaRO5XIc4YPxm+mtJAt'
+ 'H134AOI9dLgsv134CO0F/HJBBd/42/Rumv2xl6o/wN6FH66zoJRNd/A3oL/XUtQ6+VvwG9lf66'
+ '2n+dR3936x/FZtAaFKY3oBUdUwo3oM4CJXGKhIRIh1s2rIoysgK28IOwuk580dzYRC5c7XAz2K'
+ 'k3Hg5WtzkQfaVeb9KmEW5t0S8iTZUT3V5CPTipvOIDzAE2rhXZFzQlDZ44CbJsnSUkvWHzoJ1a'
+ 'wiZlyn3NCoiEJBsg5uB4k/32EpVXV/p9NvvtDpWRNKLLJPnoDpt8dJkkH92hetSLUhltd6hr1f'
+ 'WpjLY71E3qsH+MgwzvpDG9ksZ0fTAhvBtzhgiitJuRy5ejSfLZndSxq/yX2uSzl1E3rioNa/bF'
+ 'jjlMBK+ykcUZZkRNR3VpNqIonWv2srZcs5e15Zq9TBU4MzTJNXuZulIV/RGBeOouwvKi0jUB83'
+ 'pprV6nHuGf0ZWwUdIJCclHPPrsXanPevTZu1Kf9RhpgeYggWQJcrW6xr9dIBl1mgOyDwczRlOQ'
+ 'CeWFpXNirIBwOoAw1NOpDiCg83SqAxjVaerA1Q4kSxBEai8KJKsmwBbFiYBDK5J8Px1JmPRDui'
+ 'W6lI0b1OoYa2xO77LUu4lU77LUuwnqnXIgHkEGVMmBoDc3EnO9ViAd6ixhuam42do7OEIvrW8k'
+ 'Jc/QetbK2QibCJDlm5V1SdPgUF0n3t0ZRod0wIXkCOIOA6vgLA0jcCBZglyvbvTvEEhO3UNYho'
+ 'tDbHI061sj7DBKiXh3I3C6kKMu3JPqQo6W6T3UhaID8QhyFdEtgWQJckTdzMsfkE71ciQt2had'
+ 'hPflKbyd3MYsfw3xCHKtCA0NyRLkZpLbBm+XmiYso7ZFF+GdTuHtIrzThPc6B+IRpKSGHEiWIM'
+ 'PUP4M3zyG/Cd484T2XwpsnvOcI77UOBIHCgYM3T3jPpfB2q1nCcr1t0U14Z1N4uwnvLOG9woF4'
+ 'BDnoUKab8M6q64h3/9ETkK+WCM3R4t94Olxax0aL0E6yHFL7Ki2bbW3QWR3Dsc+c3Ls4XIto/2'
+ '5Em7DQmjohpEnbunzGbM0bYQOH40Fju4YEIdodtmtl/eFK0ybtJVsg2dAjDHJ7VbHlELA0WLsR'
+ 'WxjmE2esxA6H+kTBpRQFfaLgElHwSgfiEaSojjiQLEFGaIYfFEiPuh8SuDjH26MOdbXCyNlG9O'
+ 'PtLVn7kv8CU6jEzY6XWN/SP06UnJ72UE/vT/W0h5bm/SmZ2UM9vZ9k5iEHkiXIVSS0hziK/lW0'
+ '5b2WtryrUlueyTjG8c9okjn8KtrqDjLz6czhB7DVpRKAH2hJAM4RxHTIJAA/YDcvkwD8AG9eBq'
+ '+nHiQsR2wL7E4PpvB6NCUP2sWSkd3pQVosNzqQLEEGafkYvBkV2kWYkU0nTOGFYAlTeNGb0C7C'
+ 'jGw6oV2E+neZsNxsW2C7KKfwZglv2Qq5jGwXZZqGmxwI8AzRuA3eDrVqhVxG5PdqCm8H4V21Qi'
+ '4j8nvVCrmMyO9VFnIaggzLNcLysMomEHprjTaBQ35gIZjfddWhrin1wglQ3Y4rvCkecFtQj9Cm'
+ 'twWaI+g+VWiBegTdT99IQ7MEvYo2dffLntogrFfRlycf2fvL4ImNti9Da9lo+7LH+PYTz6WhWY'
+ 'KC7woONKMeIqxHUy0xEw+1fQu88hB9q9QC9Qh6Pc1iGpolKMSDmducqqZ4ERtjNTW32BirVuxo'
+ 'iEeQosOL2BirKV7sVDXIbdsCG2MthbeT27g8jo2xRjx+xIFkCeL2t0ttQXWxLbAxbqXwYmPcSv'
+ 'UXG+MW9fc6B5IlyA20Sj/iCXk8tU1oHlHZ4v/hBRxvBylpHJioXRDoqLh4NJjfA+rm3bDzCQJe'
+ '8u04xVIOBwIOKbRqVQwvl0g5B7E0IpzsLgyQ1VwNt2yVgywz0zYtlYOWYbXWf/4iS8Vo9udTTG'
+ 'S0+/MphjUa/vnUUjFa/vnUUtGa/s5FlopR7nfavoylstP2ZY/xuUvFKPo7KRGdV7tQAe3EQp/Z'
+ 'TbED9JndVO0H6DO7hOWwA8kSBHrd6z0BdavXEJobi9vJnGidgF2Pw8HORqW8scecOymWbdMLx4'
+ 'LOm2XfJmf/syoQJXyGVC+nlgX0qNekxtNN9HpNaiuDHvUa2squdSBZgpTUDf4+Frs/SnvrT3jK'
+ '4/0TrP6jtH9e48/zLzDNj3mcYz6mTWhYvGSqkyITGQ2LTWGkD8Imb0Tl+nqNbPoAeWWjnJ1vTJ'
+ 'UBg5M6zlhdUCdA2CUSkAfQteqEA8oCdJu6w//fGJRTrweeK4vngnEOb4zZpGclnzS6bernZtLL'
+ 'WrLSYltQy64vt6f9GjuNnvDvI/4YMADqFIMGHFAGoMup1Yt5+/tJj0j6DJG0eGNKX0n0Rk7Xsz'
+ 'PPE4E9kV7M08op8E+4L96Ewf2yR5vggIFRM4J2qR7/FRaEWfoZj1bPgeJ4cItOzTZ8CfGCtEy4'
+ 'rmYbKGxAj3eiSkM/IwrQVCJTGD6mmOtucekXi5k+yLj7W8AZgAtqv7/fAXvqZ9F2f6qtZ8B9Le'
+ 'AMwAO0ul/jgDPqzYyiuI5c7OCVlfVXkiAl25LU89XRIJiRs2ArW5vhw1Fw7BZaX82I5C9XS3Ni'
+ '4IPKGolJ85KjulYrD0dcDMztFIbw5va+SrfQV3e4WfXzTPZUW6yhn2+nGLJaf15TzB1uh3rb8z'
+ 'jcE8e/v+GC697WPlzo3G9rH25OPYa2l6faYsNnsGoBZwDeT8RxUXSqt7ejwN7+9nYUnYTi7e0o'
+ 'utQ70LaQaottnMH7WsAZgJFL7KLIq8fb5w2i//H2eYOP9XE9b3/rOfBu9W695P6ErM9wfWQ14h'
+ 'oiyDA3wQO05M426ttbbKFwWRQbzcL2EnaHxKoyifcnRoO76ztk/TWGtfv7hM/VTiJ7khYHMZme'
+ 'JEviJtLTVyC3quy3Y/GsSyDxh3fYXmUzU6dtI5a7KQ9lNwpR8eThWn1Hyj60SgBsIe9u55Fuos'
+ 'u7wSP7/eMO2FfvQdsrStcE01FtvbmxN2FSqGCqvqd9/n36wnsw/5f7gw64R71XE34/LY4dkO28'
+ 'LceTxgvD8r3tPe8hvO/VPXeZole9r501ewnF+9qZopdQvA9MkWbNfer97eJvH6F4fztr7iMU7w'
+ 'drptdYn/oA2h5Mte0jFAweaAFnAD5AmpCLol99sB1FP6H4YDuKfkLxQY1i2AEr9QTTonQQ8iVO'
+ 'iSXtX3eRKML9RDuRFOF+QhPJxT2gPvR94B4g3B9qxz1AuD+kcZvt0lO/gu3y193t0tPQLtIrj1'
+ 'oQtssPM4WKxQtul0kvjLL74bTU8WQX/DC2/mQCtLr7kfQEGK31I+0osAt+pB1FRv1aOwpg/rV2'
+ 'FNIaKPoZiAH+hsdujwEDoI3mNxKVq0PU+t/wWFtMQB5A8HwkoCxA0KsNck99zONiJKYNlPePpZ'
+ 'FDc/+Yx17bBMQvDtCgElAWIBSgM8gz6uPpnmOL/HgaObaRj6eRo1cfB/IrHFAWIPT8PZ7Asuq3'
+ 'tb74M14wtRbYhMqAq4c2JdwBrjijuhOUNllqu1Ln6ISKhD6YN33eXJN37dGWLuRoU/qGAzchEF'
+ 'ZfkjA4mgwNbpnfTo8Wfpnfxmj7HZAHkCLuT0A8toOkhX4yI7AO9TtAFRQ/lGGHvHGYYQBc3Aaj'
+ 'iKXjlTgVWYG/2bHGgzdP/IDzymIdqxEGh0cPD0P7h/N1u1rdHUE2DdeDofdmcai5U0HBtPGbbx'
+ '6BAhLE5ToO6PygsV0VxcREY5DKvmo/GwxWRunba5VGrL21Oj1d99jo0Oi3n4yK5yFsIIYMYyJT'
+ 'yrZjw4W2T5QLDLAh13Xxl3odITcmLWLImYgOQz0XlAPIXTCQOr+DBXOVA8oC9CIyuX7MsF1O/S'
+ '5QXVvc4nlIrJCL0574CJ5lyeQFSadAal3Uo3o+Wk1bkmGtFnH5Fcucznjgu/nd9Hhyul/ueKDM'
+ '/S7GU3RAWYCuIcvs44axOtXvA9UNxfdqxiJeQtKO4SfrgU+52Zso+IioI9SAado6ZWjDaqqcGq'
+ '3U69UoBGlKyNwpYamUOBq4JC10CGfrd0xBJf5Mmcv9Ep8OcsnUuBxuaWrhsH0n3B0yH4MS3YJo'
+ '3LbX3dJBa9wyeNmdwbHjL2FWk0Zwjs9OzA7q6IahkzqIYYTsDq3D35XQG26u309PQSdNwe+npw'
+ 'DK8O97bLInoCxAJXW9/wbDUl3q8x4fY57H+mT5A/dBLEcLq9Ejup4Xp3AbPnHPs2mqDsdBUkrB'
+ '10d3gXuUUtFudhgeeqrCpn7DYSw42T6fHlUXjerzafkM/fzzkM9XO6AsQDgZ/Z4ZVV79qR7V17'
+ 'zgnoXZGWdJmE6Nsh+Cp0akNjwrbUf7oyK2fMgXsnND3TQMSjaNvSSeAciEBL8UOtOPiEL8JV8X'
+ 'ghJXzqgr2ipNxEJGiKTT0WKpikfcf+pfGbW/Gk0WfRVdooeeVMfD2F2k8FD9aZqWeaLln6Y5BI'
+ 'bKn6aFDpxUfwqhE9iNtFt9gdeobQN30RfSyHHu9gUgP+iAPIAOOewHj9EXNPv9ZrfAfPUPHvvA'
+ '3t/NhKaVmsiyUAyZoGSO4EqjwX0QyPaJ5a+krFilCfMkLD+c1IkKYC40VrliJR/T4yU57tEIW6'
+ 'oHsCRN+iLFkmRLXomqdXB+3S4LBHNw0UScEAf16qrpXlmcScwKtjeMnNOWpMiky5OQy7LY4kCb'
+ 'U1wJq4RjtY2oWSmX9HNTa6qtfwjuIbnOEaW85Aa5pq10yQ5Rv7SOYrp4Ax+yn9BfGBoNFgxEOh'
+ 'VLRWt7YG+OI6X2I7okBaZNBi3LyrG5qb2QWS0HHiVYdqgfxVXkSmRgVmWkHHrlKh7avhxumzXj'
+ 'PDRlq+jDcUT7GUakg4uHMVGYg1q9NkKbSMRGdBovfZ9Eu8yRnTVrTWPdscHsp+pqRcmnEKFeqY'
+ 'LTdmi4JtaU5cdOA9GhCT9jVyJimUJdEgBGTUkNYuZoIHxshHYqDkhywof0kSqThYRCTJPCcRM7'
+ 'NO0Q4G1jap1cGmVcHw4iuIpxAr2+IQoY5s6UxvNZw2mhw6Isz4jjVrmCnQ7noNHbsKtm4uSlQR'
+ '3GmoxoWlHPL70p6F5ylUVWJlEfFj1h1pCZSMqQtQwCjMG+jeMtfG3LnENY6zVSDdeH3e7tEnbE'
+ 'w+3aafQTJKyXPpgudPGgI2Fxcv4PaSHok4T9h7R+DYfEP0C/vtYBZQGC3/wG1gS/CS/v/4CX90'
+ 'DKy2vGB6cu1KlvaqduP/+EZfatxL7JiWX2raRHObHMvpXI/JxYZt9KLLOcWGbfSiyzHNtAzyYy'
+ 'PyeW2bNp5DiWfjaR+TmxzJ5NZH5OLLNntczHEXyn+kcM980ZGu6V7nBriSEhY4b+8o8eh5v180'
+ '+M+TvJmDtlzN9JutUpY/5OMuZOGfN3kjF3ypi/k4y5k8f8XY9PKU0bjPm7aeQY83c9rpeagPjF'
+ 'G9SQA8oChJNKgzyj/tnj2BXTBkbVP6eRQx3+Z4+jVxKQB9BBOc3oFGuUQIhf+RififMZ93/IEK'
+ 'o3ZFS2+O7MHgeLRq/WblznCFD8unsdKyKHuNJyhog52vMAseX8kKufmpQHLSwQik+Ct6lVm4tW'
+ 'tNKfpHexk3P1QjnrC+Lt8oZ5xLLSqQ3KMTZiFHCPJcqmUmueOO6TONgkjXXUkFuHARDZfGKn6y'
+ 'wIPPb6zB6nm5e7TWjuuFFvCzgH8D7xqiRgD2AccKbBWYBxwul+3lM/kZEjzgt9Hnz5E+2fh6fk'
+ 'J9o/72mUOOVMg7MAYwF82BPWyqqfBhvdsvfh9AV5KP2glZd8aK28ttPVbi0/BTAaQh3Qd0He8h'
+ '3mMisBLg7usAvqBKhHrINOcXEQ6Bp1swPikY6qo35dQB3q5zJsiD6Q9CDp9AXPYxuRVuP3PHL1'
+ '9zpzNX3oMJ90QTmAXMkFtwCBjA3XKW4BAkGgnqAfXeqtGRKoj2faAnl1z80poZavIlphRNFred'
+ 'Hyu5jt35axorVLmPxtSQe7hMHflnSwS5j7bRkrWruEsd+WsaK1i5n6sYx19HUJCz+WRg72fSxj'
+ 'bb0uYd3HMtbR1yVsSyDj6Oti0fqODEdHmzaYzXekkUO0vgPIr3JAHkBXi0zuEtFKIERI35RHHY'
+ 'N3grS/BNJekSKtVB0RasKMeieo+SLuU56p+a6Emnmh5ruSPuWFmu9KqJkXar4roWZeqPmuhJp5'
+ '7ve7MxyPYNqAmu9OI8dG9W4gDxwQv3gdjS4BZQFCTIJBnlG/mOHYF9MG1PzFNHJQ8xczHP2SgD'
+ 'yAEP6SgLIAIf4F5zrd6v2g5m+AmqWW8DukWEup+RRlYUO+P8NhBP38E5T9QELZbqHsB5L+dQtl'
+ 'P5BQtlso+4GEst1C2Q9oyv6CJzBP/XKG3QZv9Ej8cjQ/0j90YD970lgSsHMjirWrcE8f2g6qhZ'
+ 'Mht2cxPutWk1q6OotuDWZCvT0c0fQXs/zL6YFizfxyeqCeHkNBFlu3zDKBrpXV3s2z/CQwlWwb'
+ 'zPKTaeTwNj6ZRg4CPQnk1zigLEABTbxBnlW/Ckw32jYQ1L+aRg5B/asZjphJQB5AVwrLdoug/l'
+ 'XcQXSDf7eAOtSvAdPtxduDKZMnzlXCtaUd6BJUMCx1UScDN0k7SRc6DC4XlAPIaPDdIoEJpJxe'
+ 'QQL/mu5VAsoDdKO6zQF1AXSLerHte059dO++S4Hstr4LvL3vcMJ+NN33nEbv9h1Ww0fTfYcT9q'
+ 'Ppvueo7x9N9z1Hff+o7vtHEQTlqz/D2v3jrPKOzwR3/uv/8wOpqOAf/+N9waRz444bVq9zWrGd'
+ '8t1YJVNKLQibOsPbXYd+8BAbjMn9Lc52rW0+TpylJUbbsvFNrAZxFWmqKG9fQWIYTQt2bvgawl'
+ '3j5wxoC23qu8I0DmigZOtXtrarbP1br6F7z4UJJAKivQOJwrgtkCgKjghpHFwySqet0UF2I9ZD'
+ 'xE2kE4nYTVCBdInrLWY4a1Icc5W4y41GtoljgVrcJItYezg4MgsPdFVwW+rd7WRyaoI7AvgWl/'
+ 'YyF6OuK5ilHc+nFZZy/FVu1GN9s187CYL7In0S49yDwz68erBV17OgnbQOjXb45CYiGVtBWXTH'
+ 'VzUMUmnu2KpTf/mjPIex7tpKFNV8TTcdYUGURBsHO0Q/k11faWICL010DfNKerYlabi8UY/1rR'
+ 'Y65Rm3yx1hN45pqHvGjmITusdWmY34whTjsgV93452PTeQ3lxvrBNXPiqZ6LgQkU+Jtkg/56CP'
+ 'qvnGMBNSgg2lyy++hf4DFtThhzvoDvxnDovktAt3EnHGc8zXG2AhALTqXjnBl9zpmWXU3Asr1K'
+ 'QHoxcZ9YqkMMUbQn4evs5q4qwGiw1fwiGFThtqss3XrEBXj0Y24YMzhQ5G1qv1lbA6YmdwpBGt'
+ 'Izt810kk5cHXjc7uhNPaoNwFBNDsmlRzDJ1TvnEFq1b+Od0SeGb5XlayCkbGg63q9nqlNsRDSb'
+ '2yE63ElSYOKdeSK8yGJLGjwRcs1oGsJrco0VRWWR7Vd5jsFX0ZJT0RytMiXpJLJfCc2YdfrteY'
+ 'Vq1DGuU0FR3qCCssapsnKXcARDiKtgi4W3p6cWyy3WyKe1LERby9MpIKieQjMb0izPKOdV4mST'
+ '7NdpzoHwfObTF89eL3W9YBSDT0+roRU3L6DM873wnJyTu8FWj5AZmA6bB3fxpGhP9we0s4I9ym'
+ '7tPq0heihDGnEMvhj+ERrbrC8/dnGb4VlNOMfNZdv4g9+/ri017Ad9hhxPfQvAdSPg8iqwGVhT'
+ 'CT7E02KTkOot6L71d6RzKqGpZ5Izy9a04uh520G4MY6zfGcfbKduKSqa81IeYqNceLYh2kqfet'
+ 'N7WGEDCMnY/vNW+sUJudsLEaGyeLKMlaN/FFR/9ioq74oqN/MdEufdHRvwjt8kUOKAsQnF3/OS'
+ 'MwT/01UN1c/PWMPbE0N3gaytnjMze5misuILFXTnBFjPoB1xTV7yaXqCUGvTnKZHEb8cWJHMPB'
+ 'yvwxn5b8q7crtJXqBCt9Vk1itTQyQgtmmetj8g0TwihaDIjTepBnnDY/09Pk6EFngMfaJhCJz3'
+ '3Uk2guh9sJ+bM+y4ZKuQJVhraYurjg9agwjCFnQmBL/HV6QmBL/HV6QjxN64JYg77YEn+d4cSd'
+ 'D2YFllFfA6pTxbdmZULs0VlUS3icNSdhaa6eQnOWHBHRa1sh/zkKiunc6OQ+xiTvUmTKsJwV8O'
+ 'YsS1YrWVZHcbOseQ63OUwCmpSJ/oCuwVfPrNjAkApJt1VNO4cvdHAHx97vVYaW0NtQy5Y39VdM'
+ 'gKXvLGqc3Yp+qU96uRrOetRM7MXBIWMdhwi2JhQ4Cqzt+g5BHJ2khQCSjmrmDsbT19KTDpvka4'
+ 'nR4IuN9zUYDdc7oCxAN4mPRYPyAA2pkw6oC6Bb1R0cpefza8/ge5PFq/QKEknp3hDk9A9m4jPp'
+ '/iEw/5l0/2AmPoP+jTgg/tAt6rgDygN0Qk1wNJyAdLtb1bj/VSNNOtS38cmXFf88cxHmPX5x7r'
+ 'VGgc+6044+z+A7GjFeKKJ8cAiHdbMZbW6xQrUZanNEtpGQjySXFs+MvMTnKBHqzKu3+bxYiwB9'
+ 'UZ5cMBZIrVKnvIHu1GrdKtbUynJl6OQsiN6neRLFSLeacfLx9LdjczIpc4f4hxopIvomNBlcop'
+ '7yt6tQaSSN1ZnfDkNsF5QDyJ1fGNzfxvze5ICyAA2JC9cXg5tAw+pOB9QF0O3qpf4cg5D48F18'
+ '7//FuchLA1vKy24Tctq6V/UIY+rFBLdj0GkThLOLtvUpC8LO/k9wyPeVbrVfSQopMB5gFuNsGA'
+ 'KrElaNnq69+xYVfYGRdbeAMwD3qn18J7IBe+p7aFsoduoiB6WjHMeflOWa3eIdxMZ9iwCUu9NH'
+ '09/2DL59LeAMwAh+d7+dUf+S4fjb291Br8JYAaMKg05XmpEt8tTCGe5HMBbGp1rA/BnE7vfLRO'
+ 'fU67LYb+zMwxvCIN8BdQJkDid98YYQ6JC4PnzxhhDosCPY4A0hkCvY4A0hEATb00ZsdKo34IOl'
+ '4v+dSVS5s/UWRY6WLNeA+n4UORJCdV07bLgNqaikbkmpdO0s1pZHOLAJtU7cOECrCfJJfYBqdM'
+ 'PwPZmqWaP6VScgyX2Lr9hk/kltrrzFCBo+eNsbS2tfUi+b/HcrnhyRgUi3N6RnFpFub8im9BSc'
+ 'FBPI+Dw1KAvQteo6/887BNalHmO+Kf5eR7Cg8x6kjLjRIOK0awh5XlA+zL2vdwVBScqKl+wrOm'
+ '6XIw1MFS+I69ou3w5YKcO2CObnxoN4l/SLTe2y2uWXki9xoRDE64R8pa+71cRt3SCl0VT8WdVV'
+ '1Miu1yYmivkEZ1sHtROJv4etuIcRHltfs1qTfAnCPckh4QPgsEHNJSeC3VOtxRH9PT6mqbGjY1'
+ 'J0UI+TrUJjWQvP6ztCtZiQjvval5DeUl2Karv5wiSlfbDeQHiPFnA2ipPYDDGfCCPmmg6rkXga'
+ 'tNMAkYvtbKDTYlI6tY4jIeuWA2K23Y0NkYuPpbkUkYuPZVMbGw7dCOQqVohcJJCrWHWR/HksLX'
+ '+6NOdC/hgRmFdvx/eSzQ8Bf29PdwEBf29PdwEnVW9HF250QFmABkmhT0CM/mbaSBNQF0C3kdA1'
+ 'XehW70hLYYQFviPdBaSRviPdBRzpvCNNBYQFviNNhW7qwjvSVOhGvlaaCr56HN9LuomgnMfTXU'
+ 'BQzuPpLsA0fxxduMEBZQE6LEEcGpQH6IgzQlybTKAXU6++bWz6HvUefPB48cteMBWn6oEZpr/L'
+ 'D/RdfWD3uhafZD2Tog+h30RWlYQZQh+JSPijfZLfZR3acjUqrchdfbumo+ezt6/StHuE0T4Qm6'
+ 'VvzD1lmuN9P6hGYdx0Qy05u8soJfwlMwStdlZTJj3qcbwnTWoU5HhPmtRInHoPSF10QFmArpEj'
+ 'bQ3KAxSoYw6oC6Cb1S3+vzek7lXvy/LxyasDfQVDbMLq+OyQ72Owl3ZLfbC9SrWNpsx5X0tFar'
+ 'zeph8C2fjNNzvD7qVhvy897F4a9vvSw+Zkr6w9eNGgLEDXO0uvl4b9PvD9bQ6oC6Bj6sX+28yw'
+ '96kP4YNDxR93vEZ1410MymJm6hsgRLbx7aTaKcp2Jvwjziv+XmNt0UxGtVDVAYFGnDqU2EeU+F'
+ 'CaEvuIEh9Kb83IWfsQtubrHVAWICz3aQH1qV8BpsHiqcBeLcHEb+vmKdOT2LhaRENxetZHPfuV'
+ 'dM/6qGe/ku4ZUuF+BT0rOaAsQChi9Vaj6PWrj2Y5MOTfZxwXW7CAizjcTZrXHafUtjvfoP2Omy'
+ 'htDmqiuSF1D+vq8OhhbTjxZfFxGUczpgitjp+qm6k12Rzx0Xh3c6Vehb9NG/wSFN1M7LTYvaB2'
+ 'WEc7chft+YkEvusDH/9in7FfSejZj/PHNIn7cf6YJjFSBT+aTTn0+nH+mFXXET/8mGFwpT6hZ3'
+ '8r4e+tja1L5Ws0beMTfw9+npC5Q+QqmeC7zngUjecT6fEoGs8n0uNBeuIn0iyjaDyf0CzzGTOe'
+ 'AfXJLIe0/5rHxpgzLezzSa4mtwlDEGB7jsP22k+63TbZ9lFaZ2eN2003wJICzzRRV9YWI7SdcO'
+ 'hB/+pBuKAcQC49kFL5yayNTtKgLECIbv1DQ4+C+h2gGi1+6l9BD3MvjSWM3z6fz0mYxBfs0sa3'
+ 'xLkk2hSQ0JWmTQEJXWnaFJDQBdoMOqAsQDerEf/3DG32q89q8fKx56KNmVWE5G2TvfCDs4pERf'
+ '9AzMKfbhe5+4kmn03TZD/R5LNpmuwnmnw2LQ/2E00+q+XBjwrogPpclguF1H6gQiG+PWxKF402'
+ 'ikFp1kQRmNMnt6oId4AG87n0YA6Qwf+5rK0qokEeQKaqiAZlAUJVkZ/UE5xTf5jlNNFH/9VlRX'
+ '7wcWl1GTVIqDOmBokvNUgYNOCAMgChBok+xepWf4QR9AmWbsLyRyDEPnmlm7G0gTIGhPCRHvV/'
+ 'ZdVl6uc7lMdYoRUSJK+u8L+Y49/woD2dZT/s53LYBdjEcs41k5yaY8axhFZuWYK1VEKkvfncqQ'
+ 'uOFvawagQaMpFwpcLl9azzsgW7L+jJfsRJqdRLSM66K1pLTGrv6uonJ3FqfjgOkHHkw1tKViSn'
+ 'jcJ/uhbt4FA8CpvbjUiujMdMY+9nvZ2TEVZb6g3bXBnj5Y8eCbkycCqSILDNz9TrwY/omuey9i'
+ '9wmVVwJ1P7lG7rsOCtmIDN8BF+8tp0UHfkBH7AQtFxEyCD6Z5OwzjlEDSWsFhu6k6Vz5mRaeZn'
+ 'e59D7sy44S5g099o9ad0vVdWbkxwy4oEfMf6gChmu6g1z4c/edpmgogFZE5ytWDUwUfNHQ4HaD'
+ 'YqZVuon2c/QtHFsnhK7OaSShzU4oOZmyTK04lE0aAcQMZq6BHP79OwGg47oCxAR8TzrUF5gIzn'
+ 'W4O6AILn+xlPYJ76O3zwTPEpL5ioxIm55Lh7xBtnrigLSqvOwVMpMNeUmbhnIjGXwV8j/myaxG'
+ '19lGAwmWgec2TK8ksfUBIjkXJsS7eKgiZLmtZMJWqcCmrRjnh+9DoLz9crhpPkBM7pZMkhMU40'
+ '/y5NYpxo/l2axJ6mi1JHHVAWoOMixzUoD9CtatIBdQF0l5rwv2lInFHfwAePFf97YvqbRfGCWf'
+ '/Oyvs+TX6x+P1LNvmdxWLIgPOyb6SpDH/8N9JUBvd9I7H6NSgL0DWygWpQHqBrycRPQF0AHaHp'
+ 'eV+nwLLqDR0KtUXf3AltxmbTGUrrlZsOzHDUxnCLE1l2tSgRCsLRumVSLk1qoYVoMRoA8tKXR7'
+ 'u4Im044Pt78OfLAF/WbH9ncOyUn2gpq246ZLVefzjmYkkGnXT4XLjFUcF8J5+R0K6UNvf3peVy'
+ '0iKsBtKt4OFoVzrR1sR2WCy9O4Pj0uy1+h8rFNMdahmdH0y1lAziuEgdYABB6DhO9LyY7t/JW7'
+ 'jl1RXclAJ5G9JCZjcE5qaSWhDWw01IN7Dj4xmNUZfKkMQTTXDn2HNsboqVJ04OaitwxIecJn6K'
+ 'C70jLruyFtjsar0W9s4F5Vym2cXJk6bItLiBrTrdUtafNjKO3zBqC3OVrofrG2NbJ54LAhF0Js'
+ '6vsplyOOtzATFczMYigYnuBoOjc14mLigHkLsucXROICVx+xrE6wtlzW9iULf6SeDZVzrIgQ44'
+ 'sFu2x5W0iWh1socVwZ8E+l7BpRXBNlDGgK4T9G/S6AuMvhbW6sthvIzPJJg9NHLReHuBMgY0L2'
+ 'PpUD/b8XxWGGScBqsL6gSoxxFlOLMm0LXOHoIzawKZCoPwjb6544WqMNjD2j3hN9p9j2j3DBpw'
+ 'QBmAoN1DL+9Vb+0gvfzrRi+H25IgeXXA/2CGf0Mvf7yDjfe3ZpiqfAlnwv3mhJOD+26+uTVMQh'
+ 'T4MAmu9i9QuUPCecnexRTZMLMdrA7SJoztY06EfEsqJOPvGH0jbeQiONrKgyQspk67sdTqRm+w'
+ 'LUYIuSRVp7FLyk50GK68BvuEOfVwNeLMaA6C3obObg7FrtNc0Ssq3uMJo2hQJ0Amo6lXVDwCme'
+ 'pCvaLiEehK2Sx7RcUj0FUS4tIrKh6BblLDXKaKr4lQv4Dv/VKHlKkyV0cQFGWqbrAgTOI7O1D5'
+ 'qthvnSabXKScj9ptK+QxoV0rOAPwPrIB9ztgT727w0YKWKAB51vAGYCxWF0UGfWLHbZQmgUik6'
+ 'jDxgEkYG6NOIC/MLzpqSdAgauK/zUjK55LKggTSHCHvspZG39Wxm81UEkOm5DolhzSzPINCTgw'
+ 'xKzN1sawmotgQI0G86EoJPQxgx0mDq5wMj4T1O81pZ6SiLZIs6U2NiRVNWw0aHPlAvFctpG3Kh'
+ 'v8V20tg7dSra+MBlOmeMWw3kXMmSU2kKa+64XjA/kYVCuLWq2W81dNNKdqmuE5qNRPpFkaKvUT'
+ 'yabSK1P+BDaVKxxQFiCw9LtyAsuojwPVieJP5Xiu9GW7NiJM3ExREhO7wIqUJpr1z0nCQl0qsk'
+ 'jNDHc/hb1vL19jeuC9224NVngJNyOyP6o8HWuVR0wdKD8YpEe33TocbMu/sfzLjRggfw2hjI9T'
+ 'gdUMxN6Y6+s6cMIyPIfueHQUlzGpeCJIS6zruiTQvSscoaWDycDCGwhYlTCrkDQkUpSSojVSDU'
+ 'ac4cFata5Vd53VkHwW3iOWnLt4aq/ztRaFDIIN4pSZSJidzksCjW/VG+dZScc/l0yxL45BicjU'
+ 'r9QbTnIPCx89V35gbx3mLO+U5mavAmlqN0hKuNtqAXWZbDcSQsd26thuAuP4Q8fkI3phlIyEjd'
+ 'Ep8IFVoHrFsPl4mte5OFyHdWD2imwiUEF22V4xbD7ewWncCSgP0DUSjtgrhg2BBtUxK7499Ql8'
+ '7z+64tvT0C6SqsMWBPH9Wx0c5HVQfMROeIPOEbzcbU1Ifistmk2Jwd/q4CCuUQfsqU9q3Fcy7j'
+ 'ZOjVuwe+aNfS3gDMCt2DPq0xfAnkTLu2jQn0+3YxdEwP6tXiFpVj3VwWHgX+o10TxOXtCKNcmq'
+ '4aOV6u5dQTAdPrprY7jNma+oVCOgo6mkrjNe4LeQuj07JhRUh/U6lienqrGior82rOVShQuoSb'
+ 'vDcVJFjKWv5I5L/xDLLYaB1lV13JHeALT3UVZTCqsI8nJTJ3sk+LizUkmPi95pN45xJ0rkP4Yf'
+ 'clobRttiM8HHtdaIIn0CwZaerWPDCh1ig9ZRyqsB8u7arLN0laWmLa9lFFiTDmbDUn27SaUOPm'
+ 'A4xtvr61FsSielPGwhXwQHza8S6UplIduWwJPqT6oeF9errjfEzesIjBWy1B+OIl1OEGUGNjAX'
+ 'xBHiTZALVVJRlJU2sWSCmoNQh8Zyj+UeJmTErcmxFlykztkNzfIpn88xJdCby0ixYxhXxoROUj'
+ 'HR7cx2A9MABQWshuo1I7hfxt4P4zsfq7j9cW6j0h0+xRFaTRMJaj4GbCziMXbto7MZQfqDzM7l'
+ '7YZOleSdrKrLKaURgukrNZQ845QqrieEoGgp5aHZkqjo+pVbrfd2X0N5Iyo/bMsTGfVNZ8b5vE'
+ 'HS/Kdyj2iWgBnV+WhIYIupeEqvW8T8Dg4ZjS61un3+diNCtpBmSC6UJH6D9FKET58v6uQlkOqZ'
+ 'cwxRizBg3J1qUmzqJkpyD5xs+EieZF0HiOr1XanpYlmy+fE9rJzoAcIMY2OIQqt6bm03tuo6Pg'
+ 'aE8c3KgBJTa91xxcvL5I4vSm/f+uRt5ammXIhUaboUN0cjTtyeMzdGWqa7wajlitYjOh3miHSj'
+ 'wrf9prrCTtAjHF5/xL9Ys7RsMvJMV99KORQ3oHQjAVQo7mgJcLM8ldYS4GZ5Kq0Rw83yVIctqt'
+ 'orbhYCHXQUB2QoPAUl+YgD6gIIpYS+5gmsQ/1VBzuZ/5sbXwZx9oK5mI3fP/7BHMyBrqZ6STFl'
+ 'UiXcEKDDDNcF5QBy6Qvd6a86rHu5V7wtf9Vh3csalAfIuJc1qAsguJdPCyinnsb3hovHvv+b5g'
+ 'xahJ8/ne51TiN2e43w86fTXIHw86fBFVc6oDxARTnw0aAugG4kRpkXUKf66vPq6WKcNIqvpkfR'
+ 'qT/U41AVodZfTTxdGpQFyHi6EA739y+Yp6uXPV1/n3i6esXT9feJp6tXPF1/rz1dL2JQt3pGOx'
+ '775Sa21eAhVvoELfyZzyQuxl7xZ7aBMgYEB9o+9U040P7RONAQ7fZN7UCb55/Q3Z99Xqdqn/ia'
+ 'nk2map/4mp5Npmqf2AHPJlO1T3xNzyZThXi9b79gU7WPp+rbyVTtk6n6djJV+2Sqvp04JfvU/w'
+ 'RNfzonNEWc3v/s4ETaMv8ETb+HXgfFeR0qkI6ZMZEDIY7E4YDf0peKiwNSX6fEF3Y2k1Qp35QO'
+ '5m71CZG/lxC5T85sv5es6j4h8vewqq9yQFmAULf5K57APPXjOZbiX0ikuNS+egHPCXWG5Asrw/'
+ 'mQ3SEbnEY8VheUA8glm6cpYkR4nziNCGREeJ+cwxLIiPA+OYclEET4dQzqVq/PXfRAoY+X9utz'
+ 'dh33ydJuA2UMaF4+llFvzD2fa7dPHA1vTNMH8vyNObt2+8QOfmPOrt0+cTQQyKxdRLT+VO6FWr'
+ 't9vHYJv1m7fbJ2GTTggDIAmbXbr342R2v3F8zaRQAoQfL0+Lse/8bifYteCl9pWQraVHzBF4T+'
+ 'zgt9du4UvJPJ7xeZ8pZk8vtFprwlWRz9IlPekiyOfpEpb0kWR78cErwlWRz9ckjwFr045gXkqc'
+ 'eeVxbulyX+WHoUKDf2WMLC/bLEH0tYuF+W+GMJCyNi+PEXjIX7mYUfT1i4X1j48YSF+4WFH09Y'
+ 'WKl3goU/YlgYMb/vzHGs2lNZ/g0WfiLHOR9O4EeSg/4C8q985IVmXpMGNeofp0UK6/xkoCuZ2V'
+ 'oxxwJTJObEcVMFLbn5RKvRh+PAKtLzc+OIOlhr0F6LQ3gyEu9DAZt6tb4ObuMrxOpkoInlGjv3'
+ 'VNXJMie2rZ6PYgkjCFDWh7PaTAVf7fzh/Cyuzb3C2WBothqVK+K+MWd9c+JIAqLTumaIsLeSRf'
+ 'pEwt5KFukTySJVskifSBapkkX6RM5mtihZpAQymS1KFimBkNkyLyBPPfm8LlIli/TJ9CiwSJ9M'
+ 'FqmSRfpkskiVLNInk0WKMPgPv2CLVPEi/XCySJUs0g8ni1TJIv1wskgH1K9jkf6hWaQIRP91LN'
+ 'LL/f+W5d9YpJ/Si/QpNzqLXWwvcHAWvvHCx2ZJ/vb/31bogKzQTyW8PSAr9FPJCh2QFfqpZIUO'
+ 'yAr9VLJCB2SFfipZoQOyQj+lV+j/8BiGw/b/hA9+Nqey6XA/8dmuRiO6JMIIO84HUUcAnlSa47'
+ 'sXF+ewpqthrRwNacZYjTa36vCaDXOpuZp2d92l2yJbepXzW1s9Y4k39OzkIhhnRVcsoC/5hiV0'
+ 'OPHckvM8+Zx1zpoTh5aDubnZhUVLaB1OQOPuUgf53F6DsLQ+k1Md6mo+o7FAasvggy3gDMAo2D'
+ 'rkgD31n9H2UOmADnlCip7tpZ/C4JnG+1vAGYCvoO+91AFn1O9y29Jhl8q6zqYpJMgFXvR0xelv'
+ 'oWP8fl8LmNHiGsOCMImnfg8M8V9yUqdiQGTu76X5EjL393K28OyAjIdAV0t0x4DIXAKZAiYDYv'
+ 'sQ8k7JFxwQ2+e/oBc38M4xwN363PO6cwyIhfK59ChgoXwu2TkGhFKfS3aOAbFQPpfsHEgY+oMX'
+ 'bOcY4J3jD5KdY0B2jj9Ido4B2Tn+QO8cP4OtoaC+gK3j72jrKP5zJhizbl97ZA8xFVp/QkJVe8'
+ 'BjiSiJpDqoHcf0oc7WN0OSioL68gKT5G/D+E6enJO6jMjf4XwmWya2Xq+aurKxCFs+1+NShujg'
+ 'hHPvBud1xqOpJPiWLlRqqZs69Bu6Vp2ccej+JWhPnhQUg0NaRhEmfTVNS7Px+tbuYn1waEgON7'
+ 'nQDS+zJbcUpK0XaYpN6jJpSJP6Qo6L/P9Rhn+jmv1TYJv/Dln7SR3Z4xaPSFWYTI4UuYio1Mix'
+ 'c6mLNa9L4QqcD63WmyOm1NSqiVWvxMtJcZyKvvklqKytOW+7KGtOmclgcDUipjDlb/TtYJiwFC'
+ 'cgbC1uDRZFrYZJmoHhHwl+uLRWr5eGdYzOq4bp90rYGF0JHyUYOsOgV28/YpsEr3V65Ad4fXRQ'
+ '3hkaRUtZ0QWpdE8k9eWGyIKtdP8XEHUlFnUWSKufwb0t4BzA+0QIJ2AP4APqmhZwFmAUBXY/6K'
+ 'm/BObrU20hNP+y/YNwGv2lXsppMCNBClkanAUYdeL6GYzRfQlcdI1QQY/sS4lcK4jX9EuQa/sd'
+ 'kAfQAZEqBRkNgRDiwff/FXgoXwaqw7j/bzF9zr03hw5j9nc2iOuwPjjahpXN+sMRREnDx3alSw'
+ 'Jz+dUwDla3GzpAS47sJiXfR24E1GJB4oflgsFkaCDrl9OjBUm/nLMBLQUh55dzNqO1IKQk0I20'
+ 'rxlSZtTTwDRk22CLeDqNnI9S0shBpaeB/AYHlAUIBXcM8qz6G2AatG1wyPY3aeQ4ZPubnI2k1C'
+ 'APoAFJLNcgxoXy7QZ5h/pbYEradBiQ74ByALk9xwnT3+ZsoqIGZQFyOSynvpKzRbQZQMi/kkae'
+ '063cnuMg6Cvo+TUOKAuQKaJd4A33q8B0k23D5zNp5CiF89V0z/l8Bj2/zgFlAUId9qfAvvvVN7'
+ 'EDfquTdsCHgslaOdyKpYxxpaYzwiR7cFtC3c3FezpmVirzITJAgthQ5rwatVQ5D3ZCp/ARGSoP'
+ 'Pp+Fo5PecEwaOq43FOSYflP7a14/wL+5Gn+nQlX3ZxVt+NN1KZlbSWpwh8FWJdLxGWm0SWFGHj'
+ 'UPGElQDRK9W/XaqpRndM63kwLWNgnKoWollpqtcrVSctcT/ZiamOQ7BFfl4r0IR7DpbM+k4oCU'
+ 'PqxsVuirwFWv2muypHjqMFkGuBBKcvP0EGxGygXzBEFbqTVHj0x6yWv9YDrilMZ6/WHUT+Zy20'
+ 'nodjJuxn4xVA9IrsoDD9h/8L8HHsDDUB6ulPkfokWwFgTrGxUf9qgtHG1LXlF/9HzqtJ14izTM'
+ 'gMtbBen/3P0yCH44HK4M0T/BrcPBLcPBcfr/4FXcDuJ8Z6NebR/YqLy40vLicHAr3sWL1XAlqp'
+ 'L5J6Mf0q+Uh1fbXnmxeUXfUqrJJO2j4bW29sdMe11mmOgpjdeHN9oan7CNdYXewWND5lYekGmE'
+ 'loEhm8S52NsHbIy0BE01ya5fk1tUJSaEC1AGLtPreyKlPnWlOeTk/22boDRdAZHTWmiZSfhzrE'
+ 'u3BwG8DDrcKqqVq/U4XaNVkgK1LoY4KJfJORq0WWkkBY45NLr8cDC4VY/jykrVFnJn14kJZ0p0'
+ 'OKfovFZjueiwTmiVsCBLrh2U79b8xVSzx4ilxHwpWSqyS8UGC3PNrpqm1iim4Zzpi2XixEq1CZ'
+ 'X4liGojgaOTTiwqWBr6edepsO3Wm412MzHh3V9dTt8LjgnV1kEm/WYvTb1lfOV+nZsiGsulNVj'
+ 'Wy0JXcN1hIqZKtWmsLlbk9udhvSVP7i4F9Vbpea/U/V7j1GnWfVwrJe3CWzTqVZcalq4CpE/Wg'
+ 'WXtppXdI+EXZzxRCjKu5aujekS0Nh0OgARWFYi2gqZjUTXa6WMTuWON8KGNpVaqsabQDVd7Zrf'
+ '4UHeo+OpdFxYuNeI3WHG9U2JHmttCczWUEUIa2Buy2IUMAKpt+EeiygorTfq21slMc9ZSHKZ41'
+ 'BLKIzMuQTArszU7U1Jkd2Eo4Eo2TAr+uLMphF8OhIfSKVGZKXBFjIpuTZg1l79RIQaTwq46evJ'
+ 'OElHtG1nGYleTHv3SriiA2Rp8JX1GjsauWw8+2Hpk3VTasdxlOhqPEiMHoYqzhkhOrAcoXT2K2'
+ 'VdJi7QNz2VEZiXVN/lVCFRyfeLAcKKiAvqBMgYIPvFACHQAQko3y8GCIFwgUuBQbCR3w1M3+yU'
+ '6PH9YuYRFGbeG7ssDOrPb3aSfTVc/Fane++I3H+AMt7CyxfS4kxGv9zm5lsC8NUHTjKLeK4dP7'
+ 'Tc66glnpQ+5JDFUItQqJE6fx5XaO/ZA1PnCeJK6mqKyzLRs3iNOyscWSCEnjUC3hlP8M54O2+j'
+ 'fiK4T2r5XI1GTW0ETPLgCdpQjx7l90xm7SiPavD2IatPUAOgtA2wLw8mj7nBsST80yzuPYaY+r'
+ 'iuEOGS8Fbupd2XW+mTevnO4FaEE9famun+tyM/nka+131FgUkHPi6o97zUiNWSNvTH9lQGua2k'
+ 'FycSQ1dcYLZIbqC2+249uThUvDZTa9BtJZpLM1E1ROyoZsa2ycfMW9Zo2bzTCl+yAwzaGqtW8v'
+ 'tmuWhdzwgzzl8y56eblXK9Wq8NSXrDfse5wmuxtwWcA9jc3Lffca4QeL941Pc7zhUCw6OeBucB'
+ 'vlrd7B9Mg8l+pwfXqCP+n2ecJ576tBYLn8mYrOUNvpBGexkQ7B3pK0S2G1ZdOymF/6skEIblbx'
+ 'rq9mZtGDdGrvKDRP8ddmKcwzjeRgEH3t1xi7ZFNDTMr2o89rYbnFlJchoJeKcIvp4lTqSTHL3y'
+ 'LrFDEkUOnJpTNUp7UAWUj0aN+og+YoECY6P8UU+fdxspqI8MAR+naHWtq8h51WolJkm0WzH3P2'
+ '/rRGx3JuB6+XT7LMP98un2Wfb0RLTOMtwwn26fZRwLfLp9lj2e5U/rWX6y13mSUc+gK0PFt/ba'
+ 'GzAW2MTFTjpFpmnaW2qrVjurwFTND7k23S7s301R3llBqiTHeawl8B3OWkYnK0l8BfyCCWk39z'
+ 'iZReTqEigKw4WyZc/lI4P27yLRk3biqLzN6ZxoFuuawihbyNznay9by1t637ft9TFtU1z1fOG2'
+ 'NT34o+hQlXRpAi7jfmf0e9l2iBEgiQQbarjeCLc2uNu2ATOm7oBviDWIUykoajSCms7RaNaH9C'
+ 'GBzq8w625Ub7MWNyfOGCc27tdEta/WwdQTiiUbtNZNEvPEVjmY5YyojeQVSVpyKyidsg83w8bD'
+ 'WFH6COHo0SFtx8V8T3XEBodomFovNnQYNjQEPzSl2BszDW5EIr6pxA/7yd0rBl27FGYbkqsigz'
+ 'HqSRau9kiQ7kaCZCbaYZow50oqd5IGzrf66dubzF0zqc2K6whZxwXv5+MyfJdx4Xr3gws+hlN+'
+ 'D7QE1jvqXvvjSvgoPTxx6qJoHzVfHauJKQBKtLW5CI5Xbz8iOJ4Lk2npXLS+vUJrg+Ba5RAEE7'
+ 'IwLJ/oK9Ma686t4WB4wwSImWiEFc7KMSwiqPRXA/O+e3F3Q4uilWpYe1gzvVkNku6stUpGAxNm'
+ '9Lm7lyyt4PjonnOim90ZvFjPypHgtMvYllqsDh7Rd3vwsINpGath71iaGCYXBWY0OHL0opjFbK'
+ 'E3qZ+oxSovtDCWfkgdvc3OikScBKstw49bNi74y59p37jg6X6m0579JGAP4APqhhZwFmC4+Pc7'
+ '4Kz6OjAfSbWFq//r7R+Eu//r7R+Ey//r+OCNLWDGPaiGUh/sUN8A5uOpth0G3NsCzgHc+kEcA3'
+ 'wDHxxpAWcBvkUd878Bh/oB9Y+diFjvUh5iQ5KrTbWkrWpbcaOyRbPd3EFiUTr/TzsNUB0t7Wg3'
+ 'N9eMJTdX2D3brX4Ux/VyJbRHkPaqLvsV3/XcJ7EQ5vIZ1oT5rg+wbRIoLy+l6ttoJztqH9KY8+'
+ 'pK/0f4J4zM73Zy0dOHkFk2ZiOjzN4Wa6cFezOgP0eP8GbUQgl2LprNyjceHVOyjBaR0xmxrA+I'
+ 'Xv3dxLI+IJb1d2FZX+6APICukKrMB0SXJhCuwywwCJb1PwHTT3WJZX1ALOt/gmV9wH+rZ2EY9L'
+ '9oFfpHXcOaI43T+23rEYo7DOdyRI6414cFrAwYX1jYqrONmkOMUe0mY/60vSJq/EvC5Ak4B7DR'
+ 'PxOwB7DRPxNwFmCjfybgPMBG/3TAuKdD65+LzgNP/VgX9eSG4l2tFGJ+4vr92h4zN2ftSamWEU'
+ 'LDZry9LeAcwGYZJ2DuxQF1bQs4CzAu0n6NA86o1wPzNcX11h6zwaJVjzU44mhucVWOdW+meVku'
+ 'NOD1n+SaOIe8OgqjZWSQW69vHxlE8Ou72uYO9CWwuVo+AWcBxoH1l1x2zao3AfVVxd/32vhVQi'
+ 'AvZWSBzre+yMgYiy5zFdXMb+eoGtJpK4ybjtGOyL/zsLz49qFBublUlzwxBjcLjTsZ5YjWBIda'
+ 'yIcN5U3t5MOG8qZ28mFDeRPId0ULmAmF8iUf6veHwq3K0fMnjtI/y8yLR7nkRAx/7rKJveEHhZ'
+ '7NOm20pM6Mnj9RWvP3zdmGC1GzUPTzCOkBVx/yAm+we97+Ltzh9yRY40OZIDvYd/zgqINvNEE2'
+ '77Y98nOe7yfP6CtXzE3On5taWJianVlemlmYmxyfOjM1OaEuKyi/9+7ZxemphcXlyYmpReUVrv'
+ 'ALBjI2cW5qhv6YnFeZQp/vE4alSd0uWyj4fYRjemJ5YvKMhnUUDvkHEti9Y9Omde504ZWqlWj3'
+ 'fKHXz9OGcZk6ojzc5dfLPwrHn/ICRBA1cAlvcPyW47cw14xvkAZX2d4MxjjVn+uqVQNuFNtAVt'
+ 'JIl+KkuEDq4JY2DXiNJawpDE4vTIzEzV0cTlQrZTIGxVUgRYLW6ts1axhMT41PzixM6uNS+C+J'
+ 'Ty9+AyUqSo3o7x8V9PHRlXjVP36/ke5VU7c1tleS6sqxpgwsawF6TaEoRIymuJxAc5iuYTIcbN'
+ 'Sb1UrMmhskrk80PODjwo5e+uswgPke+XuJI+QU/R2QYnKWKSjnYVHgcJCJez4nzBbgRlI2nmi1'
+ '2UtJkTZ3MrjN903CpVI51e/fpkPs6EMFBNsUb7IFpnQ+nR6+rVezXdvWGdXKvEeY8OYhB4IbBy'
+ 'G75gTikejOqP3FH9IxjrbnTmGGcHVV6Addmr4Ub5srDUJDM+ernuDscyAZgiDA80GBZNRBjl2c'
+ 'u/BXVyP2lNgvsLGiK3YLxNa3iI9uRqbgifkmRoZvHHAg+CqyxO8QSJbEUEYNFIcu3ItotaJPFT'
+ 'FoBz2kG17udSC4Xr1fKf8egaBwTUZdXjz5XOhNtGb7kbjFDm0Z2JQDyRAEJbs+4wkIEaOo2PXr'
+ '3nN8sekGVaU/j8vv4D5xeTgxpiTt3xw5mhuEmDq+c34Axx0OFWqx882wadLD2K+RyEBnnIjfwS'
+ 'CucCAZgmC/eFUeoeI3ajFXnOUxypmddhUejlPd5nR742gyu8EF1t4JrD0dk30jpzpM5E049mF0'
+ 'p3hrW6ikwRg86Hz0QeciUx6WCdTOMZ68A/EI0q0GHAjuyDlA3/6hvInjHkKUUvEWFi/NjfSs6D'
+ 'HrUwEa6YOmPw8634UuN8SqewLpJAg09wSC71yhrnMguB3oBnXjSqcufvb/AXZbZdc=')))
+_INDEX = {
+ f.name: {
+ 'descriptor': f,
+ 'services': {s.name: s for s in f.service},
+ }
+ for f in FILE_DESCRIPTOR_SET.file
+}
+
+
+PermissionsServiceDescription = {
+ 'file_descriptor_set': FILE_DESCRIPTOR_SET,
+ 'file_descriptor': _INDEX[u'api/v3/api_proto/permissions.proto']['descriptor'],
+ 'service_descriptor': _INDEX[u'api/v3/api_proto/permissions.proto']['services'][u'Permissions'],
+}
diff --git a/api/v3/api_proto/project_objects.proto b/api/v3/api_proto/project_objects.proto
new file mode 100644
index 0000000..0c2a4c1
--- /dev/null
+++ b/api/v3/api_proto/project_objects.proto
@@ -0,0 +1,543 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+// This file defines protobufs for projects and their resources.
+
+syntax = "proto3";
+
+package monorail.v3;
+
+option go_package = "api/v3/api_proto";
+
+import "google/protobuf/timestamp.proto";
+import "google/api/field_behavior.proto";
+import "google/api/resource.proto";
+import "api/v3/api_proto/issue_objects.proto";
+import "api/v3/api_proto/permission_objects.proto";
+
+// The top level organization of issues in Monorail.
+//
+// See monorail/doc/userguide/concepts.md#Projects-and-roles.
+// and monorail/doc/userguide/project-owners.md#why-does-monorail-have-projects
+// Next available tag: 5
+message Project {
+ option (google.api.resource) = {
+ type: "api.crbug.com/Project"
+ pattern: "projects/{project}"
+ };
+
+ // Resource name of the project.
+ string name = 1;
+ // Display name of the project.
+ string display_name = 2 [(google.api.field_behavior) = IMMUTABLE];
+ // Summary of the project, ie describing what its use and purpose.
+ string summary = 3;
+ // URL pointing to this project's logo image.
+ string thumbnail_url = 4;
+}
+
+// Potential steps along the development process that an issue can be in.
+//
+// See monorail/doc/userguide/project-owners.md#How-to-configure-statuses
+// (-- aip.dev/not-precedent: "Status" should be reserved for HTTP/gRPC codes
+// per aip.dev/216. Monorail's Status preceded the AIP standards, and is
+// used extensively throughout the system.)
+// Next available tag: 7
+message StatusDef {
+ option (google.api.resource) = {
+ type: "api.crbug.com/StatusDef"
+ pattern: "projects/{project}/statusDefs/{status_def}"
+ };
+
+ // Type of this status.
+ // Next available tag: 4
+ enum StatusDefType {
+ // Default enum value. This value is unused.
+ STATUS_DEF_TYPE_UNSPECIFIED = 0;
+ // This status means issue is open.
+ OPEN = 1;
+ // This status means issue is closed.
+ CLOSED = 2;
+ // This status means issue is merged into another.
+ MERGED = 3;
+ }
+
+ // State of this status.
+ // Next available tag: 3
+ enum StatusDefState {
+ // Default value. This value is unused.
+ STATUS_DEF_STATE_UNSPECIFIED = 0;
+ // This status is deprecated
+ DEPRECATED = 1;
+ // This status is not deprecated
+ ACTIVE = 2;
+ }
+
+ // Resource name of the status.
+ string name = 1;
+ // String value of the status.
+ string value = 2;
+ // Type of this status.
+ StatusDefType type = 3;
+ // Sorting rank of this status. If we sort issues by status
+ // this rank determines the sort order rather than status value.
+ uint32 rank = 4;
+ // Brief explanation of this status.
+ string docstring = 5;
+ // State of this status.
+ StatusDefState state = 6;
+}
+
+// Well-known labels that can be applied to issues within the project.
+//
+// See monorail/doc/userguide/concepts.md#issue-fields-and-labels.
+// Next available tag: 5
+// Labels defined in this project.
+message LabelDef {
+
+ option (google.api.resource) = {
+ type: "api.crbug.com/LabelDef"
+ pattern: "projects/{project}/labelDefs/{label_def}"
+ };
+
+ // State of this label.
+ // Next available tag: 3
+ enum LabelDefState {
+ // Default enum value. This value is unused.
+ LABEL_DEF_STATE_UNSPECIFIED = 0;
+ // This label is deprecated
+ DEPRECATED = 1;
+ // This label is not deprecated
+ ACTIVE = 2;
+ }
+
+ // Resource name of the label.
+ string name = 1;
+ // String value of the label.
+ string value = 2;
+ // Brief explanation of this label.
+ string docstring = 3;
+ // State of this label.
+ LabelDefState state = 4;
+}
+
+// Custom fields defined for the project.
+//
+// See monorail/doc/userguide/concepts.md#issue-fields-and-labels.
+// Check bugs.chromium.org/p/{project}/adminLabels to see the FieldDef IDs.
+// If your code needs to call multiple monorail instances
+// (e.g. monorail-{prod|staging|dev}) FieldDef IDs for FieldDefs
+// with the same display_name will differ between each monorail
+// instance. To see what FieldDef ID to use when calling staging
+// you must check bugs-staging.chromium.org/p/{project}/adminLabels.
+// Next available tag: 15
+message FieldDef {
+ option (google.api.resource) = {
+ type: "api.crbug.com/FieldDef"
+ pattern: "projects/{project}/fieldDefs/{field_def_id}"
+ };
+
+ // Resource name of the field.
+ string name = 1;
+ // Display name of the field.
+ string display_name = 2 [(google.api.field_behavior) = IMMUTABLE];
+ // Brief explanation of this field.
+ string docstring = 3;
+ // Type of this field.
+ // Next available tag: 7
+ enum Type {
+ // Default enum value. This value is unused.
+ TYPE_UNSPECIFIED = 0;
+ // This field can be filled only with enumerated option(s).
+ ENUM = 1;
+ // This field can be filled with integer(s).
+ INT = 2;
+ // This field can be filled with string(s).
+ STR = 3;
+ // This field can be filled with user(s).
+ USER = 4;
+ // This field can be filled with date(s).
+ DATE = 5;
+ // This field can be filled with URL(s).
+ URL = 6;
+ }
+ Type type = 4 [(google.api.field_behavior) = IMMUTABLE];
+
+ // Type of issue this field applies: ie Bug or Enhancement.
+ // Note: type is indicated by any "Type-foo" label or "Type" custom field.
+ string applicable_issue_type = 5;
+ // Administrators of this field.
+ repeated string admins = 6 [
+ (google.api.resource_reference) = { type: "api.crbug.com/User" }
+ ];
+
+ // Traits of this field, ie is required or can support multiple values.
+ // Next available tag: 6
+ enum Traits {
+ // Default enum value. This value is unused.
+ TRAITS_UNSPECIFIED = 0;
+ // This field must be filled out in issues where it's applicable.
+ REQUIRED = 1;
+ // This field defaults to hidden.
+ DEFAULT_HIDDEN = 2;
+ // This field can have multiple values.
+ MULTIVALUED = 3;
+ // This is a phase field, meaning it is repeated for each phase of an
+ // approval process. It cannot be the child of a particular approval.
+ PHASE = 4;
+ // Values of this field can only be edited in issues/templates by editors.
+ // Project owners and field admins are not subject of this restriction.
+ RESTRICTED = 5;
+ }
+ repeated Traits traits = 7;
+
+ // ApprovalDef that this field belongs to, if applicable.
+ // A field may not both have `approval_parent` set and have the PHASE trait.
+ string approval_parent = 8 [
+ (google.api.resource_reference) = { type: "api.crbug.com/ApprovalDef" },
+ (google.api.field_behavior) = IMMUTABLE
+ ];
+
+ // Settings specific to enum type fields.
+ // Next available tag: 2
+ message EnumTypeSettings {
+ // One available choice for an enum field.
+ // Next available tag: 3
+ message Choice {
+ // Value of this choice.
+ string value = 1;
+ // Brief explanation of this choice.
+ string docstring = 2;
+ }
+ repeated Choice choices = 1;
+ }
+ EnumTypeSettings enum_settings = 9;
+
+ // Settings specific to int type fields.
+ // Next available tag: 3
+ message IntTypeSettings {
+ // Minimum value that this field can have.
+ int32 min_value = 1;
+ // Maximum value that this field can have.
+ int32 max_value = 2;
+ }
+ IntTypeSettings int_settings = 10;
+
+ // Settings specific to str type fields.
+ // Next available tag: 2
+ message StrTypeSettings {
+ // Regex that this field value(s) must match.
+ string regex = 1;
+ }
+ StrTypeSettings str_settings = 11;
+
+ // Settings specific to user type fields.
+ // Next available tag: 5
+ message UserTypeSettings {
+ // Event that triggers a notification.
+ // Next available tag: 3
+ enum NotifyTriggers {
+ // Default notify trigger value. This value is unused.
+ NOTIFY_TRIGGERS_UNSPECIFIED = 0;
+ // There are no notifications.
+ NEVER = 1;
+ // Notify whenever any comment is made.
+ ANY_COMMENT = 2;
+ }
+ NotifyTriggers notify_triggers = 1;
+ // Field value(s) can only be set to users that fulfill the role
+ // requirements.
+ // Next available tag: 3
+ enum RoleRequirements {
+ // Default role requirement value. This value is unused.
+ ROLE_REQUIREMENTS_UNSPECIFIED = 0;
+ // There is no requirement.
+ NO_ROLE_REQUIREMENT = 1;
+ // Field value(s) can only be set to users who are members.
+ PROJECT_MEMBER = 2;
+ }
+ RoleRequirements role_requirements = 2;
+ // User(s) named in this field are granted this permission in the issue.
+ string grants_perm = 3;
+ // Field value(s) can only be set to users with this permission.
+ string needs_perm = 4;
+ }
+ UserTypeSettings user_settings = 12;
+
+ // Settings specific to date type fields.
+ // Next available tag: 2
+ message DateTypeSettings {
+ // Action to do when a date field value arrives.
+ // Next available tag: 4
+ enum DateAction {
+ // Default date action value. This value is unused.
+ DATE_ACTION_UNSPECIFIED = 0;
+ // No action will be taken when a date arrives.
+ NO_ACTION = 1;
+ // Notify owner only when a date arrives.
+ NOTIFY_OWNER = 2;
+ // Notify all participants when a date arrives.
+ NOTIFY_PARTICIPANTS = 3;
+ }
+ DateAction date_action = 1;
+ }
+ DateTypeSettings date_settings = 13;
+
+ // Editors of this field, only for RESTRICTED fields.
+ repeated string editors = 14 [
+ (google.api.resource_reference) = { type: "api.crbug.com/User" }
+ ];
+}
+
+// A high level definition of the part of the software affected by an issue.
+//
+// See monorail/doc/userguide/project-owners.md#how-to-configure-components.
+// Check crbug.com/p/{project}/adminComponents to see the ComponenttDef IDs.
+// Next available tag: 12
+message ComponentDef {
+ option (google.api.resource) = {
+ type: "api.crbug.com/ComponentDef"
+ pattern: "projects/{project}/componentDefs/{component_def_id}"
+ };
+
+ // The current state of the component definition.
+ // Next available tag: 3
+ enum ComponentDefState {
+ // Default enum value. This value is unused.
+ COMPONENT_DEF_STATE_UNSPECIFIED = 0;
+ // This component is deprecated
+ DEPRECATED = 1;
+ // This component is not deprecated
+ ACTIVE = 2;
+ }
+
+ // Resource name of the component, aka identifier.
+ // the API will always return ComponentDef names with format:
+ // projects/{project}/componentDefs/<component_def_id>.
+ // However the API will accept ComponentDef names with formats:
+ // projects/{project}/componentDefs/<component_def_id>|<value>.
+ string name = 1;
+ // String value of the component, ie 'Tools>Stability' or 'Blink'.
+ string value = 2;
+ // Brief explanation of this component.
+ string docstring = 3;
+ // Administrators of this component.
+ repeated string admins = 4 [
+ (google.api.resource_reference) = { type: "api.crbug.com/User" }
+ ];
+ // Auto cc'ed users of this component.
+ repeated string ccs = 5 [
+ (google.api.resource_reference) = { type: "api.crbug.com/User" }
+ ];
+ // State of this component.
+ ComponentDefState state = 6;
+ // The user that created this component.
+ string creator = 7 [
+ (google.api.resource_reference) = { type: "api.crbug.com/User" },
+ (google.api.field_behavior) = OUTPUT_ONLY
+ ];
+ // The user that last modified this component.
+ string modifier = 8 [
+ (google.api.resource_reference) = { type: "api.crbug.com/User" },
+ (google.api.field_behavior) = OUTPUT_ONLY
+ ];
+ // The time this component was created.
+ google.protobuf.Timestamp create_time = 9 [
+ (google.api.field_behavior) = OUTPUT_ONLY
+ ];
+ // The time this component was last modified.
+ google.protobuf.Timestamp modify_time = 10 [
+ (google.api.field_behavior) = OUTPUT_ONLY
+ ];
+ // Labels that auto-apply to issues in this component.
+ repeated string labels = 11;
+}
+
+// Defines approvals that issues within the project may need.
+// See monorail/doc/userguide/concepts.md#issue-approvals-and-gates and
+// monorail/doc/userguide/project-owners.md#How-to-configure-approvals
+// Check bugs.chromium.org/p/{project}/adminLabels to see the ApprovalDef IDs.
+// If your code needs to call multiple monorail instances
+// (e.g. monorail-{prod|staging|dev}) ApprovalDef IDs for ApprovalDefs
+// with the same display_name will differ between each monorail
+// instance. To see what ApprovalDef ID to use when calling staging
+// you must check bugs-staging.chromium.org/p/{project}/adminLabels.
+// Next available tag: 7
+message ApprovalDef {
+ option (google.api.resource) = {
+ type: "api.crbug.com/ApprovalDef"
+ pattern: "projects/{project}/approvalDefs/{approval_def_id}"
+ };
+
+ // Resource name of the approval.
+ string name = 1;
+ // Display name of the field.
+ string display_name = 2 [(google.api.field_behavior) = IMMUTABLE];
+ // Brief explanation of this field.
+ string docstring = 3;
+ // Information approvers need from requester.
+ // May be adjusted on the issue after creation.
+ string survey = 4;
+ // Default list of users who can approve this field.
+ // May be adjusted on the issue after creation.
+ repeated string approvers = 5 [
+ (google.api.resource_reference) = { type: "api.crbug.com/User" }
+ ];
+ // Administrators of this field.
+ repeated string admins = 6 [
+ (google.api.resource_reference) = { type: "api.crbug.com/User" }
+ ];
+}
+
+
+// Defines saved queries that belong to a project
+//
+// Next available tag: 4
+message ProjectSavedQuery {
+ option (google.api.resource) = {
+ type: "api.crbug.com/ProjectSavedQuery"
+ pattern: "projects/{project}/savedQueries/{saved_query_id}"
+ };
+
+ // Resource name of this saved query.
+ string name = 1;
+ // Display name of this saved query, ie 'open issues'.
+ string display_name = 2;
+ // Search term of this saved query.
+ string query = 3;
+}
+
+
+// Defines a template for filling issues.
+// Next available tag: 10
+message IssueTemplate {
+ option (google.api.resource) = {
+ type: "api.crbug.com/IssueTemplate"
+ pattern: "projects/{project}/templates/{template_id}"
+ };
+ // Resource name of the template.
+ string name = 1;
+ // Display name of this template.
+ string display_name = 2 [(google.api.field_behavior) = IMMUTABLE];
+ // Canonical Issue for this template.
+ Issue issue = 3;
+ // ApprovalValues to be created with the issue when using this template.
+ repeated ApprovalValue approval_values = 9;
+ // Boolean indicating subsequent issue creation must have delta in summary.
+ bool summary_must_be_edited = 4;
+ // Visibility permission of template.
+ // Next available tag: 3
+ enum TemplatePrivacy {
+ // This value is unused.
+ TEMPLATE_PRIVACY_UNSPECIFIED = 0;
+ // Owner project members may view this template.
+ MEMBERS_ONLY = 1;
+ // Anyone on the web can view this template.
+ PUBLIC = 2;
+ }
+ TemplatePrivacy template_privacy = 5;
+ // Indicator of who if anyone should be the default owner of the issue
+ // created with this template.
+ // Next available tag: 2
+ enum DefaultOwner {
+ // There is no default owner.
+ // This value is used if the default owner is omitted.
+ DEFAULT_OWNER_UNSPECIFIED = 0;
+ // The owner should default to the Issue reporter if the reporter is a
+ // member of the project.
+ PROJECT_MEMBER_REPORTER = 1;
+ }
+ DefaultOwner default_owner = 6;
+ // Boolean indicating whether issue must have a component.
+ bool component_required = 7;
+ // Names of Users who can administer this template.
+ repeated string admins = 8 [
+ (google.api.resource_reference) = { type: "api.crbug.com/User" }];
+}
+
+
+// Defines configurations of a project
+//
+// Next available tag: 11
+message ProjectConfig {
+ option (google.api.resource) = {
+ type: "api.crbug.com/ProjectConfig"
+ pattern: "projects/{project}/config"
+ };
+
+ // Resource name of the project config.
+ string name = 1;
+ // Set of label prefixes that only apply once per issue.
+ // E.g. priority, since no issue can be both Priority-High and Priority-Low.
+ repeated string exclusive_label_prefixes = 2;
+ // Default search query for this project's members.
+ string member_default_query = 3;
+ // TODO(crbug.com/monorail/7517): consider using IssuesListColumn
+ // Default sort specification for this project.
+ string default_sort = 4;
+ // Default columns for displaying issue list for this project.
+ repeated IssuesListColumn default_columns = 5;
+ // Grid view configurations.
+ // Next available tag: 3
+ message GridViewConfig {
+ // Default column dimension in grid view for this project.
+ string default_x_attr = 1;
+ // Default row dimension in grid view for this project.
+ string default_y_attr = 2;
+ }
+ GridViewConfig project_grid_config = 6;
+ // Default template used for issue entry for members of this project.
+ string member_default_template = 7 [
+ (google.api.resource_reference) = { type: "api.crbug.com/Template" }];
+ // Default template used for issue entry for non-members of this project.
+ string non_members_default_template = 8 [
+ (google.api.resource_reference) = { type: "api.crbug.com/Template" }];
+ // URL to browse project's source code revisions for any given revnum.
+ // E.g. https://crrev.com/{revnum}
+ string revision_url_format = 9;
+ // A project's custom URL for the "New issue" link, only if specified.
+ string custom_issue_entry_url = 10;
+}
+
+// Specifies info for a member of a project.
+//
+// Next available tag: 7
+message ProjectMember {
+ // Resource name of the Project Member.
+ // projects/{project}/members/{user_id}
+ string name = 1;
+ // The role the user has in the project.
+ // Next available tag: 4
+ enum ProjectRole {
+ // The user has no role in the project.
+ PROJECT_ROLE_UNSPECIFIED = 0;
+ // The user can make any changes to the project.
+ OWNER = 1;
+ // The user may participate in the project but may not edit the project.
+ COMMITTER = 2;
+ // The user starts with the same permissions as a non-member.
+ CONTRIBUTOR = 3;
+ }
+ ProjectRole role = 2;
+ // Which built-in/standard permissions the user has set.
+ repeated Permission standard_perms = 3;
+ // Custom permissions defined for the user.
+ // eg. "Google" in "Restrict-View-Google" is an example custom permission.
+ repeated string custom_perms = 4;
+ // Annotations about a user configured by project owners.
+ // Visible to anyone who can see the project's settings.
+ string notes = 5;
+ // Whether the user should show up in autocomplete.
+ // Next available tag: 3
+ enum AutocompleteVisibility {
+ // No autocomplete visibility value specified.
+ AUTOCOMPLETE_VISIBILITY_UNSPECIFIED = 0;
+ // The user should not show up in autocomplete.
+ HIDDEN = 1;
+ // The user may show up in autocomplete.
+ SHOWN = 2;
+ }
+ AutocompleteVisibility include_in_autocomplete = 6;
+}
diff --git a/api/v3/api_proto/project_objects_pb2.py b/api/v3/api_proto/project_objects_pb2.py
new file mode 100644
index 0000000..52f0cbb
--- /dev/null
+++ b/api/v3/api_proto/project_objects_pb2.py
@@ -0,0 +1,1715 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/v3/api_proto/project_objects.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()
+
+
+from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
+from google.api import field_behavior_pb2 as google_dot_api_dot_field__behavior__pb2
+from google.api import resource_pb2 as google_dot_api_dot_resource__pb2
+from api.v3.api_proto import issue_objects_pb2 as api_dot_v3_dot_api__proto_dot_issue__objects__pb2
+from api.v3.api_proto import permission_objects_pb2 as api_dot_v3_dot_api__proto_dot_permission__objects__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='api/v3/api_proto/project_objects.proto',
+ package='monorail.v3',
+ syntax='proto3',
+ serialized_options=b'Z\020api/v3/api_proto',
+ create_key=_descriptor._internal_create_key,
+ serialized_pb=b'\n&api/v3/api_proto/project_objects.proto\x12\x0bmonorail.v3\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a$api/v3/api_proto/issue_objects.proto\x1a)api/v3/api_proto/permission_objects.proto\"\x8a\x01\n\x07Project\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x19\n\x0c\x64isplay_name\x18\x02 \x01(\tB\x03\xe0\x41\x05\x12\x0f\n\x07summary\x18\x03 \x01(\t\x12\x15\n\rthumbnail_url\x18\x04 \x01(\t:.\xea\x41+\n\x15\x61pi.crbug.com/Project\x12\x12projects/{project}\"\xa1\x03\n\tStatusDef\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\x12\x32\n\x04type\x18\x03 \x01(\x0e\x32$.monorail.v3.StatusDef.StatusDefType\x12\x0c\n\x04rank\x18\x04 \x01(\r\x12\x11\n\tdocstring\x18\x05 \x01(\t\x12\x34\n\x05state\x18\x06 \x01(\x0e\x32%.monorail.v3.StatusDef.StatusDefState\"R\n\rStatusDefType\x12\x1f\n\x1bSTATUS_DEF_TYPE_UNSPECIFIED\x10\x00\x12\x08\n\x04OPEN\x10\x01\x12\n\n\x06\x43LOSED\x10\x02\x12\n\n\x06MERGED\x10\x03\"N\n\x0eStatusDefState\x12 \n\x1cSTATUS_DEF_STATE_UNSPECIFIED\x10\x00\x12\x0e\n\nDEPRECATED\x10\x01\x12\n\n\x06\x41\x43TIVE\x10\x02:H\xea\x41\x45\n\x17\x61pi.crbug.com/StatusDef\x12*projects/{project}/statusDefs/{status_def}\"\x83\x02\n\x08LabelDef\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\x12\x11\n\tdocstring\x18\x03 \x01(\t\x12\x32\n\x05state\x18\x04 \x01(\x0e\x32#.monorail.v3.LabelDef.LabelDefState\"L\n\rLabelDefState\x12\x1f\n\x1bLABEL_DEF_STATE_UNSPECIFIED\x10\x00\x12\x0e\n\nDEPRECATED\x10\x01\x12\n\n\x06\x41\x43TIVE\x10\x02:E\xea\x41\x42\n\x16\x61pi.crbug.com/LabelDef\x12(projects/{project}/labelDefs/{label_def}\"\xcb\r\n\x08\x46ieldDef\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x19\n\x0c\x64isplay_name\x18\x02 \x01(\tB\x03\xe0\x41\x05\x12\x11\n\tdocstring\x18\x03 \x01(\t\x12-\n\x04type\x18\x04 \x01(\x0e\x32\x1a.monorail.v3.FieldDef.TypeB\x03\xe0\x41\x05\x12\x1d\n\x15\x61pplicable_issue_type\x18\x05 \x01(\t\x12\'\n\x06\x61\x64mins\x18\x06 \x03(\tB\x17\xfa\x41\x14\n\x12\x61pi.crbug.com/User\x12,\n\x06traits\x18\x07 \x03(\x0e\x32\x1c.monorail.v3.FieldDef.Traits\x12:\n\x0f\x61pproval_parent\x18\x08 \x01(\tB!\xfa\x41\x1b\n\x19\x61pi.crbug.com/ApprovalDef\xe0\x41\x05\x12=\n\renum_settings\x18\t \x01(\x0b\x32&.monorail.v3.FieldDef.EnumTypeSettings\x12;\n\x0cint_settings\x18\n \x01(\x0b\x32%.monorail.v3.FieldDef.IntTypeSettings\x12;\n\x0cstr_settings\x18\x0b \x01(\x0b\x32%.monorail.v3.FieldDef.StrTypeSettings\x12=\n\ruser_settings\x18\x0c \x01(\x0b\x32&.monorail.v3.FieldDef.UserTypeSettings\x12=\n\rdate_settings\x18\r \x01(\x0b\x32&.monorail.v3.FieldDef.DateTypeSettings\x12(\n\x07\x65\x64itors\x18\x0e \x03(\tB\x17\xfa\x41\x14\n\x12\x61pi.crbug.com/User\x1a~\n\x10\x45numTypeSettings\x12>\n\x07\x63hoices\x18\x01 \x03(\x0b\x32-.monorail.v3.FieldDef.EnumTypeSettings.Choice\x1a*\n\x06\x43hoice\x12\r\n\x05value\x18\x01 \x01(\t\x12\x11\n\tdocstring\x18\x02 \x01(\t\x1a\x37\n\x0fIntTypeSettings\x12\x11\n\tmin_value\x18\x01 \x01(\x05\x12\x11\n\tmax_value\x18\x02 \x01(\x05\x1a \n\x0fStrTypeSettings\x12\r\n\x05regex\x18\x01 \x01(\t\x1a\x92\x03\n\x10UserTypeSettings\x12N\n\x0fnotify_triggers\x18\x01 \x01(\x0e\x32\x35.monorail.v3.FieldDef.UserTypeSettings.NotifyTriggers\x12R\n\x11role_requirements\x18\x02 \x01(\x0e\x32\x37.monorail.v3.FieldDef.UserTypeSettings.RoleRequirements\x12\x13\n\x0bgrants_perm\x18\x03 \x01(\t\x12\x12\n\nneeds_perm\x18\x04 \x01(\t\"M\n\x0eNotifyTriggers\x12\x1f\n\x1bNOTIFY_TRIGGERS_UNSPECIFIED\x10\x00\x12\t\n\x05NEVER\x10\x01\x12\x0f\n\x0b\x41NY_COMMENT\x10\x02\"b\n\x10RoleRequirements\x12!\n\x1dROLE_REQUIREMENTS_UNSPECIFIED\x10\x00\x12\x17\n\x13NO_ROLE_REQUIREMENT\x10\x01\x12\x12\n\x0ePROJECT_MEMBER\x10\x02\x1a\xbf\x01\n\x10\x44\x61teTypeSettings\x12\x46\n\x0b\x64\x61te_action\x18\x01 \x01(\x0e\x32\x31.monorail.v3.FieldDef.DateTypeSettings.DateAction\"c\n\nDateAction\x12\x1b\n\x17\x44\x41TE_ACTION_UNSPECIFIED\x10\x00\x12\r\n\tNO_ACTION\x10\x01\x12\x10\n\x0cNOTIFY_OWNER\x10\x02\x12\x17\n\x13NOTIFY_PARTICIPANTS\x10\x03\"U\n\x04Type\x12\x14\n\x10TYPE_UNSPECIFIED\x10\x00\x12\x08\n\x04\x45NUM\x10\x01\x12\x07\n\x03INT\x10\x02\x12\x07\n\x03STR\x10\x03\x12\x08\n\x04USER\x10\x04\x12\x08\n\x04\x44\x41TE\x10\x05\x12\x07\n\x03URL\x10\x06\"n\n\x06Traits\x12\x16\n\x12TRAITS_UNSPECIFIED\x10\x00\x12\x0c\n\x08REQUIRED\x10\x01\x12\x12\n\x0e\x44\x45\x46\x41ULT_HIDDEN\x10\x02\x12\x0f\n\x0bMULTIVALUED\x10\x03\x12\t\n\x05PHASE\x10\x04\x12\x0e\n\nRESTRICTED\x10\x05:H\xea\x41\x45\n\x16\x61pi.crbug.com/FieldDef\x12+projects/{project}/fieldDefs/{field_def_id}\"\xcc\x04\n\x0c\x43omponentDef\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\x12\x11\n\tdocstring\x18\x03 \x01(\t\x12\'\n\x06\x61\x64mins\x18\x04 \x03(\tB\x17\xfa\x41\x14\n\x12\x61pi.crbug.com/User\x12$\n\x03\x63\x63s\x18\x05 \x03(\tB\x17\xfa\x41\x14\n\x12\x61pi.crbug.com/User\x12:\n\x05state\x18\x06 \x01(\x0e\x32+.monorail.v3.ComponentDef.ComponentDefState\x12+\n\x07\x63reator\x18\x07 \x01(\tB\x1a\xfa\x41\x14\n\x12\x61pi.crbug.com/User\xe0\x41\x03\x12,\n\x08modifier\x18\x08 \x01(\tB\x1a\xfa\x41\x14\n\x12\x61pi.crbug.com/User\xe0\x41\x03\x12\x34\n\x0b\x63reate_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03\x12\x34\n\x0bmodify_time\x18\n \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03\x12\x0e\n\x06labels\x18\x0b \x03(\t\"T\n\x11\x43omponentDefState\x12#\n\x1f\x43OMPONENT_DEF_STATE_UNSPECIFIED\x10\x00\x12\x0e\n\nDEPRECATED\x10\x01\x12\n\n\x06\x41\x43TIVE\x10\x02:T\xea\x41Q\n\x1a\x61pi.crbug.com/ComponentDef\x12\x33projects/{project}/componentDefs/{component_def_id}\"\x81\x02\n\x0b\x41pprovalDef\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x19\n\x0c\x64isplay_name\x18\x02 \x01(\tB\x03\xe0\x41\x05\x12\x11\n\tdocstring\x18\x03 \x01(\t\x12\x0e\n\x06survey\x18\x04 \x01(\t\x12*\n\tapprovers\x18\x05 \x03(\tB\x17\xfa\x41\x14\n\x12\x61pi.crbug.com/User\x12\'\n\x06\x61\x64mins\x18\x06 \x03(\tB\x17\xfa\x41\x14\n\x12\x61pi.crbug.com/User:Q\xea\x41N\n\x19\x61pi.crbug.com/ApprovalDef\x12\x31projects/{project}/approvalDefs/{approval_def_id}\"\x9e\x01\n\x11ProjectSavedQuery\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12\r\n\x05query\x18\x03 \x01(\t:V\xea\x41S\n\x1f\x61pi.crbug.com/ProjectSavedQuery\x12\x30projects/{project}/savedQueries/{saved_query_id}\"\xe8\x04\n\rIssueTemplate\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x19\n\x0c\x64isplay_name\x18\x02 \x01(\tB\x03\xe0\x41\x05\x12!\n\x05issue\x18\x03 \x01(\x0b\x32\x12.monorail.v3.Issue\x12\x33\n\x0f\x61pproval_values\x18\t \x03(\x0b\x32\x1a.monorail.v3.ApprovalValue\x12\x1e\n\x16summary_must_be_edited\x18\x04 \x01(\x08\x12\x44\n\x10template_privacy\x18\x05 \x01(\x0e\x32*.monorail.v3.IssueTemplate.TemplatePrivacy\x12>\n\rdefault_owner\x18\x06 \x01(\x0e\x32\'.monorail.v3.IssueTemplate.DefaultOwner\x12\x1a\n\x12\x63omponent_required\x18\x07 \x01(\x08\x12\'\n\x06\x61\x64mins\x18\x08 \x03(\tB\x17\xfa\x41\x14\n\x12\x61pi.crbug.com/User\"Q\n\x0fTemplatePrivacy\x12 \n\x1cTEMPLATE_PRIVACY_UNSPECIFIED\x10\x00\x12\x10\n\x0cMEMBERS_ONLY\x10\x01\x12\n\n\x06PUBLIC\x10\x02\"J\n\x0c\x44\x65\x66\x61ultOwner\x12\x1d\n\x19\x44\x45\x46\x41ULT_OWNER_UNSPECIFIED\x10\x00\x12\x1b\n\x17PROJECT_MEMBER_REPORTER\x10\x01:L\xea\x41I\n\x1b\x61pi.crbug.com/IssueTemplate\x12*projects/{project}/templates/{template_id}\"\xb0\x04\n\rProjectConfig\x12\x0c\n\x04name\x18\x01 \x01(\t\x12 \n\x18\x65xclusive_label_prefixes\x18\x02 \x03(\t\x12\x1c\n\x14member_default_query\x18\x03 \x01(\t\x12\x14\n\x0c\x64\x65\x66\x61ult_sort\x18\x04 \x01(\t\x12\x36\n\x0f\x64\x65\x66\x61ult_columns\x18\x05 \x03(\x0b\x32\x1d.monorail.v3.IssuesListColumn\x12\x46\n\x13project_grid_config\x18\x06 \x01(\x0b\x32).monorail.v3.ProjectConfig.GridViewConfig\x12<\n\x17member_default_template\x18\x07 \x01(\tB\x1b\xfa\x41\x18\n\x16\x61pi.crbug.com/Template\x12\x41\n\x1cnon_members_default_template\x18\x08 \x01(\tB\x1b\xfa\x41\x18\n\x16\x61pi.crbug.com/Template\x12\x1b\n\x13revision_url_format\x18\t \x01(\t\x12\x1e\n\x16\x63ustom_issue_entry_url\x18\n \x01(\t\x1a@\n\x0eGridViewConfig\x12\x16\n\x0e\x64\x65\x66\x61ult_x_attr\x18\x01 \x01(\t\x12\x16\n\x0e\x64\x65\x66\x61ult_y_attr\x18\x02 \x01(\t:;\xea\x41\x38\n\x1b\x61pi.crbug.com/ProjectConfig\x12\x19projects/{project}/config\"\xaf\x03\n\rProjectMember\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x34\n\x04role\x18\x02 \x01(\x0e\x32&.monorail.v3.ProjectMember.ProjectRole\x12/\n\x0estandard_perms\x18\x03 \x03(\x0e\x32\x17.monorail.v3.Permission\x12\x14\n\x0c\x63ustom_perms\x18\x04 \x03(\t\x12\r\n\x05notes\x18\x05 \x01(\t\x12R\n\x17include_in_autocomplete\x18\x06 \x01(\x0e\x32\x31.monorail.v3.ProjectMember.AutocompleteVisibility\"V\n\x0bProjectRole\x12\x1c\n\x18PROJECT_ROLE_UNSPECIFIED\x10\x00\x12\t\n\x05OWNER\x10\x01\x12\r\n\tCOMMITTER\x10\x02\x12\x0f\n\x0b\x43ONTRIBUTOR\x10\x03\"X\n\x16\x41utocompleteVisibility\x12\'\n#AUTOCOMPLETE_VISIBILITY_UNSPECIFIED\x10\x00\x12\n\n\x06HIDDEN\x10\x01\x12\t\n\x05SHOWN\x10\x02\x42\x12Z\x10\x61pi/v3/api_protob\x06proto3'
+ ,
+ dependencies=[google_dot_protobuf_dot_timestamp__pb2.DESCRIPTOR,google_dot_api_dot_field__behavior__pb2.DESCRIPTOR,google_dot_api_dot_resource__pb2.DESCRIPTOR,api_dot_v3_dot_api__proto_dot_issue__objects__pb2.DESCRIPTOR,api_dot_v3_dot_api__proto_dot_permission__objects__pb2.DESCRIPTOR,])
+
+
+
+_STATUSDEF_STATUSDEFTYPE = _descriptor.EnumDescriptor(
+ name='StatusDefType',
+ full_name='monorail.v3.StatusDef.StatusDefType',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='STATUS_DEF_TYPE_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='OPEN', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='CLOSED', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='MERGED', index=3, number=3,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=552,
+ serialized_end=634,
+)
+_sym_db.RegisterEnumDescriptor(_STATUSDEF_STATUSDEFTYPE)
+
+_STATUSDEF_STATUSDEFSTATE = _descriptor.EnumDescriptor(
+ name='StatusDefState',
+ full_name='monorail.v3.StatusDef.StatusDefState',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='STATUS_DEF_STATE_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='DEPRECATED', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='ACTIVE', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=636,
+ serialized_end=714,
+)
+_sym_db.RegisterEnumDescriptor(_STATUSDEF_STATUSDEFSTATE)
+
+_LABELDEF_LABELDEFSTATE = _descriptor.EnumDescriptor(
+ name='LabelDefState',
+ full_name='monorail.v3.LabelDef.LabelDefState',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='LABEL_DEF_STATE_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='DEPRECATED', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='ACTIVE', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=903,
+ serialized_end=979,
+)
+_sym_db.RegisterEnumDescriptor(_LABELDEF_LABELDEFSTATE)
+
+_FIELDDEF_USERTYPESETTINGS_NOTIFYTRIGGERS = _descriptor.EnumDescriptor(
+ name='NotifyTriggers',
+ full_name='monorail.v3.FieldDef.UserTypeSettings.NotifyTriggers',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='NOTIFY_TRIGGERS_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='NEVER', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='ANY_COMMENT', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=2148,
+ serialized_end=2225,
+)
+_sym_db.RegisterEnumDescriptor(_FIELDDEF_USERTYPESETTINGS_NOTIFYTRIGGERS)
+
+_FIELDDEF_USERTYPESETTINGS_ROLEREQUIREMENTS = _descriptor.EnumDescriptor(
+ name='RoleRequirements',
+ full_name='monorail.v3.FieldDef.UserTypeSettings.RoleRequirements',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='ROLE_REQUIREMENTS_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='NO_ROLE_REQUIREMENT', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='PROJECT_MEMBER', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=2227,
+ serialized_end=2325,
+)
+_sym_db.RegisterEnumDescriptor(_FIELDDEF_USERTYPESETTINGS_ROLEREQUIREMENTS)
+
+_FIELDDEF_DATETYPESETTINGS_DATEACTION = _descriptor.EnumDescriptor(
+ name='DateAction',
+ full_name='monorail.v3.FieldDef.DateTypeSettings.DateAction',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='DATE_ACTION_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='NO_ACTION', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='NOTIFY_OWNER', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='NOTIFY_PARTICIPANTS', index=3, number=3,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=2420,
+ serialized_end=2519,
+)
+_sym_db.RegisterEnumDescriptor(_FIELDDEF_DATETYPESETTINGS_DATEACTION)
+
+_FIELDDEF_TYPE = _descriptor.EnumDescriptor(
+ name='Type',
+ full_name='monorail.v3.FieldDef.Type',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='TYPE_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='ENUM', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='INT', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='STR', index=3, number=3,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='USER', index=4, number=4,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='DATE', index=5, number=5,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='URL', index=6, number=6,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=2521,
+ serialized_end=2606,
+)
+_sym_db.RegisterEnumDescriptor(_FIELDDEF_TYPE)
+
+_FIELDDEF_TRAITS = _descriptor.EnumDescriptor(
+ name='Traits',
+ full_name='monorail.v3.FieldDef.Traits',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='TRAITS_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='REQUIRED', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='DEFAULT_HIDDEN', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='MULTIVALUED', index=3, number=3,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='PHASE', index=4, number=4,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='RESTRICTED', index=5, number=5,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=2608,
+ serialized_end=2718,
+)
+_sym_db.RegisterEnumDescriptor(_FIELDDEF_TRAITS)
+
+_COMPONENTDEF_COMPONENTDEFSTATE = _descriptor.EnumDescriptor(
+ name='ComponentDefState',
+ full_name='monorail.v3.ComponentDef.ComponentDefState',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='COMPONENT_DEF_STATE_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='DEPRECATED', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='ACTIVE', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=3213,
+ serialized_end=3297,
+)
+_sym_db.RegisterEnumDescriptor(_COMPONENTDEF_COMPONENTDEFSTATE)
+
+_ISSUETEMPLATE_TEMPLATEPRIVACY = _descriptor.EnumDescriptor(
+ name='TemplatePrivacy',
+ full_name='monorail.v3.IssueTemplate.TemplatePrivacy',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='TEMPLATE_PRIVACY_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='MEMBERS_ONLY', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='PUBLIC', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=4188,
+ serialized_end=4269,
+)
+_sym_db.RegisterEnumDescriptor(_ISSUETEMPLATE_TEMPLATEPRIVACY)
+
+_ISSUETEMPLATE_DEFAULTOWNER = _descriptor.EnumDescriptor(
+ name='DefaultOwner',
+ full_name='monorail.v3.IssueTemplate.DefaultOwner',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='DEFAULT_OWNER_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='PROJECT_MEMBER_REPORTER', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=4271,
+ serialized_end=4345,
+)
+_sym_db.RegisterEnumDescriptor(_ISSUETEMPLATE_DEFAULTOWNER)
+
+_PROJECTMEMBER_PROJECTROLE = _descriptor.EnumDescriptor(
+ name='ProjectRole',
+ full_name='monorail.v3.ProjectMember.ProjectRole',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='PROJECT_ROLE_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='OWNER', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='COMMITTER', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='CONTRIBUTOR', index=3, number=3,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=5244,
+ serialized_end=5330,
+)
+_sym_db.RegisterEnumDescriptor(_PROJECTMEMBER_PROJECTROLE)
+
+_PROJECTMEMBER_AUTOCOMPLETEVISIBILITY = _descriptor.EnumDescriptor(
+ name='AutocompleteVisibility',
+ full_name='monorail.v3.ProjectMember.AutocompleteVisibility',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='AUTOCOMPLETE_VISIBILITY_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='HIDDEN', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='SHOWN', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=5332,
+ serialized_end=5420,
+)
+_sym_db.RegisterEnumDescriptor(_PROJECTMEMBER_AUTOCOMPLETEVISIBILITY)
+
+
+_PROJECT = _descriptor.Descriptor(
+ name='Project',
+ full_name='monorail.v3.Project',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.Project.name', 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.v3.Project.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=b'\340A\005', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='summary', full_name='monorail.v3.Project.summary', 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='thumbnail_url', full_name='monorail.v3.Project.thumbnail_url', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=b'\352A+\n\025api.crbug.com/Project\022\022projects/{project}',
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=230,
+ serialized_end=368,
+)
+
+
+_STATUSDEF = _descriptor.Descriptor(
+ name='StatusDef',
+ full_name='monorail.v3.StatusDef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.StatusDef.name', 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='value', full_name='monorail.v3.StatusDef.value', 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='type', full_name='monorail.v3.StatusDef.type', index=2,
+ number=3, type=14, cpp_type=8, 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='rank', full_name='monorail.v3.StatusDef.rank', index=3,
+ number=4, type=13, cpp_type=3, 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='docstring', full_name='monorail.v3.StatusDef.docstring', index=4,
+ number=5, 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='state', full_name='monorail.v3.StatusDef.state', index=5,
+ number=6, type=14, cpp_type=8, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ _STATUSDEF_STATUSDEFTYPE,
+ _STATUSDEF_STATUSDEFSTATE,
+ ],
+ serialized_options=b'\352AE\n\027api.crbug.com/StatusDef\022*projects/{project}/statusDefs/{status_def}',
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=371,
+ serialized_end=788,
+)
+
+
+_LABELDEF = _descriptor.Descriptor(
+ name='LabelDef',
+ full_name='monorail.v3.LabelDef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.LabelDef.name', 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='value', full_name='monorail.v3.LabelDef.value', 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='docstring', full_name='monorail.v3.LabelDef.docstring', 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='state', full_name='monorail.v3.LabelDef.state', index=3,
+ number=4, type=14, cpp_type=8, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ _LABELDEF_LABELDEFSTATE,
+ ],
+ serialized_options=b'\352AB\n\026api.crbug.com/LabelDef\022(projects/{project}/labelDefs/{label_def}',
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=791,
+ serialized_end=1050,
+)
+
+
+_FIELDDEF_ENUMTYPESETTINGS_CHOICE = _descriptor.Descriptor(
+ name='Choice',
+ full_name='monorail.v3.FieldDef.EnumTypeSettings.Choice',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='value', full_name='monorail.v3.FieldDef.EnumTypeSettings.Choice.value', 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='docstring', full_name='monorail.v3.FieldDef.EnumTypeSettings.Choice.docstring', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1787,
+ serialized_end=1829,
+)
+
+_FIELDDEF_ENUMTYPESETTINGS = _descriptor.Descriptor(
+ name='EnumTypeSettings',
+ full_name='monorail.v3.FieldDef.EnumTypeSettings',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='choices', full_name='monorail.v3.FieldDef.EnumTypeSettings.choices', 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=[_FIELDDEF_ENUMTYPESETTINGS_CHOICE, ],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1703,
+ serialized_end=1829,
+)
+
+_FIELDDEF_INTTYPESETTINGS = _descriptor.Descriptor(
+ name='IntTypeSettings',
+ full_name='monorail.v3.FieldDef.IntTypeSettings',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='min_value', full_name='monorail.v3.FieldDef.IntTypeSettings.min_value', 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='max_value', full_name='monorail.v3.FieldDef.IntTypeSettings.max_value', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1831,
+ serialized_end=1886,
+)
+
+_FIELDDEF_STRTYPESETTINGS = _descriptor.Descriptor(
+ name='StrTypeSettings',
+ full_name='monorail.v3.FieldDef.StrTypeSettings',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='regex', full_name='monorail.v3.FieldDef.StrTypeSettings.regex', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1888,
+ serialized_end=1920,
+)
+
+_FIELDDEF_USERTYPESETTINGS = _descriptor.Descriptor(
+ name='UserTypeSettings',
+ full_name='monorail.v3.FieldDef.UserTypeSettings',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='notify_triggers', full_name='monorail.v3.FieldDef.UserTypeSettings.notify_triggers', index=0,
+ number=1, type=14, cpp_type=8, 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='role_requirements', full_name='monorail.v3.FieldDef.UserTypeSettings.role_requirements', index=1,
+ number=2, type=14, cpp_type=8, 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='grants_perm', full_name='monorail.v3.FieldDef.UserTypeSettings.grants_perm', 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='needs_perm', full_name='monorail.v3.FieldDef.UserTypeSettings.needs_perm', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ _FIELDDEF_USERTYPESETTINGS_NOTIFYTRIGGERS,
+ _FIELDDEF_USERTYPESETTINGS_ROLEREQUIREMENTS,
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1923,
+ serialized_end=2325,
+)
+
+_FIELDDEF_DATETYPESETTINGS = _descriptor.Descriptor(
+ name='DateTypeSettings',
+ full_name='monorail.v3.FieldDef.DateTypeSettings',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='date_action', full_name='monorail.v3.FieldDef.DateTypeSettings.date_action', index=0,
+ number=1, type=14, cpp_type=8, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ _FIELDDEF_DATETYPESETTINGS_DATEACTION,
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2328,
+ serialized_end=2519,
+)
+
+_FIELDDEF = _descriptor.Descriptor(
+ name='FieldDef',
+ full_name='monorail.v3.FieldDef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.FieldDef.name', 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.v3.FieldDef.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=b'\340A\005', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='docstring', full_name='monorail.v3.FieldDef.docstring', 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='type', full_name='monorail.v3.FieldDef.type', index=3,
+ number=4, type=14, cpp_type=8, 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=b'\340A\005', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='applicable_issue_type', full_name='monorail.v3.FieldDef.applicable_issue_type', index=4,
+ number=5, 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='admins', full_name='monorail.v3.FieldDef.admins', index=5,
+ number=6, 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=b'\372A\024\n\022api.crbug.com/User', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='traits', full_name='monorail.v3.FieldDef.traits', index=6,
+ number=7, type=14, cpp_type=8, 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='approval_parent', full_name='monorail.v3.FieldDef.approval_parent', index=7,
+ number=8, 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=b'\372A\033\n\031api.crbug.com/ApprovalDef\340A\005', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='enum_settings', full_name='monorail.v3.FieldDef.enum_settings', index=8,
+ number=9, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ 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='int_settings', full_name='monorail.v3.FieldDef.int_settings', index=9,
+ number=10, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ 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='str_settings', full_name='monorail.v3.FieldDef.str_settings', index=10,
+ number=11, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ 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='user_settings', full_name='monorail.v3.FieldDef.user_settings', index=11,
+ number=12, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ 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='date_settings', full_name='monorail.v3.FieldDef.date_settings', index=12,
+ number=13, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ 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='editors', full_name='monorail.v3.FieldDef.editors', index=13,
+ number=14, 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=b'\372A\024\n\022api.crbug.com/User', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ ],
+ extensions=[
+ ],
+ nested_types=[_FIELDDEF_ENUMTYPESETTINGS, _FIELDDEF_INTTYPESETTINGS, _FIELDDEF_STRTYPESETTINGS, _FIELDDEF_USERTYPESETTINGS, _FIELDDEF_DATETYPESETTINGS, ],
+ enum_types=[
+ _FIELDDEF_TYPE,
+ _FIELDDEF_TRAITS,
+ ],
+ serialized_options=b'\352AE\n\026api.crbug.com/FieldDef\022+projects/{project}/fieldDefs/{field_def_id}',
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1053,
+ serialized_end=2792,
+)
+
+
+_COMPONENTDEF = _descriptor.Descriptor(
+ name='ComponentDef',
+ full_name='monorail.v3.ComponentDef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.ComponentDef.name', 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='value', full_name='monorail.v3.ComponentDef.value', 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='docstring', full_name='monorail.v3.ComponentDef.docstring', 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='admins', full_name='monorail.v3.ComponentDef.admins', index=3,
+ number=4, 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=b'\372A\024\n\022api.crbug.com/User', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='ccs', full_name='monorail.v3.ComponentDef.ccs', 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=b'\372A\024\n\022api.crbug.com/User', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='state', full_name='monorail.v3.ComponentDef.state', index=5,
+ number=6, type=14, cpp_type=8, 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='creator', full_name='monorail.v3.ComponentDef.creator', index=6,
+ number=7, 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=b'\372A\024\n\022api.crbug.com/User\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='modifier', full_name='monorail.v3.ComponentDef.modifier', index=7,
+ number=8, 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=b'\372A\024\n\022api.crbug.com/User\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='create_time', full_name='monorail.v3.ComponentDef.create_time', index=8,
+ number=9, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='modify_time', full_name='monorail.v3.ComponentDef.modify_time', index=9,
+ number=10, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='labels', full_name='monorail.v3.ComponentDef.labels', index=10,
+ number=11, 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=[
+ _COMPONENTDEF_COMPONENTDEFSTATE,
+ ],
+ serialized_options=b'\352AQ\n\032api.crbug.com/ComponentDef\0223projects/{project}/componentDefs/{component_def_id}',
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=2795,
+ serialized_end=3383,
+)
+
+
+_APPROVALDEF = _descriptor.Descriptor(
+ name='ApprovalDef',
+ full_name='monorail.v3.ApprovalDef',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.ApprovalDef.name', 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.v3.ApprovalDef.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=b'\340A\005', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='docstring', full_name='monorail.v3.ApprovalDef.docstring', 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='survey', full_name='monorail.v3.ApprovalDef.survey', 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='approvers', full_name='monorail.v3.ApprovalDef.approvers', 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=b'\372A\024\n\022api.crbug.com/User', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='admins', full_name='monorail.v3.ApprovalDef.admins', index=5,
+ number=6, 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=b'\372A\024\n\022api.crbug.com/User', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=b'\352AN\n\031api.crbug.com/ApprovalDef\0221projects/{project}/approvalDefs/{approval_def_id}',
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=3386,
+ serialized_end=3643,
+)
+
+
+_PROJECTSAVEDQUERY = _descriptor.Descriptor(
+ name='ProjectSavedQuery',
+ full_name='monorail.v3.ProjectSavedQuery',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.ProjectSavedQuery.name', 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.v3.ProjectSavedQuery.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='query', full_name='monorail.v3.ProjectSavedQuery.query', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=b'\352AS\n\037api.crbug.com/ProjectSavedQuery\0220projects/{project}/savedQueries/{saved_query_id}',
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=3646,
+ serialized_end=3804,
+)
+
+
+_ISSUETEMPLATE = _descriptor.Descriptor(
+ name='IssueTemplate',
+ full_name='monorail.v3.IssueTemplate',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.IssueTemplate.name', 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.v3.IssueTemplate.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=b'\340A\005', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='issue', full_name='monorail.v3.IssueTemplate.issue', index=2,
+ number=3, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ 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='approval_values', full_name='monorail.v3.IssueTemplate.approval_values', index=3,
+ number=9, 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='summary_must_be_edited', full_name='monorail.v3.IssueTemplate.summary_must_be_edited', index=4,
+ number=4, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ 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='template_privacy', full_name='monorail.v3.IssueTemplate.template_privacy', index=5,
+ number=5, type=14, cpp_type=8, 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='default_owner', full_name='monorail.v3.IssueTemplate.default_owner', index=6,
+ number=6, type=14, cpp_type=8, 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='component_required', full_name='monorail.v3.IssueTemplate.component_required', index=7,
+ number=7, type=8, cpp_type=7, label=1,
+ has_default_value=False, default_value=False,
+ 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='admins', full_name='monorail.v3.IssueTemplate.admins', 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=b'\372A\024\n\022api.crbug.com/User', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ _ISSUETEMPLATE_TEMPLATEPRIVACY,
+ _ISSUETEMPLATE_DEFAULTOWNER,
+ ],
+ serialized_options=b'\352AI\n\033api.crbug.com/IssueTemplate\022*projects/{project}/templates/{template_id}',
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=3807,
+ serialized_end=4423,
+)
+
+
+_PROJECTCONFIG_GRIDVIEWCONFIG = _descriptor.Descriptor(
+ name='GridViewConfig',
+ full_name='monorail.v3.ProjectConfig.GridViewConfig',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='default_x_attr', full_name='monorail.v3.ProjectConfig.GridViewConfig.default_x_attr', 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='default_y_attr', full_name='monorail.v3.ProjectConfig.GridViewConfig.default_y_attr', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=4861,
+ serialized_end=4925,
+)
+
+_PROJECTCONFIG = _descriptor.Descriptor(
+ name='ProjectConfig',
+ full_name='monorail.v3.ProjectConfig',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.ProjectConfig.name', 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='exclusive_label_prefixes', full_name='monorail.v3.ProjectConfig.exclusive_label_prefixes', index=1,
+ number=2, 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='member_default_query', full_name='monorail.v3.ProjectConfig.member_default_query', 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='default_sort', full_name='monorail.v3.ProjectConfig.default_sort', 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='default_columns', full_name='monorail.v3.ProjectConfig.default_columns', index=4,
+ 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='project_grid_config', full_name='monorail.v3.ProjectConfig.project_grid_config', index=5,
+ number=6, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ 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='member_default_template', full_name='monorail.v3.ProjectConfig.member_default_template', index=6,
+ number=7, 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=b'\372A\030\n\026api.crbug.com/Template', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='non_members_default_template', full_name='monorail.v3.ProjectConfig.non_members_default_template', index=7,
+ number=8, 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=b'\372A\030\n\026api.crbug.com/Template', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='revision_url_format', full_name='monorail.v3.ProjectConfig.revision_url_format', index=8,
+ number=9, 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='custom_issue_entry_url', full_name='monorail.v3.ProjectConfig.custom_issue_entry_url', index=9,
+ number=10, 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=[_PROJECTCONFIG_GRIDVIEWCONFIG, ],
+ enum_types=[
+ ],
+ serialized_options=b'\352A8\n\033api.crbug.com/ProjectConfig\022\031projects/{project}/config',
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=4426,
+ serialized_end=4986,
+)
+
+
+_PROJECTMEMBER = _descriptor.Descriptor(
+ name='ProjectMember',
+ full_name='monorail.v3.ProjectMember',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.ProjectMember.name', 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.v3.ProjectMember.role', index=1,
+ number=2, type=14, cpp_type=8, 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='standard_perms', full_name='monorail.v3.ProjectMember.standard_perms', index=2,
+ number=3, type=14, cpp_type=8, 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='custom_perms', full_name='monorail.v3.ProjectMember.custom_perms', index=3,
+ number=4, 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='notes', full_name='monorail.v3.ProjectMember.notes', index=4,
+ number=5, 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='include_in_autocomplete', full_name='monorail.v3.ProjectMember.include_in_autocomplete', index=5,
+ number=6, type=14, cpp_type=8, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ _PROJECTMEMBER_PROJECTROLE,
+ _PROJECTMEMBER_AUTOCOMPLETEVISIBILITY,
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=4989,
+ serialized_end=5420,
+)
+
+_STATUSDEF.fields_by_name['type'].enum_type = _STATUSDEF_STATUSDEFTYPE
+_STATUSDEF.fields_by_name['state'].enum_type = _STATUSDEF_STATUSDEFSTATE
+_STATUSDEF_STATUSDEFTYPE.containing_type = _STATUSDEF
+_STATUSDEF_STATUSDEFSTATE.containing_type = _STATUSDEF
+_LABELDEF.fields_by_name['state'].enum_type = _LABELDEF_LABELDEFSTATE
+_LABELDEF_LABELDEFSTATE.containing_type = _LABELDEF
+_FIELDDEF_ENUMTYPESETTINGS_CHOICE.containing_type = _FIELDDEF_ENUMTYPESETTINGS
+_FIELDDEF_ENUMTYPESETTINGS.fields_by_name['choices'].message_type = _FIELDDEF_ENUMTYPESETTINGS_CHOICE
+_FIELDDEF_ENUMTYPESETTINGS.containing_type = _FIELDDEF
+_FIELDDEF_INTTYPESETTINGS.containing_type = _FIELDDEF
+_FIELDDEF_STRTYPESETTINGS.containing_type = _FIELDDEF
+_FIELDDEF_USERTYPESETTINGS.fields_by_name['notify_triggers'].enum_type = _FIELDDEF_USERTYPESETTINGS_NOTIFYTRIGGERS
+_FIELDDEF_USERTYPESETTINGS.fields_by_name['role_requirements'].enum_type = _FIELDDEF_USERTYPESETTINGS_ROLEREQUIREMENTS
+_FIELDDEF_USERTYPESETTINGS.containing_type = _FIELDDEF
+_FIELDDEF_USERTYPESETTINGS_NOTIFYTRIGGERS.containing_type = _FIELDDEF_USERTYPESETTINGS
+_FIELDDEF_USERTYPESETTINGS_ROLEREQUIREMENTS.containing_type = _FIELDDEF_USERTYPESETTINGS
+_FIELDDEF_DATETYPESETTINGS.fields_by_name['date_action'].enum_type = _FIELDDEF_DATETYPESETTINGS_DATEACTION
+_FIELDDEF_DATETYPESETTINGS.containing_type = _FIELDDEF
+_FIELDDEF_DATETYPESETTINGS_DATEACTION.containing_type = _FIELDDEF_DATETYPESETTINGS
+_FIELDDEF.fields_by_name['type'].enum_type = _FIELDDEF_TYPE
+_FIELDDEF.fields_by_name['traits'].enum_type = _FIELDDEF_TRAITS
+_FIELDDEF.fields_by_name['enum_settings'].message_type = _FIELDDEF_ENUMTYPESETTINGS
+_FIELDDEF.fields_by_name['int_settings'].message_type = _FIELDDEF_INTTYPESETTINGS
+_FIELDDEF.fields_by_name['str_settings'].message_type = _FIELDDEF_STRTYPESETTINGS
+_FIELDDEF.fields_by_name['user_settings'].message_type = _FIELDDEF_USERTYPESETTINGS
+_FIELDDEF.fields_by_name['date_settings'].message_type = _FIELDDEF_DATETYPESETTINGS
+_FIELDDEF_TYPE.containing_type = _FIELDDEF
+_FIELDDEF_TRAITS.containing_type = _FIELDDEF
+_COMPONENTDEF.fields_by_name['state'].enum_type = _COMPONENTDEF_COMPONENTDEFSTATE
+_COMPONENTDEF.fields_by_name['create_time'].message_type = google_dot_protobuf_dot_timestamp__pb2._TIMESTAMP
+_COMPONENTDEF.fields_by_name['modify_time'].message_type = google_dot_protobuf_dot_timestamp__pb2._TIMESTAMP
+_COMPONENTDEF_COMPONENTDEFSTATE.containing_type = _COMPONENTDEF
+_ISSUETEMPLATE.fields_by_name['issue'].message_type = api_dot_v3_dot_api__proto_dot_issue__objects__pb2._ISSUE
+_ISSUETEMPLATE.fields_by_name['approval_values'].message_type = api_dot_v3_dot_api__proto_dot_issue__objects__pb2._APPROVALVALUE
+_ISSUETEMPLATE.fields_by_name['template_privacy'].enum_type = _ISSUETEMPLATE_TEMPLATEPRIVACY
+_ISSUETEMPLATE.fields_by_name['default_owner'].enum_type = _ISSUETEMPLATE_DEFAULTOWNER
+_ISSUETEMPLATE_TEMPLATEPRIVACY.containing_type = _ISSUETEMPLATE
+_ISSUETEMPLATE_DEFAULTOWNER.containing_type = _ISSUETEMPLATE
+_PROJECTCONFIG_GRIDVIEWCONFIG.containing_type = _PROJECTCONFIG
+_PROJECTCONFIG.fields_by_name['default_columns'].message_type = api_dot_v3_dot_api__proto_dot_issue__objects__pb2._ISSUESLISTCOLUMN
+_PROJECTCONFIG.fields_by_name['project_grid_config'].message_type = _PROJECTCONFIG_GRIDVIEWCONFIG
+_PROJECTMEMBER.fields_by_name['role'].enum_type = _PROJECTMEMBER_PROJECTROLE
+_PROJECTMEMBER.fields_by_name['standard_perms'].enum_type = api_dot_v3_dot_api__proto_dot_permission__objects__pb2._PERMISSION
+_PROJECTMEMBER.fields_by_name['include_in_autocomplete'].enum_type = _PROJECTMEMBER_AUTOCOMPLETEVISIBILITY
+_PROJECTMEMBER_PROJECTROLE.containing_type = _PROJECTMEMBER
+_PROJECTMEMBER_AUTOCOMPLETEVISIBILITY.containing_type = _PROJECTMEMBER
+DESCRIPTOR.message_types_by_name['Project'] = _PROJECT
+DESCRIPTOR.message_types_by_name['StatusDef'] = _STATUSDEF
+DESCRIPTOR.message_types_by_name['LabelDef'] = _LABELDEF
+DESCRIPTOR.message_types_by_name['FieldDef'] = _FIELDDEF
+DESCRIPTOR.message_types_by_name['ComponentDef'] = _COMPONENTDEF
+DESCRIPTOR.message_types_by_name['ApprovalDef'] = _APPROVALDEF
+DESCRIPTOR.message_types_by_name['ProjectSavedQuery'] = _PROJECTSAVEDQUERY
+DESCRIPTOR.message_types_by_name['IssueTemplate'] = _ISSUETEMPLATE
+DESCRIPTOR.message_types_by_name['ProjectConfig'] = _PROJECTCONFIG
+DESCRIPTOR.message_types_by_name['ProjectMember'] = _PROJECTMEMBER
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+Project = _reflection.GeneratedProtocolMessageType('Project', (_message.Message,), {
+ 'DESCRIPTOR' : _PROJECT,
+ '__module__' : 'api.v3.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.Project)
+ })
+_sym_db.RegisterMessage(Project)
+
+StatusDef = _reflection.GeneratedProtocolMessageType('StatusDef', (_message.Message,), {
+ 'DESCRIPTOR' : _STATUSDEF,
+ '__module__' : 'api.v3.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.StatusDef)
+ })
+_sym_db.RegisterMessage(StatusDef)
+
+LabelDef = _reflection.GeneratedProtocolMessageType('LabelDef', (_message.Message,), {
+ 'DESCRIPTOR' : _LABELDEF,
+ '__module__' : 'api.v3.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.LabelDef)
+ })
+_sym_db.RegisterMessage(LabelDef)
+
+FieldDef = _reflection.GeneratedProtocolMessageType('FieldDef', (_message.Message,), {
+
+ 'EnumTypeSettings' : _reflection.GeneratedProtocolMessageType('EnumTypeSettings', (_message.Message,), {
+
+ 'Choice' : _reflection.GeneratedProtocolMessageType('Choice', (_message.Message,), {
+ 'DESCRIPTOR' : _FIELDDEF_ENUMTYPESETTINGS_CHOICE,
+ '__module__' : 'api.v3.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.FieldDef.EnumTypeSettings.Choice)
+ })
+ ,
+ 'DESCRIPTOR' : _FIELDDEF_ENUMTYPESETTINGS,
+ '__module__' : 'api.v3.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.FieldDef.EnumTypeSettings)
+ })
+ ,
+
+ 'IntTypeSettings' : _reflection.GeneratedProtocolMessageType('IntTypeSettings', (_message.Message,), {
+ 'DESCRIPTOR' : _FIELDDEF_INTTYPESETTINGS,
+ '__module__' : 'api.v3.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.FieldDef.IntTypeSettings)
+ })
+ ,
+
+ 'StrTypeSettings' : _reflection.GeneratedProtocolMessageType('StrTypeSettings', (_message.Message,), {
+ 'DESCRIPTOR' : _FIELDDEF_STRTYPESETTINGS,
+ '__module__' : 'api.v3.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.FieldDef.StrTypeSettings)
+ })
+ ,
+
+ 'UserTypeSettings' : _reflection.GeneratedProtocolMessageType('UserTypeSettings', (_message.Message,), {
+ 'DESCRIPTOR' : _FIELDDEF_USERTYPESETTINGS,
+ '__module__' : 'api.v3.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.FieldDef.UserTypeSettings)
+ })
+ ,
+
+ 'DateTypeSettings' : _reflection.GeneratedProtocolMessageType('DateTypeSettings', (_message.Message,), {
+ 'DESCRIPTOR' : _FIELDDEF_DATETYPESETTINGS,
+ '__module__' : 'api.v3.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.FieldDef.DateTypeSettings)
+ })
+ ,
+ 'DESCRIPTOR' : _FIELDDEF,
+ '__module__' : 'api.v3.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.FieldDef)
+ })
+_sym_db.RegisterMessage(FieldDef)
+_sym_db.RegisterMessage(FieldDef.EnumTypeSettings)
+_sym_db.RegisterMessage(FieldDef.EnumTypeSettings.Choice)
+_sym_db.RegisterMessage(FieldDef.IntTypeSettings)
+_sym_db.RegisterMessage(FieldDef.StrTypeSettings)
+_sym_db.RegisterMessage(FieldDef.UserTypeSettings)
+_sym_db.RegisterMessage(FieldDef.DateTypeSettings)
+
+ComponentDef = _reflection.GeneratedProtocolMessageType('ComponentDef', (_message.Message,), {
+ 'DESCRIPTOR' : _COMPONENTDEF,
+ '__module__' : 'api.v3.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ComponentDef)
+ })
+_sym_db.RegisterMessage(ComponentDef)
+
+ApprovalDef = _reflection.GeneratedProtocolMessageType('ApprovalDef', (_message.Message,), {
+ 'DESCRIPTOR' : _APPROVALDEF,
+ '__module__' : 'api.v3.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ApprovalDef)
+ })
+_sym_db.RegisterMessage(ApprovalDef)
+
+ProjectSavedQuery = _reflection.GeneratedProtocolMessageType('ProjectSavedQuery', (_message.Message,), {
+ 'DESCRIPTOR' : _PROJECTSAVEDQUERY,
+ '__module__' : 'api.v3.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ProjectSavedQuery)
+ })
+_sym_db.RegisterMessage(ProjectSavedQuery)
+
+IssueTemplate = _reflection.GeneratedProtocolMessageType('IssueTemplate', (_message.Message,), {
+ 'DESCRIPTOR' : _ISSUETEMPLATE,
+ '__module__' : 'api.v3.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.IssueTemplate)
+ })
+_sym_db.RegisterMessage(IssueTemplate)
+
+ProjectConfig = _reflection.GeneratedProtocolMessageType('ProjectConfig', (_message.Message,), {
+
+ 'GridViewConfig' : _reflection.GeneratedProtocolMessageType('GridViewConfig', (_message.Message,), {
+ 'DESCRIPTOR' : _PROJECTCONFIG_GRIDVIEWCONFIG,
+ '__module__' : 'api.v3.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ProjectConfig.GridViewConfig)
+ })
+ ,
+ 'DESCRIPTOR' : _PROJECTCONFIG,
+ '__module__' : 'api.v3.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ProjectConfig)
+ })
+_sym_db.RegisterMessage(ProjectConfig)
+_sym_db.RegisterMessage(ProjectConfig.GridViewConfig)
+
+ProjectMember = _reflection.GeneratedProtocolMessageType('ProjectMember', (_message.Message,), {
+ 'DESCRIPTOR' : _PROJECTMEMBER,
+ '__module__' : 'api.v3.api_proto.project_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ProjectMember)
+ })
+_sym_db.RegisterMessage(ProjectMember)
+
+
+DESCRIPTOR._options = None
+_PROJECT.fields_by_name['display_name']._options = None
+_PROJECT._options = None
+_STATUSDEF._options = None
+_LABELDEF._options = None
+_FIELDDEF.fields_by_name['display_name']._options = None
+_FIELDDEF.fields_by_name['type']._options = None
+_FIELDDEF.fields_by_name['admins']._options = None
+_FIELDDEF.fields_by_name['approval_parent']._options = None
+_FIELDDEF.fields_by_name['editors']._options = None
+_FIELDDEF._options = None
+_COMPONENTDEF.fields_by_name['admins']._options = None
+_COMPONENTDEF.fields_by_name['ccs']._options = None
+_COMPONENTDEF.fields_by_name['creator']._options = None
+_COMPONENTDEF.fields_by_name['modifier']._options = None
+_COMPONENTDEF.fields_by_name['create_time']._options = None
+_COMPONENTDEF.fields_by_name['modify_time']._options = None
+_COMPONENTDEF._options = None
+_APPROVALDEF.fields_by_name['display_name']._options = None
+_APPROVALDEF.fields_by_name['approvers']._options = None
+_APPROVALDEF.fields_by_name['admins']._options = None
+_APPROVALDEF._options = None
+_PROJECTSAVEDQUERY._options = None
+_ISSUETEMPLATE.fields_by_name['display_name']._options = None
+_ISSUETEMPLATE.fields_by_name['admins']._options = None
+_ISSUETEMPLATE._options = None
+_PROJECTCONFIG.fields_by_name['member_default_template']._options = None
+_PROJECTCONFIG.fields_by_name['non_members_default_template']._options = None
+_PROJECTCONFIG._options = None
+# @@protoc_insertion_point(module_scope)
diff --git a/api/v3/api_proto/projects.proto b/api/v3/api_proto/projects.proto
new file mode 100644
index 0000000..d902067
--- /dev/null
+++ b/api/v3/api_proto/projects.proto
@@ -0,0 +1,178 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+syntax = "proto3";
+
+package monorail.v3;
+
+option go_package = "api/v3/api_proto";
+
+import "google/protobuf/empty.proto";
+import "google/api/field_behavior.proto";
+import "google/api/resource.proto";
+import "api/v3/api_proto/project_objects.proto";
+
+// ***ONLY CALL rpcs WITH `status: {ALPHA|STABLE}`***
+// rpcs without `status` are not ready.
+
+// Projects service includes all methods needed for managing Projects.
+service Projects {
+ // status: NOT READY
+ // Creates a new FieldDef (custom field).
+ //
+ // Raises:
+ // NOT_FOUND if some given users do not exist.
+ // ALREADY_EXISTS if a field with the same name owned by the project
+ // already exists.
+ // INVALID_INPUT if there was a problem with the input.
+ // PERMISSION_DENIED if the user cannot edit the project.
+ rpc CreateFieldDef (CreateFieldDefRequest) returns (FieldDef) {}
+
+ // status: ALPHA
+ // Creates a new ComponentDef.
+ //
+ // Raises:
+ // INVALID_INPUT if the request is invalid.
+ // ALREADY_EXISTS if the component already exists.
+ // PERMISSION_DENIED if the user is not allowed to create a/this component.
+ // NOT_FOUND if the parent project or a component cc or admin is not found.
+ rpc CreateComponentDef (CreateComponentDefRequest) returns (ComponentDef) {}
+
+ // status: ALPHA
+ // Deletes a ComponentDef.
+ //
+ // Raises:
+ // INVALID_INPUT if the request is invalid.
+ // PERMISSION_DENIED if the user is not allowed to delete a/this component.
+ // NOT_FOUND if the component or project is not found.
+ rpc DeleteComponentDef (DeleteComponentDefRequest) returns (google.protobuf.Empty) {}
+
+ // status: NOT READY
+ // Returns all templates for specified project.
+ //
+ // Raises:
+ // NOT_FOUND if the requested parent project is not found.
+ // INVALID_ARGUMENT if the given `parent` is not valid.
+ rpc ListIssueTemplates (ListIssueTemplatesRequest) returns (ListIssueTemplatesResponse) {}
+
+ // status: ALPHA
+ // Returns all field defs for specified project.
+ //
+ // Raises:
+ // NOT_FOUND if the request arent project is not found.
+ // INVALID_ARGUMENT if the given `parent` is not valid.
+ rpc ListComponentDefs (ListComponentDefsRequest) returns (ListComponentDefsResponse) {}
+
+ // status: NOT READY
+ // Returns all projects hosted on Monorail.
+ rpc ListProjects (ListProjectsRequest) returns (ListProjectsResponse) {}
+}
+
+// Request message for CreateFieldDef method.
+// Next available tag: 3
+message CreateFieldDefRequest {
+ // The project resource where this field will be created.
+ string parent = 1 [
+ (google.api.field_behavior) = REQUIRED,
+ (google.api.resource_reference) = {type: "api.crbug.com/Project" }];
+ // The field to create.
+ // It must have a display_name and a type with its corresponding settings.
+ FieldDef fielddef = 2 [ (google.api.field_behavior) = REQUIRED ];
+}
+
+// Request message for CreateComponentDef method.
+// Next available tag: 3
+message CreateComponentDefRequest {
+ // The project resource where this component will be created.
+ string parent = 1 [
+ (google.api.field_behavior) = REQUIRED,
+ (google.api.resource_reference) = {type: "api.crbug.com/Project" }];
+ // The component to create.
+ ComponentDef component_def = 2 [ (google.api.field_behavior) = REQUIRED ];
+}
+
+// Request message for DeleteComponentDef method.
+// Next available tag: 2
+message DeleteComponentDefRequest {
+ // The component to delete.
+ string name = 1 [
+ (google.api.field_behavior) = REQUIRED,
+ (google.api.resource_reference) = {type: "api.crbug.com/ComponentDef"}];
+}
+
+// Request message for ListIssueTemplates
+// Next available tag: 4
+message ListIssueTemplatesRequest {
+ // The name of the project these templates belong to.
+ string parent = 1 [
+ (google.api.resource_reference) = {type: "api.crbug.com/Project"},
+ (google.api.field_behavior) = REQUIRED ];
+ // The maximum number of items to return. The service may return fewer than
+ // this value.
+ int32 page_size = 2;
+ // A page token, received from a previous `ListIssueTemplates` call.
+ // Provide this to retrieve the subsequent page.
+ // When paginating, all other parameters provided to
+ // `ListIssueTemplatesRequest` must match the call that provided the token.
+ string page_token = 3;
+}
+
+// Response message for ListIssueTemplates
+// Next available tag: 3
+message ListIssueTemplatesResponse {
+ // Templates matching the given request.
+ repeated IssueTemplate templates = 1;
+ // A token, which can be sent as `page_token` to retrieve the next page.
+ // If this field is omitted, there are no subsequent pages.
+ string next_page_token = 2;
+}
+
+// Request message for ListComponentDefs
+// Next available tag: 4
+message ListComponentDefsRequest {
+ // The name of the parent project.
+ string parent = 1 [
+ (google.api.resource_reference) = {type: "api.crbug.com/Project"},
+ (google.api.field_behavior) = REQUIRED ];
+ // The maximum number of items to return. The service may return fewer than
+ // this value.
+ int32 page_size = 2;
+ // A page token, received from a previous `ListComponentDefs` call.
+ // Provide this to retrieve the subsequent page.
+ // When paginating, all other parameters provided to
+ // `ListComponentDefsRequest` must match the call that provided the token.
+ string page_token = 3;
+}
+
+// Response message for ListComponentDefs
+// Next available tag: 3
+message ListComponentDefsResponse {
+ // Component defs matching the given request.
+ repeated ComponentDef component_defs = 1;
+ // A token which can be sent as `page_token` to retrieve the next page.
+ // If this field is omitted, there are no subsequent pages.
+ string next_page_token = 2;
+}
+
+// Request message for ListProjects
+// Next available tag: 3
+message ListProjectsRequest {
+ // The maximum number of items to return. The service may return fewer than
+ // this value.
+ int32 page_size = 1;
+ // A page token, received from a previous `ListProjects` call.
+ // Provide this to retrieve the subsequent page.
+ string page_token = 2;
+}
+
+// Response message for ListProjects
+// Next available tag: 3
+message ListProjectsResponse {
+ // Projects matching the given request.
+ repeated Project projects = 1;
+ // A token, which can be sent as `page_token` to retrieve the next page.
+ // If this field is omitted, there are no subsequent pages.
+ string next_page_token = 2;
+}
diff --git a/api/v3/api_proto/projects_pb2.py b/api/v3/api_proto/projects_pb2.py
new file mode 100644
index 0000000..0478142
--- /dev/null
+++ b/api/v3/api_proto/projects_pb2.py
@@ -0,0 +1,554 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/v3/api_proto/projects.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()
+
+
+from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
+from google.api import field_behavior_pb2 as google_dot_api_dot_field__behavior__pb2
+from google.api import resource_pb2 as google_dot_api_dot_resource__pb2
+from api.v3.api_proto import project_objects_pb2 as api_dot_v3_dot_api__proto_dot_project__objects__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='api/v3/api_proto/projects.proto',
+ package='monorail.v3',
+ syntax='proto3',
+ serialized_options=b'Z\020api/v3/api_proto',
+ create_key=_descriptor._internal_create_key,
+ serialized_pb=b'\n\x1f\x61pi/v3/api_proto/projects.proto\x12\x0bmonorail.v3\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a&api/v3/api_proto/project_objects.proto\"t\n\x15\x43reateFieldDefRequest\x12-\n\x06parent\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\n\x15\x61pi.crbug.com/Project\x12,\n\x08\x66ielddef\x18\x02 \x01(\x0b\x32\x15.monorail.v3.FieldDefB\x03\xe0\x41\x02\"\x81\x01\n\x19\x43reateComponentDefRequest\x12-\n\x06parent\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\n\x15\x61pi.crbug.com/Project\x12\x35\n\rcomponent_def\x18\x02 \x01(\x0b\x32\x19.monorail.v3.ComponentDefB\x03\xe0\x41\x02\"M\n\x19\x44\x65leteComponentDefRequest\x12\x30\n\x04name\x18\x01 \x01(\tB\"\xe0\x41\x02\xfa\x41\x1c\n\x1a\x61pi.crbug.com/ComponentDef\"q\n\x19ListIssueTemplatesRequest\x12-\n\x06parent\x18\x01 \x01(\tB\x1d\xfa\x41\x17\n\x15\x61pi.crbug.com/Project\xe0\x41\x02\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x12\n\npage_token\x18\x03 \x01(\t\"d\n\x1aListIssueTemplatesResponse\x12-\n\ttemplates\x18\x01 \x03(\x0b\x32\x1a.monorail.v3.IssueTemplate\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\"p\n\x18ListComponentDefsRequest\x12-\n\x06parent\x18\x01 \x01(\tB\x1d\xfa\x41\x17\n\x15\x61pi.crbug.com/Project\xe0\x41\x02\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x12\n\npage_token\x18\x03 \x01(\t\"g\n\x19ListComponentDefsResponse\x12\x31\n\x0e\x63omponent_defs\x18\x01 \x03(\x0b\x32\x19.monorail.v3.ComponentDef\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\"<\n\x13ListProjectsRequest\x12\x11\n\tpage_size\x18\x01 \x01(\x05\x12\x12\n\npage_token\x18\x02 \x01(\t\"W\n\x14ListProjectsResponse\x12&\n\x08projects\x18\x01 \x03(\x0b\x32\x14.monorail.v3.Project\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t2\xb2\x04\n\x08Projects\x12M\n\x0e\x43reateFieldDef\x12\".monorail.v3.CreateFieldDefRequest\x1a\x15.monorail.v3.FieldDef\"\x00\x12Y\n\x12\x43reateComponentDef\x12&.monorail.v3.CreateComponentDefRequest\x1a\x19.monorail.v3.ComponentDef\"\x00\x12V\n\x12\x44\x65leteComponentDef\x12&.monorail.v3.DeleteComponentDefRequest\x1a\x16.google.protobuf.Empty\"\x00\x12g\n\x12ListIssueTemplates\x12&.monorail.v3.ListIssueTemplatesRequest\x1a\'.monorail.v3.ListIssueTemplatesResponse\"\x00\x12\x64\n\x11ListComponentDefs\x12%.monorail.v3.ListComponentDefsRequest\x1a&.monorail.v3.ListComponentDefsResponse\"\x00\x12U\n\x0cListProjects\x12 .monorail.v3.ListProjectsRequest\x1a!.monorail.v3.ListProjectsResponse\"\x00\x42\x12Z\x10\x61pi/v3/api_protob\x06proto3'
+ ,
+ dependencies=[google_dot_protobuf_dot_empty__pb2.DESCRIPTOR,google_dot_api_dot_field__behavior__pb2.DESCRIPTOR,google_dot_api_dot_resource__pb2.DESCRIPTOR,api_dot_v3_dot_api__proto_dot_project__objects__pb2.DESCRIPTOR,])
+
+
+
+
+_CREATEFIELDDEFREQUEST = _descriptor.Descriptor(
+ name='CreateFieldDefRequest',
+ full_name='monorail.v3.CreateFieldDefRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='parent', full_name='monorail.v3.CreateFieldDefRequest.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=b'\340A\002\372A\027\n\025api.crbug.com/Project', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='fielddef', full_name='monorail.v3.CreateFieldDefRequest.fielddef', index=1,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\002', 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=177,
+ serialized_end=293,
+)
+
+
+_CREATECOMPONENTDEFREQUEST = _descriptor.Descriptor(
+ name='CreateComponentDefRequest',
+ full_name='monorail.v3.CreateComponentDefRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='parent', full_name='monorail.v3.CreateComponentDefRequest.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=b'\340A\002\372A\027\n\025api.crbug.com/Project', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='component_def', full_name='monorail.v3.CreateComponentDefRequest.component_def', index=1,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\002', 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=296,
+ serialized_end=425,
+)
+
+
+_DELETECOMPONENTDEFREQUEST = _descriptor.Descriptor(
+ name='DeleteComponentDefRequest',
+ full_name='monorail.v3.DeleteComponentDefRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.DeleteComponentDefRequest.name', 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=b'\340A\002\372A\034\n\032api.crbug.com/ComponentDef', 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=427,
+ serialized_end=504,
+)
+
+
+_LISTISSUETEMPLATESREQUEST = _descriptor.Descriptor(
+ name='ListIssueTemplatesRequest',
+ full_name='monorail.v3.ListIssueTemplatesRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='parent', full_name='monorail.v3.ListIssueTemplatesRequest.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=b'\372A\027\n\025api.crbug.com/Project\340A\002', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='page_size', full_name='monorail.v3.ListIssueTemplatesRequest.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='page_token', full_name='monorail.v3.ListIssueTemplatesRequest.page_token', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=506,
+ serialized_end=619,
+)
+
+
+_LISTISSUETEMPLATESRESPONSE = _descriptor.Descriptor(
+ name='ListIssueTemplatesResponse',
+ full_name='monorail.v3.ListIssueTemplatesResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='templates', full_name='monorail.v3.ListIssueTemplatesResponse.templates', 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),
+ _descriptor.FieldDescriptor(
+ name='next_page_token', full_name='monorail.v3.ListIssueTemplatesResponse.next_page_token', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=621,
+ serialized_end=721,
+)
+
+
+_LISTCOMPONENTDEFSREQUEST = _descriptor.Descriptor(
+ name='ListComponentDefsRequest',
+ full_name='monorail.v3.ListComponentDefsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='parent', full_name='monorail.v3.ListComponentDefsRequest.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=b'\372A\027\n\025api.crbug.com/Project\340A\002', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='page_size', full_name='monorail.v3.ListComponentDefsRequest.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='page_token', full_name='monorail.v3.ListComponentDefsRequest.page_token', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=723,
+ serialized_end=835,
+)
+
+
+_LISTCOMPONENTDEFSRESPONSE = _descriptor.Descriptor(
+ name='ListComponentDefsResponse',
+ full_name='monorail.v3.ListComponentDefsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='component_defs', full_name='monorail.v3.ListComponentDefsResponse.component_defs', 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),
+ _descriptor.FieldDescriptor(
+ name='next_page_token', full_name='monorail.v3.ListComponentDefsResponse.next_page_token', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=837,
+ serialized_end=940,
+)
+
+
+_LISTPROJECTSREQUEST = _descriptor.Descriptor(
+ name='ListProjectsRequest',
+ full_name='monorail.v3.ListProjectsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='page_size', full_name='monorail.v3.ListProjectsRequest.page_size', 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='page_token', full_name='monorail.v3.ListProjectsRequest.page_token', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=942,
+ serialized_end=1002,
+)
+
+
+_LISTPROJECTSRESPONSE = _descriptor.Descriptor(
+ name='ListProjectsResponse',
+ full_name='monorail.v3.ListProjectsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='projects', full_name='monorail.v3.ListProjectsResponse.projects', 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),
+ _descriptor.FieldDescriptor(
+ name='next_page_token', full_name='monorail.v3.ListProjectsResponse.next_page_token', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1004,
+ serialized_end=1091,
+)
+
+_CREATEFIELDDEFREQUEST.fields_by_name['fielddef'].message_type = api_dot_v3_dot_api__proto_dot_project__objects__pb2._FIELDDEF
+_CREATECOMPONENTDEFREQUEST.fields_by_name['component_def'].message_type = api_dot_v3_dot_api__proto_dot_project__objects__pb2._COMPONENTDEF
+_LISTISSUETEMPLATESRESPONSE.fields_by_name['templates'].message_type = api_dot_v3_dot_api__proto_dot_project__objects__pb2._ISSUETEMPLATE
+_LISTCOMPONENTDEFSRESPONSE.fields_by_name['component_defs'].message_type = api_dot_v3_dot_api__proto_dot_project__objects__pb2._COMPONENTDEF
+_LISTPROJECTSRESPONSE.fields_by_name['projects'].message_type = api_dot_v3_dot_api__proto_dot_project__objects__pb2._PROJECT
+DESCRIPTOR.message_types_by_name['CreateFieldDefRequest'] = _CREATEFIELDDEFREQUEST
+DESCRIPTOR.message_types_by_name['CreateComponentDefRequest'] = _CREATECOMPONENTDEFREQUEST
+DESCRIPTOR.message_types_by_name['DeleteComponentDefRequest'] = _DELETECOMPONENTDEFREQUEST
+DESCRIPTOR.message_types_by_name['ListIssueTemplatesRequest'] = _LISTISSUETEMPLATESREQUEST
+DESCRIPTOR.message_types_by_name['ListIssueTemplatesResponse'] = _LISTISSUETEMPLATESRESPONSE
+DESCRIPTOR.message_types_by_name['ListComponentDefsRequest'] = _LISTCOMPONENTDEFSREQUEST
+DESCRIPTOR.message_types_by_name['ListComponentDefsResponse'] = _LISTCOMPONENTDEFSRESPONSE
+DESCRIPTOR.message_types_by_name['ListProjectsRequest'] = _LISTPROJECTSREQUEST
+DESCRIPTOR.message_types_by_name['ListProjectsResponse'] = _LISTPROJECTSRESPONSE
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+CreateFieldDefRequest = _reflection.GeneratedProtocolMessageType('CreateFieldDefRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _CREATEFIELDDEFREQUEST,
+ '__module__' : 'api.v3.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.CreateFieldDefRequest)
+ })
+_sym_db.RegisterMessage(CreateFieldDefRequest)
+
+CreateComponentDefRequest = _reflection.GeneratedProtocolMessageType('CreateComponentDefRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _CREATECOMPONENTDEFREQUEST,
+ '__module__' : 'api.v3.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.CreateComponentDefRequest)
+ })
+_sym_db.RegisterMessage(CreateComponentDefRequest)
+
+DeleteComponentDefRequest = _reflection.GeneratedProtocolMessageType('DeleteComponentDefRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _DELETECOMPONENTDEFREQUEST,
+ '__module__' : 'api.v3.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.DeleteComponentDefRequest)
+ })
+_sym_db.RegisterMessage(DeleteComponentDefRequest)
+
+ListIssueTemplatesRequest = _reflection.GeneratedProtocolMessageType('ListIssueTemplatesRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _LISTISSUETEMPLATESREQUEST,
+ '__module__' : 'api.v3.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ListIssueTemplatesRequest)
+ })
+_sym_db.RegisterMessage(ListIssueTemplatesRequest)
+
+ListIssueTemplatesResponse = _reflection.GeneratedProtocolMessageType('ListIssueTemplatesResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _LISTISSUETEMPLATESRESPONSE,
+ '__module__' : 'api.v3.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ListIssueTemplatesResponse)
+ })
+_sym_db.RegisterMessage(ListIssueTemplatesResponse)
+
+ListComponentDefsRequest = _reflection.GeneratedProtocolMessageType('ListComponentDefsRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _LISTCOMPONENTDEFSREQUEST,
+ '__module__' : 'api.v3.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ListComponentDefsRequest)
+ })
+_sym_db.RegisterMessage(ListComponentDefsRequest)
+
+ListComponentDefsResponse = _reflection.GeneratedProtocolMessageType('ListComponentDefsResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _LISTCOMPONENTDEFSRESPONSE,
+ '__module__' : 'api.v3.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ListComponentDefsResponse)
+ })
+_sym_db.RegisterMessage(ListComponentDefsResponse)
+
+ListProjectsRequest = _reflection.GeneratedProtocolMessageType('ListProjectsRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _LISTPROJECTSREQUEST,
+ '__module__' : 'api.v3.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ListProjectsRequest)
+ })
+_sym_db.RegisterMessage(ListProjectsRequest)
+
+ListProjectsResponse = _reflection.GeneratedProtocolMessageType('ListProjectsResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _LISTPROJECTSRESPONSE,
+ '__module__' : 'api.v3.api_proto.projects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ListProjectsResponse)
+ })
+_sym_db.RegisterMessage(ListProjectsResponse)
+
+
+DESCRIPTOR._options = None
+_CREATEFIELDDEFREQUEST.fields_by_name['parent']._options = None
+_CREATEFIELDDEFREQUEST.fields_by_name['fielddef']._options = None
+_CREATECOMPONENTDEFREQUEST.fields_by_name['parent']._options = None
+_CREATECOMPONENTDEFREQUEST.fields_by_name['component_def']._options = None
+_DELETECOMPONENTDEFREQUEST.fields_by_name['name']._options = None
+_LISTISSUETEMPLATESREQUEST.fields_by_name['parent']._options = None
+_LISTCOMPONENTDEFSREQUEST.fields_by_name['parent']._options = None
+
+_PROJECTS = _descriptor.ServiceDescriptor(
+ name='Projects',
+ full_name='monorail.v3.Projects',
+ file=DESCRIPTOR,
+ index=0,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ serialized_start=1094,
+ serialized_end=1656,
+ methods=[
+ _descriptor.MethodDescriptor(
+ name='CreateFieldDef',
+ full_name='monorail.v3.Projects.CreateFieldDef',
+ index=0,
+ containing_service=None,
+ input_type=_CREATEFIELDDEFREQUEST,
+ output_type=api_dot_v3_dot_api__proto_dot_project__objects__pb2._FIELDDEF,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='CreateComponentDef',
+ full_name='monorail.v3.Projects.CreateComponentDef',
+ index=1,
+ containing_service=None,
+ input_type=_CREATECOMPONENTDEFREQUEST,
+ output_type=api_dot_v3_dot_api__proto_dot_project__objects__pb2._COMPONENTDEF,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='DeleteComponentDef',
+ full_name='monorail.v3.Projects.DeleteComponentDef',
+ index=2,
+ containing_service=None,
+ input_type=_DELETECOMPONENTDEFREQUEST,
+ output_type=google_dot_protobuf_dot_empty__pb2._EMPTY,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListIssueTemplates',
+ full_name='monorail.v3.Projects.ListIssueTemplates',
+ index=3,
+ containing_service=None,
+ input_type=_LISTISSUETEMPLATESREQUEST,
+ output_type=_LISTISSUETEMPLATESRESPONSE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListComponentDefs',
+ full_name='monorail.v3.Projects.ListComponentDefs',
+ index=4,
+ containing_service=None,
+ input_type=_LISTCOMPONENTDEFSREQUEST,
+ output_type=_LISTCOMPONENTDEFSRESPONSE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListProjects',
+ full_name='monorail.v3.Projects.ListProjects',
+ index=5,
+ containing_service=None,
+ input_type=_LISTPROJECTSREQUEST,
+ output_type=_LISTPROJECTSRESPONSE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+])
+_sym_db.RegisterServiceDescriptor(_PROJECTS)
+
+DESCRIPTOR.services_by_name['Projects'] = _PROJECTS
+
+# @@protoc_insertion_point(module_scope)
diff --git a/api/v3/api_proto/projects_prpc_pb2.py b/api/v3/api_proto/projects_prpc_pb2.py
new file mode 100644
index 0000000..326f69d
--- /dev/null
+++ b/api/v3/api_proto/projects_prpc_pb2.py
@@ -0,0 +1,889 @@
+# Generated by the pRPC protocol buffer compiler plugin. DO NOT EDIT!
+# source: api/v3/api_proto/projects.proto
+
+import base64
+import zlib
+
+from google.protobuf import descriptor_pb2
+
+# Includes description of the api/v3/api_proto/projects.proto and all of its transitive
+# dependencies. Includes source code info.
+FILE_DESCRIPTOR_SET = descriptor_pb2.FileDescriptorSet()
+FILE_DESCRIPTOR_SET.ParseFromString(zlib.decompress(base64.b64decode(
+ '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'
+ 'd+3sj/kzOUzH9mdyOGrE7cEevVT//zc7BrM=')))
+_INDEX = {
+ f.name: {
+ 'descriptor': f,
+ 'services': {s.name: s for s in f.service},
+ }
+ for f in FILE_DESCRIPTOR_SET.file
+}
+
+
+ProjectsServiceDescription = {
+ 'file_descriptor_set': FILE_DESCRIPTOR_SET,
+ 'file_descriptor': _INDEX[u'api/v3/api_proto/projects.proto']['descriptor'],
+ 'service_descriptor': _INDEX[u'api/v3/api_proto/projects.proto']['services'][u'Projects'],
+}
diff --git a/api/v3/api_proto/user_objects.proto b/api/v3/api_proto/user_objects.proto
new file mode 100644
index 0000000..958efbc
--- /dev/null
+++ b/api/v3/api_proto/user_objects.proto
@@ -0,0 +1,183 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+// This file defines protobufs for users and related business
+// objects, e.g., users, user preferences.
+
+syntax = "proto3";
+
+package monorail.v3;
+
+option go_package = "api/v3/api_proto";
+
+import "google/api/resource.proto";
+import "google/api/field_behavior.proto";
+
+// User represents a user of the Monorail site.
+// Next available tag: 5
+message User {
+ option (google.api.resource) = {
+ type: "api.crbug.com/User"
+ pattern: "users/{user_id}"
+ };
+ // Resource name of the user.
+ // The API will always return User names with format: users/<user_id>.
+ // However the API will accept User names with formats: users/<user_id> or users/<email>.
+ // To fetch the display_name for any users/<user_id> returned by the API,
+ // you can call {Batch}GetUser{s}.
+ // We represent deleted users within Monorail with `users/1` or `users/2103649657`.
+ string name = 1;
+ // User display_name to show other users using the site.
+ // By default this is the obscured or un-obscured email.
+ string display_name = 2;
+ // Obscured or un-obscured user email or empty if this represents
+ // a deleted user.
+ string email = 4 [ (google.api.field_behavior) = OUTPUT_ONLY ];
+ // User-written indication of their availability or working hours.
+ string availability_message = 3;
+}
+
+
+// UserSettings represents preferences and account settings of a User.
+// Next available tag: 8
+message UserSettings {
+ option (google.api.resource) = {
+ type: "api.crbug.com/UserSettings"
+ pattern: "usersettings/{user_id}"
+ };
+
+ // Potential roles of a user.
+ // Next available tag: 3
+ enum SiteRole {
+ // Default value. This value is unused.
+ SITE_ROLE_UNSPECIFIED = 0;
+ // Normal site user with no special site-wide extra permissions.
+ NORMAL = 1;
+ // Site-wide admin role.
+ ADMIN = 2;
+ }
+
+ // The access the user has to the site.
+ // Next available tag: 3
+ message SiteAccess {
+ // Potential status of a user's access to the site.
+ // Next available tag: 3
+ enum Status {
+ // Default value. This value is unused.
+ STATUS_UNSPECIFIED = 0;
+ // The user has access to the site.
+ FULL_ACCESS = 1;
+ // The user is banned from the site.
+ BANNED = 2;
+ }
+
+ // The status of the user's access to the site.
+ Status status = 1;
+ // An explanation for the value of `status`.
+ string reason = 2;
+ }
+
+ // Trait options for notifications the user receives.
+ // Next available tag: 6;
+ enum NotificationTraits {
+ // Default value. This value is unused.
+ NOTIFICATION_TRAITS_UNSPECIFIED = 0;
+ // Send change notifications for issues where user is owner or cc.
+ NOTIFY_ON_OWNED_OR_CC_ISSUE_CHANGES = 1;
+ // Send change notifications for issues the user has starred.
+ NOTIFY_ON_STARRED_ISSUE_CHANGES = 2;
+ // Send date-type field notifications for issues the user has starred.
+ // See monorail/doc/userguide/email.md#why-did-i-get-a-follow_up-email-notification.
+ NOTIFY_ON_STARRED_NOTIFY_DATES = 3;
+ // Email subject lines should be compact.
+ COMPACT_SUBJECT_LINE = 4;
+ // Include a button link to the issue, in Gmail.
+ GMAIL_INCLUDE_ISSUE_LINK_BUTTON = 5;
+ }
+
+ // Privacy trait options for the user.
+ // Next available tag: 2
+ enum PrivacyTraits {
+ // Default value. This value is unused.
+ PRIVACY_TRAITS_UNSPECIFIED = 0;
+ // Obscure the user's email from non-project members throughout the site.
+ OBSCURE_EMAIL = 1;
+ }
+
+ // Site interaction trait options for the user.
+ // Next available tag: 3
+ enum SiteInteractionTraits {
+ // Default value. This value is unused.
+ SITE_INTERACTION_TRAITS_UNSPECIFIED = 0;
+ // Add 'Restrict-View-Google' labels to new issues the user reports.
+ // Issues will only be visible to the user (issue reporter)
+ // and users with the `Google` permission.
+ REPORT_RESTRICT_VIEW_GOOGLE_ISSUES = 1;
+ // When viewing public issues, show a banner to remind the user not
+ // to post sensitive information.
+ PUBLIC_ISSUE_BANNER = 2;
+ }
+
+ // Resource name of the user that has these settings.
+ string name = 1 [ (google.api.resource_reference) = {type: "api.crbug.com/UserSettings"} ];
+ // The global site role for the user.
+ SiteRole site_role = 2 [ (google.api.field_behavior) = OUTPUT_ONLY ];
+ // Resource name of linked secondary users.
+ repeated string linked_secondary_users = 3 [
+ (google.api.resource_reference) = {type: "api.crbug.com/User"},
+ (google.api.field_behavior) = OUTPUT_ONLY ];
+ // The user's access to the site.
+ SiteAccess site_access = 4 [ (google.api.field_behavior) = OUTPUT_ONLY ];
+ // Notification trait preferences of the user.
+ repeated NotificationTraits notification_traits = 5;
+ // Privacy trait preferences of the user.
+ repeated PrivacyTraits privacy_traits = 6;
+ // Site interaction trait preferences of the user.
+ repeated SiteInteractionTraits site_interaction_traits = 7;
+}
+
+// Defines saved queries that belong to a user.
+//
+// Next available tag: 6
+message UserSavedQuery {
+ option (google.api.resource) = {
+ type: "api.crbug.com/UserSavedQuery"
+ pattern: "users/{user_id}/savedQueries/{saved_query_id}"
+ };
+
+ // Resource name of this saved query.
+ string name = 1;
+ // Display name of this saved query, ie 'open issues'.
+ string display_name = 2;
+ // Search term of this saved query.
+ string query = 3;
+ // List of projects this query can be searched in.
+ repeated string projects = 4 [
+ (google.api.resource_reference) = { type: "api.crbug.com/Project" }
+ ];
+ // Subscription mode of this saved query
+ // Next available tag: 3
+ enum SubscriptionMode {
+ // Default API value. This value is unused.
+ SUBSCRIPTION_MODE_UNSPECIFIED = 0;
+ // Do not subscribe to notifications.
+ NO_NOTIFICATION = 1;
+ // Subscribe to notifications.
+ IMMEDIATE_NOTIFICATION = 2;
+ }
+ SubscriptionMode subscription_mode = 5;
+}
+
+// A project starred by a user.
+//
+// Next available tag: 2
+message ProjectStar {
+ option (google.api.resource) = {
+ type: "api.crbug.com/ProjectStar"
+ pattern: "users/{user_id}/projectStars/{project_name}"
+ };
+ // Resource name of the ProjectStar.
+ string name = 1;
+}
\ No newline at end of file
diff --git a/api/v3/api_proto/user_objects_pb2.py b/api/v3/api_proto/user_objects_pb2.py
new file mode 100644
index 0000000..2c68f09
--- /dev/null
+++ b/api/v3/api_proto/user_objects_pb2.py
@@ -0,0 +1,551 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/v3/api_proto/user_objects.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()
+
+
+from google.api import resource_pb2 as google_dot_api_dot_resource__pb2
+from google.api import field_behavior_pb2 as google_dot_api_dot_field__behavior__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='api/v3/api_proto/user_objects.proto',
+ package='monorail.v3',
+ syntax='proto3',
+ serialized_options=b'Z\020api/v3/api_proto',
+ create_key=_descriptor._internal_create_key,
+ serialized_pb=b'\n#api/v3/api_proto/user_objects.proto\x12\x0bmonorail.v3\x1a\x19google/api/resource.proto\x1a\x1fgoogle/api/field_behavior.proto\"\x86\x01\n\x04User\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12\x12\n\x05\x65mail\x18\x04 \x01(\tB\x03\xe0\x41\x03\x12\x1c\n\x14\x61vailability_message\x18\x03 \x01(\t:(\xea\x41%\n\x12\x61pi.crbug.com/User\x12\x0fusers/{user_id}\"\x9a\t\n\x0cUserSettings\x12-\n\x04name\x18\x01 \x01(\tB\x1f\xfa\x41\x1c\n\x1a\x61pi.crbug.com/UserSettings\x12:\n\tsite_role\x18\x02 \x01(\x0e\x32\".monorail.v3.UserSettings.SiteRoleB\x03\xe0\x41\x03\x12:\n\x16linked_secondary_users\x18\x03 \x03(\tB\x1a\xfa\x41\x14\n\x12\x61pi.crbug.com/User\xe0\x41\x03\x12>\n\x0bsite_access\x18\x04 \x01(\x0b\x32$.monorail.v3.UserSettings.SiteAccessB\x03\xe0\x41\x03\x12I\n\x13notification_traits\x18\x05 \x03(\x0e\x32,.monorail.v3.UserSettings.NotificationTraits\x12?\n\x0eprivacy_traits\x18\x06 \x03(\x0e\x32\'.monorail.v3.UserSettings.PrivacyTraits\x12P\n\x17site_interaction_traits\x18\x07 \x03(\x0e\x32/.monorail.v3.UserSettings.SiteInteractionTraits\x1a\x98\x01\n\nSiteAccess\x12;\n\x06status\x18\x01 \x01(\x0e\x32+.monorail.v3.UserSettings.SiteAccess.Status\x12\x0e\n\x06reason\x18\x02 \x01(\t\"=\n\x06Status\x12\x16\n\x12STATUS_UNSPECIFIED\x10\x00\x12\x0f\n\x0b\x46ULL_ACCESS\x10\x01\x12\n\n\x06\x42\x41NNED\x10\x02\"<\n\x08SiteRole\x12\x19\n\x15SITE_ROLE_UNSPECIFIED\x10\x00\x12\n\n\x06NORMAL\x10\x01\x12\t\n\x05\x41\x44MIN\x10\x02\"\xea\x01\n\x12NotificationTraits\x12#\n\x1fNOTIFICATION_TRAITS_UNSPECIFIED\x10\x00\x12\'\n#NOTIFY_ON_OWNED_OR_CC_ISSUE_CHANGES\x10\x01\x12#\n\x1fNOTIFY_ON_STARRED_ISSUE_CHANGES\x10\x02\x12\"\n\x1eNOTIFY_ON_STARRED_NOTIFY_DATES\x10\x03\x12\x18\n\x14\x43OMPACT_SUBJECT_LINE\x10\x04\x12#\n\x1fGMAIL_INCLUDE_ISSUE_LINK_BUTTON\x10\x05\"B\n\rPrivacyTraits\x12\x1e\n\x1aPRIVACY_TRAITS_UNSPECIFIED\x10\x00\x12\x11\n\rOBSCURE_EMAIL\x10\x01\"\x81\x01\n\x15SiteInteractionTraits\x12\'\n#SITE_INTERACTION_TRAITS_UNSPECIFIED\x10\x00\x12&\n\"REPORT_RESTRICT_VIEW_GOOGLE_ISSUES\x10\x01\x12\x17\n\x13PUBLIC_ISSUE_BANNER\x10\x02:7\xea\x41\x34\n\x1a\x61pi.crbug.com/UserSettings\x12\x16usersettings/{user_id}\"\xf4\x02\n\x0eUserSavedQuery\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12\r\n\x05query\x18\x03 \x01(\t\x12,\n\x08projects\x18\x04 \x03(\tB\x1a\xfa\x41\x17\n\x15\x61pi.crbug.com/Project\x12G\n\x11subscription_mode\x18\x05 \x01(\x0e\x32,.monorail.v3.UserSavedQuery.SubscriptionMode\"f\n\x10SubscriptionMode\x12!\n\x1dSUBSCRIPTION_MODE_UNSPECIFIED\x10\x00\x12\x13\n\x0fNO_NOTIFICATION\x10\x01\x12\x1a\n\x16IMMEDIATE_NOTIFICATION\x10\x02:P\xea\x41M\n\x1c\x61pi.crbug.com/UserSavedQuery\x12-users/{user_id}/savedQueries/{saved_query_id}\"h\n\x0bProjectStar\x12\x0c\n\x04name\x18\x01 \x01(\t:K\xea\x41H\n\x19\x61pi.crbug.com/ProjectStar\x12+users/{user_id}/projectStars/{project_name}B\x12Z\x10\x61pi/v3/api_protob\x06proto3'
+ ,
+ dependencies=[google_dot_api_dot_resource__pb2.DESCRIPTOR,google_dot_api_dot_field__behavior__pb2.DESCRIPTOR,])
+
+
+
+_USERSETTINGS_SITEACCESS_STATUS = _descriptor.EnumDescriptor(
+ name='Status',
+ full_name='monorail.v3.UserSettings.SiteAccess.Status',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='STATUS_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='FULL_ACCESS', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='BANNED', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=811,
+ serialized_end=872,
+)
+_sym_db.RegisterEnumDescriptor(_USERSETTINGS_SITEACCESS_STATUS)
+
+_USERSETTINGS_SITEROLE = _descriptor.EnumDescriptor(
+ name='SiteRole',
+ full_name='monorail.v3.UserSettings.SiteRole',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='SITE_ROLE_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='NORMAL', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='ADMIN', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=874,
+ serialized_end=934,
+)
+_sym_db.RegisterEnumDescriptor(_USERSETTINGS_SITEROLE)
+
+_USERSETTINGS_NOTIFICATIONTRAITS = _descriptor.EnumDescriptor(
+ name='NotificationTraits',
+ full_name='monorail.v3.UserSettings.NotificationTraits',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='NOTIFICATION_TRAITS_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='NOTIFY_ON_OWNED_OR_CC_ISSUE_CHANGES', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='NOTIFY_ON_STARRED_ISSUE_CHANGES', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='NOTIFY_ON_STARRED_NOTIFY_DATES', index=3, number=3,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='COMPACT_SUBJECT_LINE', index=4, number=4,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='GMAIL_INCLUDE_ISSUE_LINK_BUTTON', index=5, number=5,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=937,
+ serialized_end=1171,
+)
+_sym_db.RegisterEnumDescriptor(_USERSETTINGS_NOTIFICATIONTRAITS)
+
+_USERSETTINGS_PRIVACYTRAITS = _descriptor.EnumDescriptor(
+ name='PrivacyTraits',
+ full_name='monorail.v3.UserSettings.PrivacyTraits',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='PRIVACY_TRAITS_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='OBSCURE_EMAIL', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=1173,
+ serialized_end=1239,
+)
+_sym_db.RegisterEnumDescriptor(_USERSETTINGS_PRIVACYTRAITS)
+
+_USERSETTINGS_SITEINTERACTIONTRAITS = _descriptor.EnumDescriptor(
+ name='SiteInteractionTraits',
+ full_name='monorail.v3.UserSettings.SiteInteractionTraits',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='SITE_INTERACTION_TRAITS_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='REPORT_RESTRICT_VIEW_GOOGLE_ISSUES', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='PUBLIC_ISSUE_BANNER', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=1242,
+ serialized_end=1371,
+)
+_sym_db.RegisterEnumDescriptor(_USERSETTINGS_SITEINTERACTIONTRAITS)
+
+_USERSAVEDQUERY_SUBSCRIPTIONMODE = _descriptor.EnumDescriptor(
+ name='SubscriptionMode',
+ full_name='monorail.v3.UserSavedQuery.SubscriptionMode',
+ filename=None,
+ file=DESCRIPTOR,
+ create_key=_descriptor._internal_create_key,
+ values=[
+ _descriptor.EnumValueDescriptor(
+ name='SUBSCRIPTION_MODE_UNSPECIFIED', index=0, number=0,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='NO_NOTIFICATION', index=1, number=1,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ _descriptor.EnumValueDescriptor(
+ name='IMMEDIATE_NOTIFICATION', index=2, number=2,
+ serialized_options=None,
+ type=None,
+ create_key=_descriptor._internal_create_key),
+ ],
+ containing_type=None,
+ serialized_options=None,
+ serialized_start=1619,
+ serialized_end=1721,
+)
+_sym_db.RegisterEnumDescriptor(_USERSAVEDQUERY_SUBSCRIPTIONMODE)
+
+
+_USER = _descriptor.Descriptor(
+ name='User',
+ full_name='monorail.v3.User',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.User.name', 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.v3.User.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='email', full_name='monorail.v3.User.email', index=2,
+ 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=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='availability_message', full_name='monorail.v3.User.availability_message', index=3,
+ 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=b'\352A%\n\022api.crbug.com/User\022\017users/{user_id}',
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=113,
+ serialized_end=247,
+)
+
+
+_USERSETTINGS_SITEACCESS = _descriptor.Descriptor(
+ name='SiteAccess',
+ full_name='monorail.v3.UserSettings.SiteAccess',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='status', full_name='monorail.v3.UserSettings.SiteAccess.status', index=0,
+ number=1, type=14, cpp_type=8, 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='reason', full_name='monorail.v3.UserSettings.SiteAccess.reason', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ _USERSETTINGS_SITEACCESS_STATUS,
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=720,
+ serialized_end=872,
+)
+
+_USERSETTINGS = _descriptor.Descriptor(
+ name='UserSettings',
+ full_name='monorail.v3.UserSettings',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.UserSettings.name', 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=b'\372A\034\n\032api.crbug.com/UserSettings', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='site_role', full_name='monorail.v3.UserSettings.site_role', index=1,
+ number=2, type=14, cpp_type=8, 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=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='linked_secondary_users', full_name='monorail.v3.UserSettings.linked_secondary_users', 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=b'\372A\024\n\022api.crbug.com/User\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='site_access', full_name='monorail.v3.UserSettings.site_access', index=3,
+ number=4, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\003', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='notification_traits', full_name='monorail.v3.UserSettings.notification_traits', index=4,
+ number=5, type=14, cpp_type=8, 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='privacy_traits', full_name='monorail.v3.UserSettings.privacy_traits', index=5,
+ number=6, type=14, cpp_type=8, 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='site_interaction_traits', full_name='monorail.v3.UserSettings.site_interaction_traits', index=6,
+ number=7, type=14, cpp_type=8, 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=[_USERSETTINGS_SITEACCESS, ],
+ enum_types=[
+ _USERSETTINGS_SITEROLE,
+ _USERSETTINGS_NOTIFICATIONTRAITS,
+ _USERSETTINGS_PRIVACYTRAITS,
+ _USERSETTINGS_SITEINTERACTIONTRAITS,
+ ],
+ serialized_options=b'\352A4\n\032api.crbug.com/UserSettings\022\026usersettings/{user_id}',
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=250,
+ serialized_end=1428,
+)
+
+
+_USERSAVEDQUERY = _descriptor.Descriptor(
+ name='UserSavedQuery',
+ full_name='monorail.v3.UserSavedQuery',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.UserSavedQuery.name', 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.v3.UserSavedQuery.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='query', full_name='monorail.v3.UserSavedQuery.query', 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='projects', full_name='monorail.v3.UserSavedQuery.projects', index=3,
+ number=4, 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=b'\372A\027\n\025api.crbug.com/Project', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='subscription_mode', full_name='monorail.v3.UserSavedQuery.subscription_mode', index=4,
+ number=5, type=14, cpp_type=8, 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ _USERSAVEDQUERY_SUBSCRIPTIONMODE,
+ ],
+ serialized_options=b'\352AM\n\034api.crbug.com/UserSavedQuery\022-users/{user_id}/savedQueries/{saved_query_id}',
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1431,
+ serialized_end=1803,
+)
+
+
+_PROJECTSTAR = _descriptor.Descriptor(
+ name='ProjectStar',
+ full_name='monorail.v3.ProjectStar',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.ProjectStar.name', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=b'\352AH\n\031api.crbug.com/ProjectStar\022+users/{user_id}/projectStars/{project_name}',
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=1805,
+ serialized_end=1909,
+)
+
+_USERSETTINGS_SITEACCESS.fields_by_name['status'].enum_type = _USERSETTINGS_SITEACCESS_STATUS
+_USERSETTINGS_SITEACCESS.containing_type = _USERSETTINGS
+_USERSETTINGS_SITEACCESS_STATUS.containing_type = _USERSETTINGS_SITEACCESS
+_USERSETTINGS.fields_by_name['site_role'].enum_type = _USERSETTINGS_SITEROLE
+_USERSETTINGS.fields_by_name['site_access'].message_type = _USERSETTINGS_SITEACCESS
+_USERSETTINGS.fields_by_name['notification_traits'].enum_type = _USERSETTINGS_NOTIFICATIONTRAITS
+_USERSETTINGS.fields_by_name['privacy_traits'].enum_type = _USERSETTINGS_PRIVACYTRAITS
+_USERSETTINGS.fields_by_name['site_interaction_traits'].enum_type = _USERSETTINGS_SITEINTERACTIONTRAITS
+_USERSETTINGS_SITEROLE.containing_type = _USERSETTINGS
+_USERSETTINGS_NOTIFICATIONTRAITS.containing_type = _USERSETTINGS
+_USERSETTINGS_PRIVACYTRAITS.containing_type = _USERSETTINGS
+_USERSETTINGS_SITEINTERACTIONTRAITS.containing_type = _USERSETTINGS
+_USERSAVEDQUERY.fields_by_name['subscription_mode'].enum_type = _USERSAVEDQUERY_SUBSCRIPTIONMODE
+_USERSAVEDQUERY_SUBSCRIPTIONMODE.containing_type = _USERSAVEDQUERY
+DESCRIPTOR.message_types_by_name['User'] = _USER
+DESCRIPTOR.message_types_by_name['UserSettings'] = _USERSETTINGS
+DESCRIPTOR.message_types_by_name['UserSavedQuery'] = _USERSAVEDQUERY
+DESCRIPTOR.message_types_by_name['ProjectStar'] = _PROJECTSTAR
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+User = _reflection.GeneratedProtocolMessageType('User', (_message.Message,), {
+ 'DESCRIPTOR' : _USER,
+ '__module__' : 'api.v3.api_proto.user_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.User)
+ })
+_sym_db.RegisterMessage(User)
+
+UserSettings = _reflection.GeneratedProtocolMessageType('UserSettings', (_message.Message,), {
+
+ 'SiteAccess' : _reflection.GeneratedProtocolMessageType('SiteAccess', (_message.Message,), {
+ 'DESCRIPTOR' : _USERSETTINGS_SITEACCESS,
+ '__module__' : 'api.v3.api_proto.user_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.UserSettings.SiteAccess)
+ })
+ ,
+ 'DESCRIPTOR' : _USERSETTINGS,
+ '__module__' : 'api.v3.api_proto.user_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.UserSettings)
+ })
+_sym_db.RegisterMessage(UserSettings)
+_sym_db.RegisterMessage(UserSettings.SiteAccess)
+
+UserSavedQuery = _reflection.GeneratedProtocolMessageType('UserSavedQuery', (_message.Message,), {
+ 'DESCRIPTOR' : _USERSAVEDQUERY,
+ '__module__' : 'api.v3.api_proto.user_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.UserSavedQuery)
+ })
+_sym_db.RegisterMessage(UserSavedQuery)
+
+ProjectStar = _reflection.GeneratedProtocolMessageType('ProjectStar', (_message.Message,), {
+ 'DESCRIPTOR' : _PROJECTSTAR,
+ '__module__' : 'api.v3.api_proto.user_objects_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ProjectStar)
+ })
+_sym_db.RegisterMessage(ProjectStar)
+
+
+DESCRIPTOR._options = None
+_USER.fields_by_name['email']._options = None
+_USER._options = None
+_USERSETTINGS.fields_by_name['name']._options = None
+_USERSETTINGS.fields_by_name['site_role']._options = None
+_USERSETTINGS.fields_by_name['linked_secondary_users']._options = None
+_USERSETTINGS.fields_by_name['site_access']._options = None
+_USERSETTINGS._options = None
+_USERSAVEDQUERY.fields_by_name['projects']._options = None
+_USERSAVEDQUERY._options = None
+_PROJECTSTAR._options = None
+# @@protoc_insertion_point(module_scope)
diff --git a/api/v3/api_proto/users.proto b/api/v3/api_proto/users.proto
new file mode 100644
index 0000000..7d8aa48
--- /dev/null
+++ b/api/v3/api_proto/users.proto
@@ -0,0 +1,161 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+syntax = "proto3";
+
+package monorail.v3;
+
+option go_package = "api/v3/api_proto";
+
+import "google/api/field_behavior.proto";
+import "google/api/resource.proto";
+import "api/v3/api_proto/user_objects.proto";
+import "google/protobuf/empty.proto";
+import "google/protobuf/field_mask.proto";
+
+// ***ONLY CALL rpcs WITH `status: {ALPHA|STABLE}`***
+// rpcs without `status` are not ready.
+
+// Users service includes all methods needed for managing Users.
+service Users {
+ // status: ALPHA
+ // Returns the requested User.
+ //
+ // Raises:
+ // NOT_FOUND is the user is not found.
+ // INVALID_ARGUMENT if the `name` is invalid.
+ rpc GetUser (GetUserRequest) returns (User) {}
+
+ // status: ALPHA
+ // Returns all of the requested Users.
+ //
+ // Raises:
+ // NOT_FOUND if any users are not found.
+ // INVALID_ARGUMENT if any `names` are invalid.
+ rpc BatchGetUsers (BatchGetUsersRequest) returns (BatchGetUsersResponse) {}
+
+ // status: NOT READY
+ // Updates a User.
+ //
+ // Raises:
+ // NOT_FOUND if the user is not found.
+ // PERMISSION_DENIED if the requester is not allowed to update the user.
+ // INVALID_ARGUMENT if required fields are missing or fields are invalid.
+ rpc UpdateUser (UpdateUserRequest) returns (User) {}
+
+ // status: NOT READY
+ // Stars a given project for the requestor.
+ //
+ // Raises:
+ // NOT_FOUND if the requested project is not found.
+ // INVALID_ARGUMENT if the given `project` is not valid.
+ rpc StarProject (StarProjectRequest) returns (ProjectStar) {}
+
+ // status: NOT READY
+ // Unstars a given project for the requestor.
+ //
+ // Raises:
+ // NOT_FOUND if the requested project is not found.
+ // INVALID_ARGUMENT if the given `project` is not valid.
+ rpc UnStarProject (UnStarProjectRequest) returns (google.protobuf.Empty) {}
+
+ // status: NOT READY
+ // Lists all of a user's starred projects.
+ //
+ // Raises:
+ // NOT_FOUND if the requested user is not found.
+ // INVALID_ARGUMENT if the given `parent` is not valid.
+ rpc ListProjectStars (ListProjectStarsRequest) returns (ListProjectStarsResponse) {}
+}
+
+
+// The request message for Users.GetUser.
+// Next available tag: 2
+message GetUserRequest {
+ // The name of the user to request.
+ string name = 1 [
+ (google.api.resource_reference) = {type: "api.crbug.com/User"},
+ (google.api.field_behavior) = REQUIRED ];
+}
+
+
+// The request message for Users.BatchGetUsers.
+// Next available tag: 2
+message BatchGetUsersRequest {
+ // The name of the users to request. At most 100 may be requested.
+ repeated string names = 1 [
+ (google.api.resource_reference) = {type: "api.crbug.com/User"},
+ (google.api.field_behavior) = REQUIRED ];
+}
+
+
+// The response message for Users.BatchGetUsers.
+// Next available tag: 2
+message BatchGetUsersResponse {
+ // The users that were requested.
+ repeated User users = 1;
+}
+
+
+// The request message for Users.UpdateUser.
+// Next available tag: 3
+message UpdateUserRequest {
+ // The user's `name` field is used to identify the user to be updated.
+ User user = 1 [
+ (google.api.field_behavior) = REQUIRED,
+ (google.api.resource_reference) = {type: "api.crbug.com/User"} ];
+ // The list of fields to be updated.
+ google.protobuf.FieldMask update_mask = 2 [ (google.api.field_behavior) = REQUIRED ];
+}
+
+
+// The request message for Users.StarProject.
+// Next available tag: 2
+message StarProjectRequest {
+ // The resource name for the Project to star.
+ string project = 1 [
+ (google.api.resource_reference) = {type: "api.crbug.com/Project"},
+ (google.api.field_behavior) = REQUIRED ];
+}
+
+
+// The request message for Users.UnStarProject.
+// Next available tag: 2
+message UnStarProjectRequest {
+ // The resource name for the Project to unstar.
+ string project = 1 [
+ (google.api.resource_reference) = {type: "api.crbug.com/Project"},
+ (google.api.field_behavior) = REQUIRED ];
+}
+
+
+// The request message for Users.ListProjectStars.
+// Next available tag: 4
+message ListProjectStarsRequest {
+ // The resource name for the user having stars listed.
+ string parent = 1 [
+ (google.api.resource_reference) = {type: "api.crbug.com/User"},
+ (google.api.field_behavior) = REQUIRED ];
+ // The maximum number of items to return. The service may return fewer than
+ // this value.
+ // If unspecified, at most 1000 items will be returned.
+ int32 page_size = 2;
+ // A page token, received from a previous `ListProjectStars` call.
+ // Provide this to retrieve the subsequent page.
+ //
+ // When paginating, all other parameters provided to `ListProjectStars` must
+ // match the call that provided the page token.
+ string page_token = 3;
+}
+
+
+// The response message for Users.ListProjectStars.
+// Next available tag: 3
+message ListProjectStarsResponse {
+ // Data for each starred project.
+ repeated ProjectStar project_stars = 1;
+ // A token, which can be sent as `page_token` to retrieve the next page.
+ // If this field is omitted, there are no subsequent pages.
+ string next_page_token = 2;
+}
diff --git a/api/v3/api_proto/users_pb2.py b/api/v3/api_proto/users_pb2.py
new file mode 100644
index 0000000..f08b494
--- /dev/null
+++ b/api/v3/api_proto/users_pb2.py
@@ -0,0 +1,472 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: api/v3/api_proto/users.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()
+
+
+from google.api import field_behavior_pb2 as google_dot_api_dot_field__behavior__pb2
+from google.api import resource_pb2 as google_dot_api_dot_resource__pb2
+from api.v3.api_proto import user_objects_pb2 as api_dot_v3_dot_api__proto_dot_user__objects__pb2
+from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
+from google.protobuf import field_mask_pb2 as google_dot_protobuf_dot_field__mask__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='api/v3/api_proto/users.proto',
+ package='monorail.v3',
+ syntax='proto3',
+ serialized_options=b'Z\020api/v3/api_proto',
+ create_key=_descriptor._internal_create_key,
+ serialized_pb=b'\n\x1c\x61pi/v3/api_proto/users.proto\x12\x0bmonorail.v3\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a#api/v3/api_proto/user_objects.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\":\n\x0eGetUserRequest\x12(\n\x04name\x18\x01 \x01(\tB\x1a\xfa\x41\x14\n\x12\x61pi.crbug.com/User\xe0\x41\x02\"A\n\x14\x42\x61tchGetUsersRequest\x12)\n\x05names\x18\x01 \x03(\tB\x1a\xfa\x41\x14\n\x12\x61pi.crbug.com/User\xe0\x41\x02\"9\n\x15\x42\x61tchGetUsersResponse\x12 \n\x05users\x18\x01 \x03(\x0b\x32\x11.monorail.v3.User\"\x86\x01\n\x11UpdateUserRequest\x12;\n\x04user\x18\x01 \x01(\x0b\x32\x11.monorail.v3.UserB\x1a\xe0\x41\x02\xfa\x41\x14\n\x12\x61pi.crbug.com/User\x12\x34\n\x0bupdate_mask\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.FieldMaskB\x03\xe0\x41\x02\"D\n\x12StarProjectRequest\x12.\n\x07project\x18\x01 \x01(\tB\x1d\xfa\x41\x17\n\x15\x61pi.crbug.com/Project\xe0\x41\x02\"F\n\x14UnStarProjectRequest\x12.\n\x07project\x18\x01 \x01(\tB\x1d\xfa\x41\x17\n\x15\x61pi.crbug.com/Project\xe0\x41\x02\"l\n\x17ListProjectStarsRequest\x12*\n\x06parent\x18\x01 \x01(\tB\x1a\xfa\x41\x14\n\x12\x61pi.crbug.com/User\xe0\x41\x02\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x12\n\npage_token\x18\x03 \x01(\t\"d\n\x18ListProjectStarsResponse\x12/\n\rproject_stars\x18\x01 \x03(\x0b\x32\x18.monorail.v3.ProjectStar\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t2\xde\x03\n\x05Users\x12;\n\x07GetUser\x12\x1b.monorail.v3.GetUserRequest\x1a\x11.monorail.v3.User\"\x00\x12X\n\rBatchGetUsers\x12!.monorail.v3.BatchGetUsersRequest\x1a\".monorail.v3.BatchGetUsersResponse\"\x00\x12\x41\n\nUpdateUser\x12\x1e.monorail.v3.UpdateUserRequest\x1a\x11.monorail.v3.User\"\x00\x12J\n\x0bStarProject\x12\x1f.monorail.v3.StarProjectRequest\x1a\x18.monorail.v3.ProjectStar\"\x00\x12L\n\rUnStarProject\x12!.monorail.v3.UnStarProjectRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x61\n\x10ListProjectStars\x12$.monorail.v3.ListProjectStarsRequest\x1a%.monorail.v3.ListProjectStarsResponse\"\x00\x42\x12Z\x10\x61pi/v3/api_protob\x06proto3'
+ ,
+ dependencies=[google_dot_api_dot_field__behavior__pb2.DESCRIPTOR,google_dot_api_dot_resource__pb2.DESCRIPTOR,api_dot_v3_dot_api__proto_dot_user__objects__pb2.DESCRIPTOR,google_dot_protobuf_dot_empty__pb2.DESCRIPTOR,google_dot_protobuf_dot_field__mask__pb2.DESCRIPTOR,])
+
+
+
+
+_GETUSERREQUEST = _descriptor.Descriptor(
+ name='GetUserRequest',
+ full_name='monorail.v3.GetUserRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='name', full_name='monorail.v3.GetUserRequest.name', 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=b'\372A\024\n\022api.crbug.com/User\340A\002', 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=205,
+ serialized_end=263,
+)
+
+
+_BATCHGETUSERSREQUEST = _descriptor.Descriptor(
+ name='BatchGetUsersRequest',
+ full_name='monorail.v3.BatchGetUsersRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='names', full_name='monorail.v3.BatchGetUsersRequest.names', index=0,
+ number=1, 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=b'\372A\024\n\022api.crbug.com/User\340A\002', 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=265,
+ serialized_end=330,
+)
+
+
+_BATCHGETUSERSRESPONSE = _descriptor.Descriptor(
+ name='BatchGetUsersResponse',
+ full_name='monorail.v3.BatchGetUsersResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='users', full_name='monorail.v3.BatchGetUsersResponse.users', 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='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=332,
+ serialized_end=389,
+)
+
+
+_UPDATEUSERREQUEST = _descriptor.Descriptor(
+ name='UpdateUserRequest',
+ full_name='monorail.v3.UpdateUserRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='user', full_name='monorail.v3.UpdateUserRequest.user', index=0,
+ number=1, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\002\372A\024\n\022api.crbug.com/User', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='update_mask', full_name='monorail.v3.UpdateUserRequest.update_mask', index=1,
+ number=2, type=11, cpp_type=10, label=1,
+ has_default_value=False, default_value=None,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ serialized_options=b'\340A\002', 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=392,
+ serialized_end=526,
+)
+
+
+_STARPROJECTREQUEST = _descriptor.Descriptor(
+ name='StarProjectRequest',
+ full_name='monorail.v3.StarProjectRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project', full_name='monorail.v3.StarProjectRequest.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=b'\372A\027\n\025api.crbug.com/Project\340A\002', 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=528,
+ serialized_end=596,
+)
+
+
+_UNSTARPROJECTREQUEST = _descriptor.Descriptor(
+ name='UnStarProjectRequest',
+ full_name='monorail.v3.UnStarProjectRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project', full_name='monorail.v3.UnStarProjectRequest.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=b'\372A\027\n\025api.crbug.com/Project\340A\002', 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=598,
+ serialized_end=668,
+)
+
+
+_LISTPROJECTSTARSREQUEST = _descriptor.Descriptor(
+ name='ListProjectStarsRequest',
+ full_name='monorail.v3.ListProjectStarsRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='parent', full_name='monorail.v3.ListProjectStarsRequest.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=b'\372A\024\n\022api.crbug.com/User\340A\002', file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
+ _descriptor.FieldDescriptor(
+ name='page_size', full_name='monorail.v3.ListProjectStarsRequest.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='page_token', full_name='monorail.v3.ListProjectStarsRequest.page_token', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=670,
+ serialized_end=778,
+)
+
+
+_LISTPROJECTSTARSRESPONSE = _descriptor.Descriptor(
+ name='ListProjectStarsResponse',
+ full_name='monorail.v3.ListProjectStarsResponse',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ create_key=_descriptor._internal_create_key,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='project_stars', full_name='monorail.v3.ListProjectStarsResponse.project_stars', 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),
+ _descriptor.FieldDescriptor(
+ name='next_page_token', full_name='monorail.v3.ListProjectStarsResponse.next_page_token', 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),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ serialized_options=None,
+ is_extendable=False,
+ syntax='proto3',
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=780,
+ serialized_end=880,
+)
+
+_BATCHGETUSERSRESPONSE.fields_by_name['users'].message_type = api_dot_v3_dot_api__proto_dot_user__objects__pb2._USER
+_UPDATEUSERREQUEST.fields_by_name['user'].message_type = api_dot_v3_dot_api__proto_dot_user__objects__pb2._USER
+_UPDATEUSERREQUEST.fields_by_name['update_mask'].message_type = google_dot_protobuf_dot_field__mask__pb2._FIELDMASK
+_LISTPROJECTSTARSRESPONSE.fields_by_name['project_stars'].message_type = api_dot_v3_dot_api__proto_dot_user__objects__pb2._PROJECTSTAR
+DESCRIPTOR.message_types_by_name['GetUserRequest'] = _GETUSERREQUEST
+DESCRIPTOR.message_types_by_name['BatchGetUsersRequest'] = _BATCHGETUSERSREQUEST
+DESCRIPTOR.message_types_by_name['BatchGetUsersResponse'] = _BATCHGETUSERSRESPONSE
+DESCRIPTOR.message_types_by_name['UpdateUserRequest'] = _UPDATEUSERREQUEST
+DESCRIPTOR.message_types_by_name['StarProjectRequest'] = _STARPROJECTREQUEST
+DESCRIPTOR.message_types_by_name['UnStarProjectRequest'] = _UNSTARPROJECTREQUEST
+DESCRIPTOR.message_types_by_name['ListProjectStarsRequest'] = _LISTPROJECTSTARSREQUEST
+DESCRIPTOR.message_types_by_name['ListProjectStarsResponse'] = _LISTPROJECTSTARSRESPONSE
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+GetUserRequest = _reflection.GeneratedProtocolMessageType('GetUserRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _GETUSERREQUEST,
+ '__module__' : 'api.v3.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.GetUserRequest)
+ })
+_sym_db.RegisterMessage(GetUserRequest)
+
+BatchGetUsersRequest = _reflection.GeneratedProtocolMessageType('BatchGetUsersRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _BATCHGETUSERSREQUEST,
+ '__module__' : 'api.v3.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.BatchGetUsersRequest)
+ })
+_sym_db.RegisterMessage(BatchGetUsersRequest)
+
+BatchGetUsersResponse = _reflection.GeneratedProtocolMessageType('BatchGetUsersResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _BATCHGETUSERSRESPONSE,
+ '__module__' : 'api.v3.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.BatchGetUsersResponse)
+ })
+_sym_db.RegisterMessage(BatchGetUsersResponse)
+
+UpdateUserRequest = _reflection.GeneratedProtocolMessageType('UpdateUserRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _UPDATEUSERREQUEST,
+ '__module__' : 'api.v3.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.UpdateUserRequest)
+ })
+_sym_db.RegisterMessage(UpdateUserRequest)
+
+StarProjectRequest = _reflection.GeneratedProtocolMessageType('StarProjectRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _STARPROJECTREQUEST,
+ '__module__' : 'api.v3.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.StarProjectRequest)
+ })
+_sym_db.RegisterMessage(StarProjectRequest)
+
+UnStarProjectRequest = _reflection.GeneratedProtocolMessageType('UnStarProjectRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _UNSTARPROJECTREQUEST,
+ '__module__' : 'api.v3.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.UnStarProjectRequest)
+ })
+_sym_db.RegisterMessage(UnStarProjectRequest)
+
+ListProjectStarsRequest = _reflection.GeneratedProtocolMessageType('ListProjectStarsRequest', (_message.Message,), {
+ 'DESCRIPTOR' : _LISTPROJECTSTARSREQUEST,
+ '__module__' : 'api.v3.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ListProjectStarsRequest)
+ })
+_sym_db.RegisterMessage(ListProjectStarsRequest)
+
+ListProjectStarsResponse = _reflection.GeneratedProtocolMessageType('ListProjectStarsResponse', (_message.Message,), {
+ 'DESCRIPTOR' : _LISTPROJECTSTARSRESPONSE,
+ '__module__' : 'api.v3.api_proto.users_pb2'
+ # @@protoc_insertion_point(class_scope:monorail.v3.ListProjectStarsResponse)
+ })
+_sym_db.RegisterMessage(ListProjectStarsResponse)
+
+
+DESCRIPTOR._options = None
+_GETUSERREQUEST.fields_by_name['name']._options = None
+_BATCHGETUSERSREQUEST.fields_by_name['names']._options = None
+_UPDATEUSERREQUEST.fields_by_name['user']._options = None
+_UPDATEUSERREQUEST.fields_by_name['update_mask']._options = None
+_STARPROJECTREQUEST.fields_by_name['project']._options = None
+_UNSTARPROJECTREQUEST.fields_by_name['project']._options = None
+_LISTPROJECTSTARSREQUEST.fields_by_name['parent']._options = None
+
+_USERS = _descriptor.ServiceDescriptor(
+ name='Users',
+ full_name='monorail.v3.Users',
+ file=DESCRIPTOR,
+ index=0,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ serialized_start=883,
+ serialized_end=1361,
+ methods=[
+ _descriptor.MethodDescriptor(
+ name='GetUser',
+ full_name='monorail.v3.Users.GetUser',
+ index=0,
+ containing_service=None,
+ input_type=_GETUSERREQUEST,
+ output_type=api_dot_v3_dot_api__proto_dot_user__objects__pb2._USER,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='BatchGetUsers',
+ full_name='monorail.v3.Users.BatchGetUsers',
+ index=1,
+ containing_service=None,
+ input_type=_BATCHGETUSERSREQUEST,
+ output_type=_BATCHGETUSERSRESPONSE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='UpdateUser',
+ full_name='monorail.v3.Users.UpdateUser',
+ index=2,
+ containing_service=None,
+ input_type=_UPDATEUSERREQUEST,
+ output_type=api_dot_v3_dot_api__proto_dot_user__objects__pb2._USER,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='StarProject',
+ full_name='monorail.v3.Users.StarProject',
+ index=3,
+ containing_service=None,
+ input_type=_STARPROJECTREQUEST,
+ output_type=api_dot_v3_dot_api__proto_dot_user__objects__pb2._PROJECTSTAR,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='UnStarProject',
+ full_name='monorail.v3.Users.UnStarProject',
+ index=4,
+ containing_service=None,
+ input_type=_UNSTARPROJECTREQUEST,
+ output_type=google_dot_protobuf_dot_empty__pb2._EMPTY,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+ _descriptor.MethodDescriptor(
+ name='ListProjectStars',
+ full_name='monorail.v3.Users.ListProjectStars',
+ index=5,
+ containing_service=None,
+ input_type=_LISTPROJECTSTARSREQUEST,
+ output_type=_LISTPROJECTSTARSRESPONSE,
+ serialized_options=None,
+ create_key=_descriptor._internal_create_key,
+ ),
+])
+_sym_db.RegisterServiceDescriptor(_USERS)
+
+DESCRIPTOR.services_by_name['Users'] = _USERS
+
+# @@protoc_insertion_point(module_scope)
diff --git a/api/v3/api_proto/users_prpc_pb2.py b/api/v3/api_proto/users_prpc_pb2.py
new file mode 100644
index 0000000..0d0012a
--- /dev/null
+++ b/api/v3/api_proto/users_prpc_pb2.py
@@ -0,0 +1,622 @@
+# Generated by the pRPC protocol buffer compiler plugin. DO NOT EDIT!
+# source: api/v3/api_proto/users.proto
+
+import base64
+import zlib
+
+from google.protobuf import descriptor_pb2
+
+# Includes description of the api/v3/api_proto/users.proto and all of its transitive
+# dependencies. Includes source code info.
+FILE_DESCRIPTOR_SET = descriptor_pb2.FileDescriptorSet()
+FILE_DESCRIPTOR_SET.ParseFromString(zlib.decompress(base64.b64decode(
+ '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'
+ 'z50zDqK9b0KYa/ngiBIX9KwOf6zPp/AIQlirE=')))
+_INDEX = {
+ f.name: {
+ 'descriptor': f,
+ 'services': {s.name: s for s in f.service},
+ }
+ for f in FILE_DESCRIPTOR_SET.file
+}
+
+
+UsersServiceDescription = {
+ 'file_descriptor_set': FILE_DESCRIPTOR_SET,
+ 'file_descriptor': _INDEX[u'api/v3/api_proto/users.proto']['descriptor'],
+ 'service_descriptor': _INDEX[u'api/v3/api_proto/users.proto']['services'][u'Users'],
+}
diff --git a/api/v3/api_routes.py b/api/v3/api_routes.py
new file mode 100644
index 0000000..b627941
--- /dev/null
+++ b/api/v3/api_routes.py
@@ -0,0 +1,42 @@
+#
+# See the pRPC spec here: https://godoc.org/github.com/luci/luci-go/grpc/prpc
+#
+# Each Servicer corresponds to a service defined in a .proto file in this
+# directory. Each method on that Servicer corresponds to one of the rpcs
+# defined on the service.
+#
+# All APIs are served under the /prpc/* path space. Each service gets its own
+# namespace under that, and each method is an individual endpoints. For example,
+# POST https://bugs.chromium.org/prpc/monorail.v3.Issues/GetIssue
+# would be a call to the api.v3.issues_servicer.IssuesServicer.GetIssue method.
+#
+# Note that this is not a RESTful API, although it is CRUDy. All requests are
+# POSTs, all methods take exactly one input, and all methods return exactly
+# one output.
+#
+# TODO(http://crbug.com/monorail/1703): Actually integrate the rpcexplorer.
+# You can use the API Explorer here: https://bugs.chromium.org/rpcexplorer
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from api.v3 import issues_servicer
+from api.v3 import hotlists_servicer
+from api.v3 import frontend_servicer
+from api.v3 import projects_servicer
+from api.v3 import permissions_servicer
+from api.v3 import users_servicer
+
+
+def RegisterApiHandlers(prpc_server, services):
+ """Registers pRPC API services. And makes their routes
+ available in prpc_server.get_routes().
+ """
+
+ prpc_server.add_service(issues_servicer.IssuesServicer(services))
+ prpc_server.add_service(hotlists_servicer.HotlistsServicer(services))
+ prpc_server.add_service(projects_servicer.ProjectsServicer(services))
+ prpc_server.add_service(permissions_servicer.PermissionsServicer(services))
+ prpc_server.add_service(users_servicer.UsersServicer(services))
+ prpc_server.add_service(frontend_servicer.FrontendServicer(services))
diff --git a/api/v3/apps-script-client/IssueService.js b/api/v3/apps-script-client/IssueService.js
new file mode 100644
index 0000000..d1c6c9d
--- /dev/null
+++ b/api/v3/apps-script-client/IssueService.js
@@ -0,0 +1,908 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/* eslint-disable no-unused-vars */
+
+const COMMENT_TYPE_DESCRIPTION = 'DESCRIPTION';
+
+/**
+ * Fetches the issue from Monorail.
+ * @param {string} issueName The resource name of the issue.
+ * @return {Issue}
+ */
+function getIssue(issueName) {
+ const message = {'name': issueName};
+ const url = URL + 'monorail.v3.Issues/GetIssue';
+ return run_(url, message);
+}
+
+/**
+ * Fetches all the given issues from Monorail.
+ * @param {Array<string>} issueNames The resource names of the issues.
+ * @return {Array<Issue>}
+ */
+function batchGetIssues(issueNames) {
+ const message = {'names': issueNames};
+ const url = URL + 'monorail.v3.Issues/BatchGetIssues';
+ return run_(url, message);
+}
+
+/**
+ * Fetches all the ApprovalValues that belong to the given issue.
+ * @param {string} issueName The resource name of the issue.
+ * @return {Array<ApprovalValue>}
+ */
+function listApprovalValues(issueName) {
+ const message = {'parent': issueName};
+ const url = URL + 'monorail.v3.Issues/ListApprovalValues';
+ return run_(url, message);
+}
+
+/**
+ * Calls SearchIssues with the given parameters.
+ * @param {Array<string>} projectNames resource names of the projects to search.
+ * @param {string} query The query to use to search.
+ * @param {string} orderBy The issue fields to order issues by.
+ * @param {Number} pageSize The maximum issues to return.
+ * @param {string} pageToken The page token from the previous call.
+ * @return {Array<SearchIssuesResponse>}
+ */
+function searchIssuesPagination_(
+ projectNames, query, orderBy, pageSize, pageToken) {
+ const message = {
+ 'projects': projectNames,
+ 'query': query,
+ 'orderBy': orderBy,
+ 'pageToken': pageToken};
+ if (pageSize) {
+ message['pageSize'] = pageSize;
+ }
+ const url = URL + 'monorail.v3.Issues/SearchIssues';
+ return run_(url, message);
+}
+
+// TODO(crbug.com/monorail/7143): SearchIssues only accepts one project.
+/**
+ * Searches Monorail for issues using the given query.
+ * NOTE: We currently only accept `projectNames` with one and only one project.
+ * @param {Array<string>} projects Resource names of the projects to search
+ * within.
+ * @param {string=} query The query to use to search.
+ * @param {string=} orderBy The issue fields to order issues by,
+ * e.g. 'EstDays,Opened,-stars'
+ * @return {Array<Issue>}
+ */
+function searchIssues(projects, query, orderBy) {
+ const pageSize = 100;
+ let pageToken;
+
+ issues = [];
+
+ do {
+ const resp = searchIssuesPagination_(
+ projects, query, orderBy, pageSize, pageToken);
+ issues = issues.concat(resp.issues);
+ pageToken = resp.nextPageToken;
+ }
+ while (pageToken);
+
+ return issues;
+}
+
+/**
+ * Calls ListComments with the given parameters.
+ * @param {string} issueName Resource name of the issue.
+ * @param {string} filter The approval filter query.
+ * @param {Number} pageSize The maximum number of comments to return.
+ * @param {string} pageToken The page token from the previous request.
+ * @return {ListCommentsResponse}
+ */
+function listCommentsPagination_(issueName, filter, pageSize, pageToken) {
+ const message = {
+ 'parent': issueName,
+ 'pageToken': pageToken,
+ 'filter': filter,
+ };
+ if (pageSize) {
+ message['pageSize'] = pageSize;
+ }
+ const url = URL + 'monorail.v3.Issues/ListComments';
+ return run_(url, message);
+}
+
+/**
+ * Returns all comments and previous/current descriptions of an issue.
+ * @param {string} issueName Resource name of the Issue.
+ * @param {string=} filter The filter query filtering out comments.
+ * We only accept `approval = "<approvalDef resource name>""`.
+ * e.g. 'approval = "projects/chromium/approvalDefs/34"'
+ * @return {Array<Comment>}
+ */
+function listComments(issueName, filter) {
+ let pageToken;
+
+ let comments = [];
+ do {
+ const resp = listCommentsPagination_(issueName, filter, '', pageToken);
+ comments = comments.concat(resp.comments);
+ pageToken = resp.nextPageToken;
+ }
+ while (pageToken);
+
+ return comments;
+}
+
+/**
+ * Gets the current description of an issue.
+ * @param {string} issueName Resource name of the Issue.
+ * @param {string=} filter The filter query filtering out comments.
+ * We only accept `approval = "<approvalDef resource name>""`.
+ * e.g. 'approval = "projects/chromium/approvalDefs/34"'
+ * @return {Comment}
+ */
+function getCurrentDescription(issueName, filter) {
+ const allComments = listComments(issueName, filter);
+ for (let i = (allComments.length - 1); i > -1; i--) {
+ if (allComments[i].type === COMMENT_TYPE_DESCRIPTION) {
+ return allComments[i];
+ }
+ }
+}
+
+/**
+ * Gets the first (non-description) comment of an issue.
+ * @param {string} issueName Resource name of the Issue.
+ * @param {string=} filter The filter query filtering out comments.
+ * We only accept `approval = "<approvalDef resource name>""`.
+ * e.g. 'approval = "projects/chromium/approvalDefs/34"'
+ * @return {Comment}
+ */
+function getFirstComment(issueName, filter) {
+ const allComments = listComments(issueName, filter);
+ for (let i = 0; i < allComments.length; i++) {
+ if (allComments[i].type !== COMMENT_TYPE_DESCRIPTION) {
+ return allComments[i];
+ }
+ }
+ return null;
+}
+
+/**
+ * Gets the last (non-description) comment of an issue.
+ * @param {string} issueName The resource name of the issue.
+ * @param {string=} filter The filter query filtering out comments.
+ * We only accept `approval = "<approvalDef resource name>""`.
+ * e.g. 'approval = "projects/chromium/approvalDefs/34"'
+ * @return {Issue}
+ */
+function getLastComment(issueName, filter) {
+ const allComments = listComments(issueName, filter);
+ for (let i = (allComments.length - 1); i > -1; i--) {
+ if (allComments[i].type != COMMENT_TYPE_DESCRIPTION) {
+ return allComments[i];
+ }
+ }
+ return null;
+}
+
+/**
+ * Checks if the given label exists in the issue.
+ * @param {Issue} issue The issue to search within for the label.
+ * @param {string} label The label to search for.
+ * @return {boolean}
+ */
+function hasLabel(issue, label) {
+ if (issue.labels) {
+ const testLabel = label.toLowerCase();
+ return issue.labels.some(({label}) => testLabel === label.toLowerCase());
+ }
+ return false;
+}
+
+/**
+ * Checks if the issue has any labels matching the given regex.
+ * @param {Issue} issue The issue to search within for matching labels.
+ * @param {string} regex The regex pattern to use to search for labels.
+ * @return {boolean}
+ */
+function hasLabelMatching(issue, regex) {
+ if (issue.labels) {
+ const re = new RegExp(regex, 'i');
+ return issue.labels.some(({label}) => re.test(label));
+ }
+ return false;
+}
+
+/**
+ * Returns all labels in the issue that match the given regex.
+ * @param {Issue} issue The issue to search within for matching labels.
+ * @param {string} regex The regex pattern to use to search for labels.
+ * @return {Array<string>}
+ */
+function getLabelsMatching(issue, regex) {
+ const labels = [];
+ if (issue.labels) {
+ const re = new RegExp(regex, 'i');
+ for (let i = 0; i < issue.labels.length; i++) {
+ if (re.test(issue.labels[i].label)) {
+ labels.push(issue.labels[i].label);
+ }
+ }
+ }
+ return labels;
+}
+
+/**
+ * Get the comment where the given label was added, if any.
+ * @param {string} issueName The resource name of the issue.
+ * @param {string} label The label that was remove.
+ * @return {Comment}
+ */
+function getLabelSetComment(issueName, label) {
+ const comments = listComments(issueName);
+ for (let i = 0; i < comments.length; i++) {
+ const comment = comments[i];
+ if (comment.amendments) {
+ for (let j = 0; j < comment.amendments.length; j++) {
+ const amendment = comment.amendments[j];
+ if (amendment['fieldName'] === 'Labels' &&
+ amendment['newOrDeltaValue'].toLowerCase() === (
+ label.toLocaleLowerCase())) {
+ return comment;
+ }
+ }
+ }
+ }
+ return null;
+}
+
+/**
+ * Get the comment where the given label was removed, if any.
+ * @param {string} issueName The resource name of the issue.
+ * @param {string} label The label that was remove.
+ * @return {Comment}
+ */
+function getLabelRemoveComment(issueName, label) {
+ const comments = listComments(issueName);
+ for (let i = 0; i < comments.length; i++) {
+ const comment = comments[i];
+ if (comment.amendments) {
+ for (let j = 0; j < comment.amendments.length; j++) {
+ const amendment = comment.amendments[j];
+ if (amendment['fieldName'] === 'Labels' &&
+ amendment[
+ 'newOrDeltaValue'].toLowerCase() === (
+ '-' + label.toLocaleLowerCase())) {
+ return comment;
+ }
+ }
+ }
+ }
+ return null;
+}
+
+/**
+ * Updates the issue to have the given label added.
+ * This method does not call Monorail's API to save this change.
+ * Call saveChanges() to send all updates to Monorail.
+ * @param {Issue} issue The issue to update.
+ * @param {string} label The label to add.
+ */
+function addLabel(issue, label) {
+ if (hasLabel(issue, label)) return;
+ maybeCreateDelta_(issue);
+ // Add the label to the issue's delta.labelsAdd.
+ issue.delta.labelsAdd.push(label);
+ // Add the label to the issue.
+ issue.labels.push({label: label});
+ // 'labels' added to updateMask in saveChanges().
+}
+
+/**
+ * Updates the issue to have the given label removed from the issue.
+ * This method does not call Monorail's API to save this change.
+ * Call saveChanges() to send all updates to Monorail.
+ * @param {Issue} issue The issue to update.
+ * @param {string} label The label to remove.
+ */
+function removeLabel(issue, label) {
+ if (!hasLabel(issue, label)) return;
+ maybeCreateDelta_(issue);
+ // Add the label to the issue's delta.labelsRemove.
+ issue.delta.labelsRemove.push(label);
+ // Remove label from issue.
+ for (let i = 0; i < issue.labels.length; i++) {
+ if (issue.labels[i].label.toLowerCase() === label.toLowerCase()) {
+ issue.labels.splice(i, 1);
+ break;
+ }
+ }
+}
+
+/**
+ * Sets the owner of the given issue.
+ * This method does not call Monorail's API to save this change.
+ * Call saveChanges() to send all updates to Monorail.
+ * @param {Issue} issue Issue to change.
+ * @param {string} ownerName The resource name of the new owner,
+ * e.g. 'users/chicken@email.com'
+*/
+function setOwner(issue, ownerName) {
+ maybeCreateDelta_(issue);
+ issue.owner = {'user': ownerName};
+ if (issue.delta.updateMask.indexOf('owner.user') === -1) {
+ issue.delta.updateMask.push('owner.user');
+ }
+}
+
+/**
+ * Sets the summary of the given issue.
+ * This method does not call Monorail's API to save this change.
+ * Call saveChanges() to send all updates to Monorail.
+ * @param {Issue} issue Issue to change.
+ * @param {string} summary The new summary of the issue.
+*/
+function setSummary(issue, summary) {
+ maybeCreateDelta_(issue);
+ issue.summary = summary;
+ if (issue.delta.updateMask.indexOf('summary') === -1) {
+ issue.delta.updateMask.push('summary');
+ }
+}
+
+/**
+ *Sets the status of the given issue.
+ * This method does not call Monorail's API to save this change.
+ * Call saveChanges() to send all updates to Monorail.
+ * @param {Issue} issue Issue to change.
+ * @param {string} status The new status of the issue e.g. 'Available'.
+*/
+function setStatus(issue, status) {
+ maybeCreateDelta_(issue);
+ issue.status.status = status;
+ if (issue.delta.updateMask.indexOf('status.status') === -1) {
+ issue.delta.updateMask.push('status.status');
+ }
+}
+
+/**
+ * Sets the merged into issue for the given issue.
+ * This method does not call Monorail's API to save this change.
+ * Call saveChanges() to send all updates to Monorail.
+ * @param {Issue} issue Issue to change.
+ * @param {IssueRef} mergedIntoRef IssueRef of the issue to merge into.
+ */
+function setMergedInto(issue, mergedIntoRef) {
+ maybeCreateDelta_(issue);
+ issue.mergedIntoIssueRef = mergedIntoRef;
+ if (issue.delta.updateMask.indexOf('mergedIntoIssueRef') === -1) {
+ issue.delta.updateMask.push('mergedIntoIssueRef');
+ }
+}
+
+/**
+ * Checks if target is found in source.
+ * @param {IssueRef} target The IssueRef to look for.
+ * @param {Array<IssueRef>} source the IssueRefs to look in.
+ * @return {number} index of target in source, -1 if not found.
+ */
+function issueRefExists_(target, source) {
+ for (let i = 0; i < source.length; i++) {
+ if ((source[i].issue === target.issue || (!source[i].issue && !target.issue)
+ ) && (source[i].extIdentifier === target.extIdentifier || (
+ !source[i].extIdentifier && !target.extIdentifier))) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+/**
+ * Makes blocking issue ref changes.
+ * blockingIssuesAdd are added before blockingIssuesRemove are removed.
+ * This method does not call Monorail's API to save this change.
+ * Call saveChanges() to send all updates to Monorail.
+ * @param {Issue} issue Issue to change.
+ * @param {Array<IssueRef>} blockingIssuesAdd issues to add as blocking issues.
+ * @param {Array<IssueRef>} blockingIssuesRemove issues to remove from blocking
+ * issues.
+ */
+function addBlockingIssueChanges(
+ issue, blockingIssuesAdd, blockingIssuesRemove) {
+ maybeCreateDelta_(issue);
+ blockingIssuesAdd.forEach((addRef) => {
+ const iInIssue = issueRefExists_(addRef, issue.blockingIssueRefs);
+ if (iInIssue === -1) { // addRef not found in issue
+ issue.blockingIssueRefs.push(addRef);
+ issue.delta.blockingAdd.push(addRef);
+ const iInDeltaRemove = issueRefExists_(
+ addRef, issue.delta.blockingRemove);
+ if (iInDeltaRemove != -1) {
+ // Remove addRef from blckingRemove that may have been added earlier.
+ issue.delta.blockingRemove.splice(iInDeltaRemove, 1);
+ }
+ // issue.delta.updateMask is updated in saveChanges()
+ }
+ });
+ // Add blockingIssuesAdd to issue and issue.delta.blockingAdd if not in
+ // issue.blockingIssues
+ blockingIssuesRemove.forEach((removeRef) => {
+ const iInIssue = issueRefExists_(removeRef, issue.blockingIssueRefs);
+ if (iInIssue > -1) {
+ issue.blockingIssueRefs.splice(iInIssue, 1);
+ issue.delta.blockingRemove.push(removeRef);
+ const iInDeltaAdd = issueRefExists_(removeRef, issue.delta.blockingAdd);
+ if (iInDeltaAdd != -1) {
+ issue.delta.blockingAdd.splice(iInDeltaAdd, 1);
+ }
+ }
+ });
+}
+
+/**
+ * Makes blocked-on issue ref changes.
+ * blockedOnIssuesAdd are added before blockedOnIssuesRemove are removed.
+ * This method does not call Monorail's API to save this change.
+ * Call saveChanges() to send all updates to Monorail.
+ * @param {Issue} issue Issue to change.
+ * @param {Array<IssueRef>} blockedOnIssuesAdd issues to add as blockedon
+ * issues.
+ * @param {Array<IssueRef>} blockedOnIssuesRemove issues to remove from
+ * blockedon issues.
+ */
+function addBlockedOnIssueChanges(
+ issue, blockedOnIssuesAdd, blockedOnIssuesRemove) {
+ maybeCreateDelta_(issue);
+ blockedOnIssuesAdd.forEach((addRef) => {
+ const iInIssue = issueRefExists_(addRef, issue.blockedOnIssueRefs);
+ if (iInIssue === -1) { // addRef not found in issue
+ issue.blockedOnIssueRefs.push(addRef);
+ issue.delta.blockedOnAdd.push(addRef);
+ const iInDeltaRemove = issueRefExists_(
+ addRef, issue.delta.blockedOnRemove);
+ if (iInDeltaRemove != -1) {
+ // Remove addRef from blckingRemove that may have been added earlier.
+ issue.delta.blockedOnRemove.splice(iInDeltaRemove, 1);
+ }
+ // issue.delta.updateMask is updated in saveChanges()
+ }
+ });
+ // Add blockedOnIssuesAdd to issue and issue.delta.blockedOnAdd if not in
+ // issue.blockedOnIssues.
+ blockedOnIssuesRemove.forEach((removeRef) => {
+ const iInIssue = issueRefExists_(removeRef, issue.blockedOnIssueRefs);
+ if (iInIssue > -1) {
+ issue.blockedOnIssueRefs.splice(iInIssue, 1);
+ issue.delta.blockedOnRemove.push(removeRef);
+ const iInDeltaAdd = issueRefExists_(removeRef, issue.delta.blockedOnAdd);
+ if (iInDeltaAdd != -1) {
+ issue.delta.blockedOnAdd.splice(iInDeltaAdd, 1);
+ }
+ }
+ });
+}
+
+
+/**
+ * Looks for a component name in an Array of ComponentValues.
+ * @param {string} compName Resource name of the Component to look for.
+ * @param {Array<ComponentValue>} compArray List of ComponentValues.
+ * @return {number} Index of compName in compArray, -1 if not found.
+ */
+function componentExists_(compName, compArray) {
+ for (let i = 0; i < compArray.length; i++) {
+ if (compArray[i].component === compName) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+/**
+ * Adds the component changes to the issue.
+ * componentNamesAdd are added before componentNamesremove are removed.
+ * This method does not call Monorail's API to save this change.
+ * Call saveChanges() to send all updates to Monorail.
+ * @param {Issue} issue Issue to change.
+ * @param {Array<string>} componentNamesAdd Array of component resource names.
+ * @param {Array<string>} componentNamesRemove Array or component resource
+ * names.
+
+*/
+function addComponentChanges(issue, componentNamesAdd, componentNamesRemove) {
+ maybeCreateDelta_(issue);
+ componentNamesAdd.forEach((compName) => {
+ const iInIssue = componentExists_(compName, issue.components);
+ if (iInIssue === -1) { // compName is not in issue.
+ issue.components.push({'component': compName});
+ issue.delta.componentsAdd.push(compName);
+ const iInDeltaRemove = issue.delta.componentsRemove.indexOf(compName);
+ if (iInDeltaRemove != -1) {
+ // Remove compName from issue.delta.componentsRemove that may have been
+ // added before.
+ issue.delta.componentsRemove.splice(iInDeltaRemove, 1);
+ }
+ // issue.delta.updateMask is updated in saveChanges()
+ }
+ });
+
+ componentNamesRemove.forEach((compName) => {
+ const iInIssue = componentExists_(compName, issue.components);
+ if (iInIssue != -1) { // compName was found in issue.
+ issue.components.splice(iInIssue, 1);
+ issue.delta.componentsRemove.push(compName);
+ const iInDeltaAdd = issue.delta.componentsAdd.indexOf(compName);
+ if (iInDeltaAdd != -1) {
+ // Remove compName from issue.delta.componentsAdd that may have been
+ // added before.
+ issue.delta.componentsAdd.splice(iInDeltaAdd, 1);
+ }
+ }
+ });
+}
+
+/**
+ * Checks if the fieldVal is found in fieldValsArray
+ * @param {FieldValue} fieldVal the field to look for.
+ * @param {Array<FieldValue>} fieldValsArray the Array to look within.
+ * @return {number} the index of fieldVal in fieldValsArray, or -1 if not found.
+ */
+function fieldValueExists_(fieldVal, fieldValsArray) {
+ for (let i = 0; i < fieldValsArray.length; i++) {
+ const currFv = fieldValsArray[i];
+ if (currFv.field === fieldVal.field && currFv.value === fieldVal.value && (
+ currFv.phase === fieldVal.phase || (
+ !currFv.phase && !fieldVal.phase))) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+/**
+ * Adds the FieldValue changes to the issue.
+ * fieldValuesAdd are added before fieldValuesRemove are removed.
+ * This method does not call Monorail's API to save this change.
+ * Call saveChanges() to send all updates to Monorail.
+ * @param {Issue} issue Issue to change.
+ * @param {Array<FieldValue>} fieldValuesAdd Array of FieldValues to add.
+ * @param {Array<FieldValue>} fieldValuesRemove Array of FieldValues to remove.
+*/
+function addFieldValueChanges(issue, fieldValuesAdd, fieldValuesRemove) {
+ maybeCreateDelta_(issue);
+ fieldValuesAdd.forEach((fvAdd) => {
+ const iInIssue = fieldValueExists_(fvAdd, issue.fieldValues);
+ if (iInIssue === -1) { // fvAdd is not already in issue, so we can add it.
+ issue.fieldValues.push(fvAdd);
+ issue.delta.fieldValuesAdd.push(fvAdd);
+ const iInDeltaRemove = fieldValueExists_(
+ fvAdd, issue.delta.fieldValuesRemove);
+ if (iInDeltaRemove != -1) {
+ // fvAdd was added to fieldValuesRemove in a previous call.
+ issue.delta.fieldValuesRemove.splice(iInDeltaRemove, 1);
+ }
+ // issue.delta.updateMask is updated in saveChanges()
+ }
+ });
+ // issue.delta.updateMask is updated in saveChanges()
+ fieldValuesRemove.forEach((fvRemove) => {
+ const iInIssue = fieldValueExists_(fvRemove, issue.fieldValues);
+ if (iInIssue != -1) { // fvRemove is in issue, so we can remove it.
+ issue.fieldValues.splice(iInIssue, 1);
+ issue.delta.fieldValuesRemove.push(fvRemove);
+ const iInDeltaAdd = fieldValueExists_(
+ fvRemove, issue.delta.fieldValuesAdd);
+ if (iInDeltaAdd != -1) {
+ // fvRemove was added to fieldValuesAdd in a previous call.
+ issue.delta.fieldValuesAdd.splice(iInDeltaAdd, 1);
+ }
+ }
+ });
+}
+
+/**
+ * Checks for the existence of userName in userValues
+ * @param {string} userName A user resource name to look for.
+ * @param {Array<UserValue>} userValues UserValues to search through.
+ * @return {number} Index of userName's UserValue in userValues or -1 if not
+ * found.
+ */
+function userValueExists_(userName, userValues) {
+ for (let i = 0; i< userValues.length; i++) {
+ if (userValues[i].user === userName) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+/**
+ * Adds the CC changes to the issue.
+ * ccNamesAdd are added before ccNamesRemove are removed.
+ * This method does not call Monorail's API to save this change.
+ * Call saveChanges() to send all updates to Monorail.
+ * @param {Issue} issue Issue to change.
+ * @param {Array<string>} ccNamesAdd Array if user resource names.
+ * @param {Array<string>} ccNamesRemove Array if user resource names.
+*/
+function addCcChanges(issue, ccNamesAdd, ccNamesRemove) {
+ maybeCreateDelta_(issue);
+ ccNamesAdd.forEach((ccName) => {
+ const iInIssue = userValueExists_(ccName, issue.ccUsers);
+ if (iInIssue === -1) { // User is not in issue, so we can add them.
+ issue.ccUsers.push({'user': ccName});
+ issue.delta.ccsAdd.push(ccName);
+ const iInDeltaRemove = issue.delta.ccsRemove.indexOf(ccName);
+ if (iInDeltaRemove != -1) {
+ // ccName was added to ccsRemove in a previous call.
+ issue.delta.ccsRemove.splice(iInDeltaRemove, 1);
+ }
+ }
+ });
+ ccNamesRemove.forEach((ccName) => {
+ const iInIssue = userValueExists_(ccName, issue.ccUsers);
+ if (iInIssue != -1) { // User is in issue, so we can remove it.
+ issue.ccUsers.splice(iInIssue, 1);
+ issue.delta.ccsRemove.push(ccName);
+ const iInDeltaAdd = issue.delta.ccsAdd.indexOf(ccName);
+ if (iInDeltaAdd != -1) {
+ // ccName was added to delta.ccsAdd in a previous all.
+ issue.delta.ccsAdd.splice(iInDeltaAdd, 1);
+ }
+ }
+ });
+}
+
+/**
+ * Set the pending comment of the issue.
+ * @param {Issue} issue Issue whose comment we want to set.
+ * @param {string} comment Comment that we want for the issue.
+ */
+function setComment(issue, comment) {
+ maybeCreateDelta_(issue);
+ issue.delta.comment = comment;
+}
+
+/**
+ * Get the pending comment for the issue.
+ * @param {Issue} issue Issue whose comment we want.
+ * @return {string}
+ */
+function getPendingComment(issue) {
+ if (issue.delta) {
+ return issue.delta.comment;
+ }
+ return '';
+}
+
+/**
+ * Adds to the existing pending comment
+ * @param {Issue} issue Issue to update.
+ * @param {string} comment The comment string to add to the existing one.
+ */
+function appendComment(issue, comment) {
+ maybeCreateDelta_(issue);
+ issue.delta.comment = issue.delta.comment.concat(comment);
+}
+
+/**
+ * Sets up an issue for pending changes.
+ * @param {Issue} issue The issue that needs to be updated.
+ */
+function maybeCreateDelta_(issue) {
+ if (!issue.delta) {
+ issue.delta = newIssueDelta_();
+ if (!issue.components) {
+ issue.components = [];
+ };
+ if (!issue.blockingIssueRefs) {
+ issue.blockingIssueRefs = [];
+ }
+ if (!issue.blockedOnIssueRefs) {
+ issue.blockedOnIssueRefs = [];
+ }
+ if (!issue.ccUsers) {
+ issue.ccUsers = [];
+ }
+ if (!issue.labels) {
+ issue.labels = [];
+ }
+ if (!issue.fieldValues) {
+ issue.fieldValues = [];
+ }
+ }
+}
+
+/**
+ * Creates an IssueDelta
+ * @return {IssueDelta_}
+ */
+function newIssueDelta_() {
+ return new IssueDelta_();
+}
+
+/** Used to track pending changes to an issue.*/
+function IssueDelta_() {
+ /** Array<string> */ this.updateMask = [];
+
+ // User resource names.
+ /** Array<string> */ this.ccsRemove = [];
+ /** Array<string> */ this.ccsAdd = [];
+
+ /** Array<IssueRef> */ this.blockedOnRemove = [];
+ /** Array<IssueRef> */ this.blockedOnAdd = [];
+ /** Array<IssueRef> */ this.blockingRemove = [];
+ /** Array<IssueRef> */ this.blockingAdd = [];
+
+ // Component resource names.
+ /** Array<string> */ this.componentsRemove = [];
+ /** Array<string> */ this.componentsAdd = [];
+
+ // Label values, e.g. 'Security-Notify'.
+ /** Array<string> */ this.labelsRemove = [];
+ /** Array<string> */ this.labelsAdd = [];
+
+ /** Array<FieldValue> */ this.fieldValuesRemove = [];
+ /** Array<FieldValue> */ this.fieldValuesAdd = [];
+
+ this.comment = '';
+}
+
+/**
+ * Calls Monorail's API to update the issue.
+ * @param {Issue} issue The issue to update where issue['delta'] is expected
+ * to exist.
+ * @param {boolean} sendEmail True if the update should trigger email
+ * notifications.
+ * @return {Issue}
+ */
+function saveChanges(issue, sendEmail) {
+ if (!issue.delta) {
+ throw new Error('No pending changes for issue.');
+ }
+
+ const modifyDelta = {
+ 'ccsRemove': issue.delta.ccsRemove,
+ 'blockedOnIssuesRemove': issue.delta.blockedOnRemove,
+ 'blockingIssuesRemove': issue.delta.blockingRemove,
+ 'componentsRemove': issue.delta.componentsRemove,
+ 'labelsRemove': issue.delta.labelsRemove,
+ 'fieldValsRemove': issue.delta.fieldValuesRemove,
+ 'issue': {
+ 'name': issue.name,
+ 'fieldValues': issue.delta.fieldValuesAdd,
+ 'blockedOnIssueRefs': issue.delta.blockedOnAdd,
+ 'blockingIssueRefs': issue.delta.blockingAdd,
+ 'mergedIntoIssueRef': issue.mergedIntoIssueRef,
+ 'summary': issue.summary,
+ 'status': issue.status,
+ 'owner': issue.owner,
+ 'labels': [],
+ 'ccUsers': [],
+ 'components': [],
+ },
+ };
+
+ if (issue.delta.fieldValuesAdd.length > 0) {
+ issue.delta.updateMask.push('fieldValues');
+ }
+
+ if (issue.delta.blockedOnAdd.length > 0) {
+ issue.delta.updateMask.push('blockedOnIssueRefs');
+ }
+
+ if (issue.delta.blockingAdd.length > 0) {
+ issue.delta.updateMask.push('blockingIssueRefs');
+ }
+
+ if (issue.delta.ccsAdd.length > 0) {
+ issue.delta.updateMask.push('ccUsers');
+ }
+ issue.delta.ccsAdd.forEach((userResourceName) => {
+ modifyDelta.issue['ccUsers'].push({'user': userResourceName});
+ });
+
+ if (issue.delta.labelsAdd.length > 0) {
+ issue.delta.updateMask.push('labels');
+ }
+ issue.delta.labelsAdd.forEach((label) => {
+ modifyDelta.issue['labels'].push({'label': label});
+ });
+
+ if (issue.delta.componentsAdd.length > 0) {
+ issue.delta.updateMask.push('components');
+ }
+ issue.delta.componentsAdd.forEach((compResourceName) => {
+ modifyDelta.issue['components'].push({'component': compResourceName});
+ });
+
+ modifyDelta['updateMask'] = issue.delta.updateMask.join();
+
+ const message = {
+ 'deltas': [modifyDelta],
+ 'notifyType': sendEmail ? 'EMAIL' : 'NO_NOTIFICATION',
+ 'commentContent': issue.delta.comment,
+ };
+
+ const url = URL + 'monorail.v3.Issues/ModifyIssues';
+ response = run_(url, message);
+ if (!response.issues) {
+ Logger.log('All changes Noop');
+ return null;
+ }
+ issue = response.issues[0];
+ return issue;
+}
+
+/**
+ * Creates an Issue.
+ * @param {string} projectName: Resource name of the parent project.
+ * @param {string} summary: Summary of the issue.
+ * @param {string} description: Description of the issue.
+ * @param {string} status: Status of the issue, e.g. "Untriaged".
+ * @param {boolean} sendEmail: True if this should trigger email notifications.
+ * @param {string=} ownerName: Resource name of the issue owner.
+ * @param {Array<string>=} ccNames: Resource names of the users to cc.
+ * @param {Array<string>=} labels: Labels to add to the issue,
+ * e.g. "Restict-View-Google".
+ * @param {Array<string>=} componentNames: Resource names of components to add.
+ * @param {Array<FieldValue>=} fieldValues: FieldValues to add to the issue.
+ * @param {Array<IssueRef>=} blockedOnRefs: IssueRefs for blocked on issues.
+ * @param {Array<IssueRef>=} blockingRefs: IssueRefs for blocking issues.
+ * @return {Issue}
+ */
+function makeIssue(
+ projectName, summary, description, status, sendEmail, ownerName, ccNames,
+ labels, componentNames, fieldValues, blockedOnRefs, blockingRefs) {
+ const issue = {
+ 'summary': summary,
+ 'status': {'status': status},
+ 'ccUsers': [],
+ 'components': [],
+ 'labels': [],
+ };
+
+ if (ownerName) {
+ issue['owner'] = {'user': ownerName};
+ }
+
+ if (ccNames) {
+ ccNames.forEach((ccName) => {
+ issue['ccUsers'].push({'user': ccName});
+ });
+ };
+
+ if (labels) {
+ labels.forEach((label) => {
+ issue['labels'].push({'label': label});
+ });
+ };
+
+ if (componentNames) {
+ componentNames.forEach((componentName) => {
+ issue['components'].push({'component': componentName});
+ });
+ };
+
+ if (fieldValues) {
+ issue['fieldValues'] = fieldValues;
+ };
+
+ if (blockedOnRefs) {
+ issue['blockedOnIssueRefs'] = blockedOnRefs;
+ };
+
+ if (blockingRefs) {
+ issue['blockingIssueRefs'] = blockingRefs;
+ };
+
+ const message = {
+ 'parent': projectName,
+ 'issue': issue,
+ 'description': description,
+ 'notifyType': sendEmail ? 'EMAIL': 'NO_NOTIFICATION',
+ };
+ const url = URL + 'monorail.v3.Issues/MakeIssue';
+ return run_(url, message);
+}
diff --git a/api/v3/apps-script-client/ProjectService.js b/api/v3/apps-script-client/ProjectService.js
new file mode 100644
index 0000000..1487a53
--- /dev/null
+++ b/api/v3/apps-script-client/ProjectService.js
@@ -0,0 +1,52 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/* eslint-disable no-unused-vars */
+
+/**
+ * Creates a ComponentDef.
+ * @param {string} projectName The resource name of the parent project.
+ * @param {string} value The name of the component
+ * e.g. "Triage" or "Triage>Security".
+ * @param {string=} docstring Short description of the ComponentDef.
+ * @param {Array<string>=} admins Array of User resource names to set as admins.
+ * @param {Array<string>=} ccs Array of User resources names to set as auto-ccs.
+ * @param {Array<string>=} labels Array of labels.
+ * @return {ComponentDef}
+ */
+function createComponentDef(
+ projectName, value, docstring, admins, ccs, labels) {
+ const componentDef = {
+ 'value': value,
+ 'docstring': docstring,
+ };
+ if (admins) {
+ componentDef['admins'] = admins;
+ }
+ if (ccs) {
+ componentDef['ccs'] = ccs;
+ }
+ if (labels) {
+ componentDef['labels'] = labels;
+ }
+ const message = {
+ 'parent': projectName,
+ 'componentDef': componentDef,
+ };
+ const url = URL + 'monorail.v3.Projects/CreateComponentDef';
+ return run_(url, message);
+}
+
+/**
+ * Deletes a ComponentDef.
+ * @param {string} componentName Resource name of the ComponentDef to delete.
+ * @return {EmptyProto}
+ */
+function deleteComponentDef(componentName) {
+ const message = {
+ 'name': componentName,
+ };
+ const url = URL + 'monorail.v3.Projects/DeleteComponentDef';
+ return run_(url, message);
+}
diff --git a/api/v3/apps-script-client/README.md b/api/v3/apps-script-client/README.md
new file mode 100644
index 0000000..a15a5f9
--- /dev/null
+++ b/api/v3/apps-script-client/README.md
@@ -0,0 +1,8 @@
+## This directory contains code that make up our v3 Apps Script client library.
+
+client.js is purposely omitted.
+
+To make updates to the library:
+1) Update the code here and send in a CL for review.
+2) Merge the Cl and copy-paste the changes into Apps Script at go/monorail-v3-apps-script.
+3) Create a new static version in Apps Script and update the labeled version 'latest' to point to the new static version.
diff --git a/api/v3/apps-script-client/UserService.js b/api/v3/apps-script-client/UserService.js
new file mode 100644
index 0000000..6402db5
--- /dev/null
+++ b/api/v3/apps-script-client/UserService.js
@@ -0,0 +1,27 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/* eslint-disable no-unused-vars */
+
+/**
+ * Fetches the user from Monorai.
+ * @param {string} userName The resource name of the user.
+ * @return {User}
+ */
+function getUser(userName) {
+ const message = {'name': userName};
+ const url = URL + 'monorail.v3.Users/GetUser';
+ return run_(url, message);
+}
+
+/**
+ * Fetches the users from Monorail.
+ * @param {Array<string>} userNames The resource names of the users.
+ * @return {Array<User>}
+ */
+function batchGetUsers(userNames) {
+ const message = {'names': userNames};
+ const url = URL + 'monorail.v3.Users/BatchGetUsers';
+ return run_(url, message);
+}
diff --git a/api/v3/apps-script-client/helpers.js b/api/v3/apps-script-client/helpers.js
new file mode 100644
index 0000000..05ee920
--- /dev/null
+++ b/api/v3/apps-script-client/helpers.js
@@ -0,0 +1,82 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/* eslint-disable no-unused-vars */
+
+/**
+ * Returns the user's resource name.
+ * @param {string|number} user The user's email or user_id
+ * @return {string}
+ */
+function computeUserName(user) {
+ return `users/${user}`;
+}
+
+/**
+ * Returns the users' resource names.
+ * @param {Array<string|number>} users Array of user emails/user_ids.
+ * @return {Array<string>}
+ */
+function computeUserNames(users) {
+ const userNames = [];
+ users.forEach((user) => {
+ userNames.push(computeUserName(user));
+ });
+ return userNames;
+}
+
+
+/**
+ * Returns the issue's resource name.
+ * @param {string} project The name of the project the issue belongs to,
+ * e.g. 'chromium'.
+ * @param {number} id The issue's id.
+ * @return {string}
+ */
+function computeIssueName(project, id) {
+ return `projects/${project}/issues/${id}`;
+}
+
+/**
+ * Returns the project's resource name.
+ * @param {string} project The display name of the project, e.g. 'chromium'.
+ * @return {string}
+ */
+function computeProjectName(project) {
+ return `projects/${project}`;
+}
+
+/**
+ * Returns the projects' resource names in the same order.
+ * @param {Array<string>} projects The display names of the projects,
+ * e.g. 'chromium'.
+ * @return {Array<string>}
+ */
+function computeProjectNames(projects) {
+ const projectNames = [];
+ projects.forEach((project) => {
+ projectNames.push(computeProjectName(project));
+ });
+ return projectNames;
+}
+
+/**
+ * Returns the FieldDef's resource name.
+ * @param {string} project The display name of the project, e.g. 'chromium'.
+ * @param {number} fieldId ID of the FieldDef.
+ * @return {string}
+ */
+function computeFieldDefName(project, fieldId) {
+ return `projects/${project}/fieldDefs/${fieldId}`;
+}
+
+/**
+ * Returns the ComponentDef's resource name.
+ * @param {string} project The display name of the project, e.g. 'chromium'.
+ * @param {number|string} componentIdOrPath ID or value of the ComponentDef.
+ * @return {string}
+*/
+function computeComponentDefName(project, componentIdOrPath) {
+ return `projects/${project}/componentDefs/${componentIdOrPath}`;
+}
diff --git a/api/v3/apps-script-client/types.js b/api/v3/apps-script-client/types.js
new file mode 100644
index 0000000..cafba21
--- /dev/null
+++ b/api/v3/apps-script-client/types.js
@@ -0,0 +1,75 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/* eslint-disable max-len */
+
+/**
+ * The label of an issue.
+ * @typedef {Object} LabelValue
+ * @property {string} label - the string label. e.g. 'Target-99'.
+ * @property {string} derivation - How the label was derived. One of 'EXPLICIT', 'RULE'.
+ */
+
+/**
+ * A user involved in an issue.
+ * @typedef {Object} UserValue
+ * @property {string} user - The User resource name.
+ * @property {string} derivation - How the user was derived. One of 'EXPLICIT', 'RULE'.
+ */
+
+/**
+ * A component involved in an issue.
+ * @typedef {Object} ComponentValue
+ * @property {string} component - The ComponentDef resource name.
+ * @property {string} derivation - How the component was derived. One of 'EXPLICIT', 'RULE'.
+ */
+
+/**
+ * A field involved in an issue.
+ * @typedef {Object} FieldValue
+ * @property {string} field - The FieldDef resource name.
+ * @property {string} value - The value associated with the field.
+ * @property {string} derivation - How the value was derived. One of 'EXPLICIT', 'RULE'.
+ * @property {string} phase - The phase of an issue that this value belongs to, if any.
+ */
+
+/**
+ * The status of an issue.
+ * @typedef {Object} StatusValue
+ * @property {string} status - The status. e.g. 'Available'.
+ * @property {string} derivation - How the status was derived. One of 'EXPLICIT', 'RULE'.
+ */
+
+/**
+ * A reference to monorail or external issue.
+ * @typedef {Object} IssueRef
+ * @property {string} [issue] - The resource name of the issue.
+ * @property {string} [extIdentifier] - The identifier of an external issue e.g 'b/123'.
+ */
+
+/**
+ * An Issue.
+ * @typedef {Object} Issue
+ * @property {string} name - The resource name of the issue.
+ * @property {string} summary - The issue summary.
+ * @property {string} state - The current state of the issue. One of 'ACTIVE', 'DELETED', 'SPAM'.
+ * @property {string} reporter - The User resource name of the issue reporter.
+ * @property {UserValue} owner - The issue's owner.
+ * @property {StatusValue} status - The issue status.
+ * @property {IssueRef} mergedIntoIssueRef - The issue this issue is merged into.
+ * @property {Array<IssueRef>} blockedOnIssueRefs - TODO
+ * @property {Array<IssueRef>} blockingIssueRefs - TODO
+ * @property {Array<LabelValue>} labels - The labels of the issue.
+ * @property {Array<FieldValue>} fieldValues - TODO
+ * @property {Array<UserValue>} ccUsers - The users cc'd to this issue.
+ * @property {Array<ComponentValue>} components - The Components added to the issue.
+ * @property {Number} attachmentCount - The number of attachments this issue holds.
+ * @property {Number} starCount - The number of stars this issue has.
+ * @property {Array<FieldValue>} fieldValues - The field values of the issue.
+ * @property {Array<string>} phases - The names of all Phases in this issue.
+ * @property {Object} delta - Holds the pending changes that will be applied with SaveChanges().
+ */
+// TODO(crbug.com/monorail/6456): createTime, closeTime, modifyTime, componentModifyTime, statusModifyTime, ownerModifyTime
+
+// TODO(crbug.com/monorail/6456): Add other classes.
diff --git a/api/v3/converters.py b/api/v3/converters.py
new file mode 100644
index 0000000..60aebd7
--- /dev/null
+++ b/api/v3/converters.py
@@ -0,0 +1,1979 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import collections
+import itertools
+import logging
+import time
+
+from google.protobuf import timestamp_pb2
+
+from api import resource_name_converters as rnc
+from api.v3.api_proto import feature_objects_pb2
+from api.v3.api_proto import issues_pb2
+from api.v3.api_proto import issue_objects_pb2
+from api.v3.api_proto import project_objects_pb2
+from api.v3.api_proto import user_objects_pb2
+
+from framework import exceptions
+from framework import filecontent
+from framework import framework_bizobj
+from framework import framework_constants
+from framework import framework_helpers
+from proto import tracker_pb2
+from project import project_helpers
+from tracker import attachment_helpers
+from tracker import field_helpers
+from tracker import tracker_bizobj as tbo
+from tracker import tracker_helpers
+
+Choice = project_objects_pb2.FieldDef.EnumTypeSettings.Choice
+
+# Ingest/convert dicts for ApprovalStatus.
+_V3_APPROVAL_STATUS = issue_objects_pb2.ApprovalValue.ApprovalStatus.Value
+_APPROVAL_STATUS_INGEST = {
+ _V3_APPROVAL_STATUS('APPROVAL_STATUS_UNSPECIFIED'): None,
+ _V3_APPROVAL_STATUS('NOT_SET'): tracker_pb2.ApprovalStatus.NOT_SET,
+ _V3_APPROVAL_STATUS('NEEDS_REVIEW'): tracker_pb2.ApprovalStatus.NEEDS_REVIEW,
+ _V3_APPROVAL_STATUS('NA'): tracker_pb2.ApprovalStatus.NA,
+ _V3_APPROVAL_STATUS('REVIEW_REQUESTED'):
+ tracker_pb2.ApprovalStatus.REVIEW_REQUESTED,
+ _V3_APPROVAL_STATUS('REVIEW_STARTED'):
+ tracker_pb2.ApprovalStatus.REVIEW_STARTED,
+ _V3_APPROVAL_STATUS('NEED_INFO'): tracker_pb2.ApprovalStatus.NEED_INFO,
+ _V3_APPROVAL_STATUS('APPROVED'): tracker_pb2.ApprovalStatus.APPROVED,
+ _V3_APPROVAL_STATUS('NOT_APPROVED'): tracker_pb2.ApprovalStatus.NOT_APPROVED,
+}
+_APPROVAL_STATUS_CONVERT = {
+ val: key for key, val in _APPROVAL_STATUS_INGEST.items()}
+
+
+class Converter(object):
+ """Class to manage converting objects between the API and backend layer."""
+
+ def __init__(self, mc, services):
+ # type: (MonorailContext, Services) -> Converter
+ """Create a Converter with the given MonorailContext and Services.
+
+ Args:
+ mc: MonorailContext object containing the MonorailConnection to the DB
+ and the requester's AuthData object.
+ services: Services object for connections to backend services.
+ """
+ self.cnxn = mc.cnxn
+ self.user_auth = mc.auth
+ self.services = services
+
+ # Hotlists
+
+ def ConvertHotlist(self, hotlist):
+ # type: (proto.feature_objects_pb2.Hotlist)
+ # -> api_proto.feature_objects_pb2.Hotlist
+ """Convert a protorpc Hotlist into a protoc Hotlist."""
+
+ hotlist_resource_name = rnc.ConvertHotlistName(hotlist.hotlist_id)
+ members_by_id = rnc.ConvertUserNames(
+ hotlist.owner_ids + hotlist.editor_ids)
+ default_columns = self._ComputeIssuesListColumns(hotlist.default_col_spec)
+ if hotlist.is_private:
+ hotlist_privacy = feature_objects_pb2.Hotlist.HotlistPrivacy.Value(
+ 'PRIVATE')
+ else:
+ hotlist_privacy = feature_objects_pb2.Hotlist.HotlistPrivacy.Value(
+ 'PUBLIC')
+
+ return feature_objects_pb2.Hotlist(
+ name=hotlist_resource_name,
+ display_name=hotlist.name,
+ owner=members_by_id.get(hotlist.owner_ids[0]),
+ editors=[
+ members_by_id.get(editor_id) for editor_id in hotlist.editor_ids
+ ],
+ summary=hotlist.summary,
+ description=hotlist.description,
+ default_columns=default_columns,
+ hotlist_privacy=hotlist_privacy)
+
+ def ConvertHotlists(self, hotlists):
+ # type: (Sequence[proto.feature_objects_pb2.Hotlist])
+ # -> Sequence[api_proto.feature_objects_pb2.Hotlist]
+ """Convert protorpc Hotlists into protoc Hotlists."""
+ return [self.ConvertHotlist(hotlist) for hotlist in hotlists]
+
+ def ConvertHotlistItems(self, hotlist_id, items):
+ # type: (int, Sequence[proto.features_pb2.HotlistItem]) ->
+ # Sequence[api_proto.feature_objects_pb2.Hotlist]
+ """Convert a Sequence of protorpc HotlistItems into a Sequence of protoc
+ HotlistItems.
+
+ Args:
+ hotlist_id: ID of the Hotlist the items belong to.
+ items: Sequence of HotlistItem protorpc objects.
+
+ Returns:
+ Sequence of protoc HotlistItems in the same order they are given in
+ `items`.
+ In the rare event that any issues in `items` are not found, they will be
+ omitted from the result.
+ """
+ issue_ids = [item.issue_id for item in items]
+ # Converting HotlistItemNames and IssueNames both require looking up the
+ # issues in the hotlist. However, we want to keep the code clean and
+ # readable so we keep the two processes separate.
+ resource_names_dict = rnc.ConvertHotlistItemNames(
+ self.cnxn, hotlist_id, issue_ids, self.services)
+ issue_names_dict = rnc.ConvertIssueNames(
+ self.cnxn, issue_ids, self.services)
+ adders_by_id = rnc.ConvertUserNames([item.adder_id for item in items])
+
+ # Filter out items whose issues were not found.
+ found_items = [
+ item for item in items if resource_names_dict.get(item.issue_id) and
+ issue_names_dict.get(item.issue_id)
+ ]
+ if len(items) != len(found_items):
+ found_ids = [item.issue_id for item in found_items]
+ missing_ids = [iid for iid in issue_ids if iid not in found_ids]
+ logging.info('HotlistItem issues %r not found' % missing_ids)
+
+ # Generate user friendly ranks (0, 1, 2, 3,...) that are exposed to API
+ # clients, instead of using padded ranks (1, 11, 21, 31,...).
+ sorted_ranks = sorted(item.rank for item in found_items)
+ friendly_ranks_dict = {
+ rank: friendly_rank for friendly_rank, rank in enumerate(sorted_ranks)
+ }
+
+ api_items = []
+ for item in found_items:
+ api_item = feature_objects_pb2.HotlistItem(
+ name=resource_names_dict.get(item.issue_id),
+ issue=issue_names_dict.get(item.issue_id),
+ rank=friendly_ranks_dict[item.rank],
+ adder=adders_by_id.get(item.adder_id),
+ note=item.note)
+ if item.date_added:
+ api_item.create_time.FromSeconds(item.date_added)
+ api_items.append(api_item)
+
+ return api_items
+
+ # Issues
+
+ def _ConvertComponentValues(self, issue):
+ # proto.tracker_pb2.Issue ->
+ # Sequence[api_proto.issue_objects_pb2.Issue.ComponentValue]
+ """Convert the status string on issue into a ComponentValue."""
+ component_values = []
+ component_ids = itertools.chain(
+ issue.component_ids, issue.derived_component_ids)
+ ids_to_names = rnc.ConvertComponentDefNames(
+ self.cnxn, component_ids, issue.project_id, self.services)
+
+ for component_id in issue.component_ids:
+ if component_id in ids_to_names:
+ component_values.append(
+ issue_objects_pb2.Issue.ComponentValue(
+ component=ids_to_names[component_id],
+ derivation=issue_objects_pb2.Derivation.Value(
+ 'EXPLICIT')))
+ for derived_component_id in issue.derived_component_ids:
+ if derived_component_id in ids_to_names:
+ component_values.append(
+ issue_objects_pb2.Issue.ComponentValue(
+ component=ids_to_names[derived_component_id],
+ derivation=issue_objects_pb2.Derivation.Value('RULE')))
+
+ return component_values
+
+ def _ConvertStatusValue(self, issue):
+ # proto.tracker_pb2.Issue -> api_proto.issue_objects_pb2.Issue.StatusValue
+ """Convert the status string on issue into a StatusValue."""
+ derivation = issue_objects_pb2.Derivation.Value(
+ 'DERIVATION_UNSPECIFIED')
+ if issue.status:
+ derivation = issue_objects_pb2.Derivation.Value('EXPLICIT')
+ else:
+ derivation = issue_objects_pb2.Derivation.Value('RULE')
+ return issue_objects_pb2.Issue.StatusValue(
+ status=issue.status or issue.derived_status, derivation=derivation)
+
+ def _ConvertAmendments(self, amendments, user_display_names):
+ # type: (Sequence[proto.tracker_pb2.Amendment], Mapping[int, str]) ->
+ # Sequence[api_proto.issue_objects_pb2.Comment.Amendment]
+ """Convert protorpc Amendments to protoc Amendments.
+
+ Args:
+ amendments: the amendments to convert
+ user_display_names: map from user_id to display name for all users
+ involved in the amendments.
+
+ Returns:
+ The converted amendments.
+ """
+ results = []
+ for amendment in amendments:
+ field_name = tbo.GetAmendmentFieldName(amendment)
+ new_value = tbo.AmendmentString_New(amendment, user_display_names)
+ results.append(
+ issue_objects_pb2.Comment.Amendment(
+ field_name=field_name,
+ new_or_delta_value=new_value,
+ old_value=amendment.oldvalue))
+ return results
+
+ def _ConvertAttachments(self, attachments, project_name):
+ # type: (Sequence[proto.tracker_pb2.Attachment], str) ->
+ # Sequence[api_proto.issue_objects_pb2.Comment.Attachment]
+ """Convert protorpc Attachments to protoc Attachments."""
+ results = []
+ for attach in attachments:
+ if attach.deleted:
+ state = issue_objects_pb2.IssueContentState.Value('DELETED')
+ size, thumbnail_uri, view_uri, download_uri = None, None, None, None
+ else:
+ state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
+ size = attach.filesize
+ download_uri = attachment_helpers.GetDownloadURL(attach.attachment_id)
+ view_uri = attachment_helpers.GetViewURL(
+ attach, download_uri, project_name)
+ thumbnail_uri = attachment_helpers.GetThumbnailURL(attach, download_uri)
+ results.append(
+ issue_objects_pb2.Comment.Attachment(
+ filename=attach.filename,
+ state=state,
+ size=size,
+ media_type=attach.mimetype,
+ thumbnail_uri=thumbnail_uri,
+ view_uri=view_uri,
+ download_uri=download_uri))
+ return results
+
+ def ConvertComments(self, issue_id, comments):
+ # type: (int, Sequence[proto.tracker_pb2.IssueComment])
+ # -> Sequence[api_proto.issue_objects_pb2.Comment]
+ """Convert protorpc IssueComments from issue into protoc Comments."""
+ issue = self.services.issue.GetIssue(self.cnxn, issue_id)
+ users_by_id = self.services.user.GetUsersByIDs(
+ self.cnxn, tbo.UsersInvolvedInCommentList(comments))
+ (user_display_names,
+ _user_display_emails) = framework_bizobj.CreateUserDisplayNamesAndEmails(
+ self.cnxn, self.services, self.user_auth, users_by_id.values())
+ comment_names_dict = rnc.CreateCommentNames(
+ issue.local_id, issue.project_name,
+ [comment.sequence for comment in comments])
+ approval_ids = [
+ comment.approval_id
+ for comment in comments
+ if comment.approval_id is not None # In case of a 0 approval_id.
+ ]
+ approval_ids_to_names = rnc.ConvertApprovalDefNames(
+ self.cnxn, approval_ids, issue.project_id, self.services)
+
+ converted_comments = []
+ for comment in comments:
+ if comment.is_spam:
+ state = issue_objects_pb2.IssueContentState.Value('SPAM')
+ elif comment.deleted_by:
+ state = issue_objects_pb2.IssueContentState.Value('DELETED')
+ else:
+ state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
+ comment_type = issue_objects_pb2.Comment.Type.Value('COMMENT')
+ if comment.is_description:
+ comment_type = issue_objects_pb2.Comment.Type.Value('DESCRIPTION')
+ converted_attachments = self._ConvertAttachments(
+ comment.attachments, issue.project_name)
+ converted_amendments = self._ConvertAmendments(
+ comment.amendments, user_display_names)
+ converted_comment = issue_objects_pb2.Comment(
+ name=comment_names_dict[comment.sequence],
+ state=state,
+ type=comment_type,
+ create_time=timestamp_pb2.Timestamp(seconds=comment.timestamp),
+ attachments=converted_attachments,
+ amendments=converted_amendments)
+ if comment.content:
+ converted_comment.content = comment.content
+ if comment.user_id:
+ converted_comment.commenter = rnc.ConvertUserName(comment.user_id)
+ if comment.inbound_message:
+ converted_comment.inbound_message = comment.inbound_message
+ if comment.approval_id and comment.approval_id in approval_ids_to_names:
+ converted_comment.approval = approval_ids_to_names[comment.approval_id]
+ converted_comments.append(converted_comment)
+ return converted_comments
+
+ def ConvertIssue(self, issue):
+ # type: (proto.tracker_pb2.Issue) -> api_proto.issue_objects_pb2.Issue
+ """Convert a protorpc Issue into a protoc Issue."""
+ issues = self.ConvertIssues([issue])
+ if len(issues) < 1:
+ raise exceptions.NoSuchIssueException()
+ if len(issues) > 1:
+ logging.warning('More than one converted issue returned: %s', issues)
+ return issues[0]
+
+ def ConvertIssues(self, issues):
+ # type: (Sequence[proto.tracker_pb2.Issue]) ->
+ # Sequence[api_proto.issue_objects_pb2.Issue]
+ """Convert protorpc Issues into protoc Issues."""
+ issue_ids = [issue.issue_id for issue in issues]
+ issue_names_dict = rnc.ConvertIssueNames(
+ self.cnxn, issue_ids, self.services)
+ found_issues = [
+ issue for issue in issues if issue.issue_id in issue_names_dict
+ ]
+ converted_issues = []
+ for issue in found_issues:
+ status = self._ConvertStatusValue(issue)
+ content_state = issue_objects_pb2.IssueContentState.Value(
+ 'STATE_UNSPECIFIED')
+ if issue.is_spam:
+ content_state = issue_objects_pb2.IssueContentState.Value('SPAM')
+ elif issue.deleted:
+ content_state = issue_objects_pb2.IssueContentState.Value('DELETED')
+ else:
+ content_state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
+
+ owner = None
+ # Explicit values override values derived from rules.
+ if issue.owner_id:
+ owner = issue_objects_pb2.Issue.UserValue(
+ derivation=issue_objects_pb2.Derivation.Value('EXPLICIT'),
+ user=rnc.ConvertUserName(issue.owner_id))
+ elif issue.derived_owner_id:
+ owner = issue_objects_pb2.Issue.UserValue(
+ derivation=issue_objects_pb2.Derivation.Value('RULE'),
+ user=rnc.ConvertUserName(issue.derived_owner_id))
+
+ cc_users = []
+ for cc_user_id in issue.cc_ids:
+ cc_users.append(
+ issue_objects_pb2.Issue.UserValue(
+ derivation=issue_objects_pb2.Derivation.Value('EXPLICIT'),
+ user=rnc.ConvertUserName(cc_user_id)))
+ for derived_cc_user_id in issue.derived_cc_ids:
+ cc_users.append(
+ issue_objects_pb2.Issue.UserValue(
+ derivation=issue_objects_pb2.Derivation.Value('RULE'),
+ user=rnc.ConvertUserName(derived_cc_user_id)))
+
+ labels = self.ConvertLabels(
+ issue.labels, issue.derived_labels, issue.project_id)
+ components = self._ConvertComponentValues(issue)
+ non_approval_fvs = self._GetNonApprovalFieldValues(
+ issue.field_values, issue.project_id)
+ field_values = self.ConvertFieldValues(
+ non_approval_fvs, issue.project_id, issue.phases)
+ field_values.extend(
+ self.ConvertEnumFieldValues(
+ issue.labels, issue.derived_labels, issue.project_id))
+ related_issue_ids = (
+ [issue.merged_into] + issue.blocked_on_iids + issue.blocking_iids)
+ issue_names_by_ids = rnc.ConvertIssueNames(
+ self.cnxn, related_issue_ids, self.services)
+ merged_into_issue_ref = None
+ if issue.merged_into and issue.merged_into in issue_names_by_ids:
+ merged_into_issue_ref = issue_objects_pb2.IssueRef(
+ issue=issue_names_by_ids[issue.merged_into])
+ if issue.merged_into_external:
+ merged_into_issue_ref = issue_objects_pb2.IssueRef(
+ ext_identifier=issue.merged_into_external)
+
+ blocked_on_issue_refs = [
+ issue_objects_pb2.IssueRef(issue=issue_names_by_ids[iid])
+ for iid in issue.blocked_on_iids
+ if iid in issue_names_by_ids
+ ]
+ blocked_on_issue_refs.extend(
+ issue_objects_pb2.IssueRef(
+ ext_identifier=blocked_on.ext_issue_identifier)
+ for blocked_on in issue.dangling_blocked_on_refs)
+
+ blocking_issue_refs = [
+ issue_objects_pb2.IssueRef(issue=issue_names_by_ids[iid])
+ for iid in issue.blocking_iids
+ if iid in issue_names_by_ids
+ ]
+ blocking_issue_refs.extend(
+ issue_objects_pb2.IssueRef(
+ ext_identifier=blocking.ext_issue_identifier)
+ for blocking in issue.dangling_blocking_refs)
+ # All other timestamps were set when the issue was created.
+ close_time = None
+ if issue.closed_timestamp:
+ close_time = timestamp_pb2.Timestamp(seconds=issue.closed_timestamp)
+
+ phases = self._ComputePhases(issue.phases)
+
+ result = issue_objects_pb2.Issue(
+ name=issue_names_dict[issue.issue_id],
+ summary=issue.summary,
+ state=content_state,
+ status=status,
+ reporter=rnc.ConvertUserName(issue.reporter_id),
+ owner=owner,
+ cc_users=cc_users,
+ labels=labels,
+ components=components,
+ field_values=field_values,
+ merged_into_issue_ref=merged_into_issue_ref,
+ blocked_on_issue_refs=blocked_on_issue_refs,
+ blocking_issue_refs=blocking_issue_refs,
+ create_time=timestamp_pb2.Timestamp(seconds=issue.opened_timestamp),
+ close_time=close_time,
+ modify_time=timestamp_pb2.Timestamp(seconds=issue.modified_timestamp),
+ component_modify_time=timestamp_pb2.Timestamp(
+ seconds=issue.component_modified_timestamp),
+ status_modify_time=timestamp_pb2.Timestamp(
+ seconds=issue.status_modified_timestamp),
+ owner_modify_time=timestamp_pb2.Timestamp(
+ seconds=issue.owner_modified_timestamp),
+ star_count=issue.star_count,
+ phases=phases)
+ # TODO(crbug.com/monorail/5857): Set attachment_count unconditionally
+ # after the underlying source of negative attachment counts has been
+ # resolved and database has been repaired.
+ if issue.attachment_count >= 0:
+ result.attachment_count = issue.attachment_count
+ converted_issues.append(result)
+ return converted_issues
+
+ def IngestAttachmentUploads(self, attachment_uploads):
+ # type: (Sequence[api_proto.issues_pb2.AttachmentUpload] ->
+ # Sequence[framework_helpers.AttachmentUpload])
+ """Ingests protoc AttachmentUploads into framework_helpers.AttachUploads."""
+ ingested_uploads = []
+ with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
+ for up in attachment_uploads:
+ if not up.filename or not up.content:
+ err_agg.AddErrorMessage(
+ 'Uploaded atachment missing filename or content')
+ mimetype = filecontent.GuessContentTypeFromFilename(up.filename)
+ ingested_uploads.append(
+ framework_helpers.AttachmentUpload(
+ up.filename, up.content, mimetype))
+
+ return ingested_uploads
+
+ def IngestIssueDeltas(self, issue_deltas):
+ # type: (Sequence[api_proto.issues_pb2.IssueDelta]) ->
+ # Sequence[Tuple[int, proto.tracker_pb2.IssueDelta]]
+ """Ingests protoc IssueDeltas, into protorpc IssueDeltas.
+
+ Args:
+ issue_deltas: the protoc IssueDeltas to ingest.
+
+ Returns:
+ A list of (issue_id, tracker_pb2.IssueDelta) tuples that contain
+ values found in issue_deltas, ignoring all OUTPUT_ONLY and masked
+ fields.
+
+ Raises:
+ InputException: if any fields in the approval_deltas were invalid.
+ NoSuchProjectException: if any parent projects are not found.
+ NoSuchIssueException: if any issues are not found.
+ NoSuchComponentException: if any components are not found.
+ """
+ issue_names = [delta.issue.name for delta in issue_deltas]
+ issue_ids = rnc.IngestIssueNames(self.cnxn, issue_names, self.services)
+ issues_dict, misses = self.services.issue.GetIssuesDict(
+ self.cnxn, issue_ids)
+ if misses:
+ logging.info(
+ 'Issues not found for supposedly valid issue_ids: %r' % misses)
+ raise ValueError('Could not fetch some issues.')
+ configs_by_pid = self.services.config.GetProjectConfigs(
+ self.cnxn, {issue.project_id for issue in issues_dict.values()})
+
+ with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
+ for api_delta in issue_deltas:
+ if not api_delta.HasField('update_mask'):
+ err_agg.AddErrorMessage(
+ '`update_mask` must be set for {} delta.', api_delta.issue.name)
+ elif not api_delta.update_mask.IsValidForDescriptor(
+ issue_objects_pb2.Issue.DESCRIPTOR):
+ err_agg.AddErrorMessage(
+ 'Invalid `update_mask` for {} delta.', api_delta.issue.name)
+
+ ingested = []
+ for iid, api_delta in zip(issue_ids, issue_deltas):
+ delta = tracker_pb2.IssueDelta()
+
+ # Check non-repeated fields before MergeMessage because in an object
+ # where fields are not set and with a FieldMask applied, there is no
+ # way to tell if empty fields were explicitly listed or not listed
+ # in the FieldMask.
+ paths_set = set(api_delta.update_mask.paths)
+ if (not paths_set.isdisjoint({'status', 'status.status'}) and
+ api_delta.issue.status.status):
+ delta.status = api_delta.issue.status.status
+ elif 'status.status' in paths_set and not api_delta.issue.status.status:
+ delta.status = ''
+
+ if (not paths_set.isdisjoint({'owner', 'owner.user'}) and
+ api_delta.issue.owner.user):
+ delta.owner_id = rnc.IngestUserName(
+ self.cnxn, api_delta.issue.owner.user, self.services)
+ elif 'owner.user' in paths_set and not api_delta.issue.owner.user:
+ delta.owner_id = framework_constants.NO_USER_SPECIFIED
+
+ if 'summary' in paths_set:
+ if api_delta.issue.summary:
+ delta.summary = api_delta.issue.summary
+ else:
+ delta.summary = ''
+
+ merge_ref = api_delta.issue.merged_into_issue_ref
+ if 'merged_into_issue_ref' in paths_set:
+ if (api_delta.issue.merged_into_issue_ref.issue or
+ api_delta.issue.merged_into_issue_ref.ext_identifier):
+ ingested_ref = self._IngestIssueRef(merge_ref)
+ if isinstance(ingested_ref, tracker_pb2.DanglingIssueRef):
+ delta.merged_into_external = ingested_ref.ext_issue_identifier
+ else:
+ delta.merged_into = ingested_ref
+ elif 'merged_into_issue_ref.issue' in paths_set:
+ if api_delta.issue.merged_into_issue_ref.issue:
+ delta.merged_into = self._IngestIssueRef(merge_ref)
+ else:
+ delta.merged_into = 0
+ elif 'merged_into_issue_ref.ext_identifier' in paths_set:
+ if api_delta.issue.merged_into_issue_ref.ext_identifier:
+ ingested_ref = self._IngestIssueRef(merge_ref)
+ delta.merged_into_external = ingested_ref.ext_issue_identifier
+ else:
+ delta.merged_into_external = ''
+
+ filtered_api_issue = issue_objects_pb2.Issue()
+ api_delta.update_mask.MergeMessage(
+ api_delta.issue,
+ filtered_api_issue,
+ replace_message_field=True,
+ replace_repeated_field=True)
+
+ cc_names = [name for name in api_delta.ccs_remove] + [
+ user_value.user for user_value in filtered_api_issue.cc_users
+ ]
+ cc_ids = rnc.IngestUserNames(self.cnxn, cc_names, self.services)
+ delta.cc_ids_remove = cc_ids[:len(api_delta.ccs_remove)]
+ delta.cc_ids_add = cc_ids[len(api_delta.ccs_remove):]
+
+ comp_names = [component for component in api_delta.components_remove] + [
+ c_value.component for c_value in filtered_api_issue.components
+ ]
+ project_comp_ids = rnc.IngestComponentDefNames(
+ self.cnxn, comp_names, self.services)
+ comp_ids = [comp_id for (_pid, comp_id) in project_comp_ids]
+ delta.comp_ids_remove = comp_ids[:len(api_delta.components_remove)]
+ delta.comp_ids_add = comp_ids[len(api_delta.components_remove):]
+
+ # Added to delta below, after ShiftEnumFieldsIntoLabels.
+ labels_add = [value.label for value in filtered_api_issue.labels]
+ labels_remove = [label for label in api_delta.labels_remove]
+
+ config = configs_by_pid[issues_dict[iid].project_id]
+ fvs_add, add_enums = self._IngestFieldValues(
+ filtered_api_issue.field_values, config)
+ fvs_remove, remove_enums = self._IngestFieldValues(
+ api_delta.field_vals_remove, config)
+ field_helpers.ShiftEnumFieldsIntoLabels(
+ labels_add, labels_remove, add_enums, remove_enums, config)
+ delta.field_vals_add = fvs_add
+ delta.field_vals_remove = fvs_remove
+ delta.labels_add = labels_add
+ delta.labels_remove = labels_remove
+ assert len(add_enums) == 0 # ShiftEnumFieldsIntoLabels clears all enums.
+ assert len(remove_enums) == 0
+
+ blocked_on_iids_rm, blocked_on_dangling_rm = self._IngestIssueRefs(
+ api_delta.blocked_on_issues_remove)
+ delta.blocked_on_remove = blocked_on_iids_rm
+ delta.ext_blocked_on_remove = [
+ ref.ext_issue_identifier for ref in blocked_on_dangling_rm
+ ]
+
+ blocked_on_iids_add, blocked_on_dangling_add = self._IngestIssueRefs(
+ filtered_api_issue.blocked_on_issue_refs)
+ delta.blocked_on_add = blocked_on_iids_add
+ delta.ext_blocked_on_add = [
+ ref.ext_issue_identifier for ref in blocked_on_dangling_add
+ ]
+
+ blocking_iids_rm, blocking_dangling_rm = self._IngestIssueRefs(
+ api_delta.blocking_issues_remove)
+ delta.blocking_remove = blocking_iids_rm
+ delta.ext_blocking_remove = [
+ ref.ext_issue_identifier for ref in blocking_dangling_rm
+ ]
+
+ blocking_iids_add, blocking_dangling_add = self._IngestIssueRefs(
+ filtered_api_issue.blocking_issue_refs)
+ delta.blocking_add = blocking_iids_add
+ delta.ext_blocking_add = [
+ ref.ext_issue_identifier for ref in blocking_dangling_add
+ ]
+
+ ingested.append((iid, delta))
+
+ return ingested
+
+ def IngestApprovalDeltas(self, approval_deltas, setter_id):
+ # type: (Sequence[api_proto.issues_pb2.ApprovalDelta], int) ->
+ # Sequence[Tuple[int, int, proto.tracker_pb2.ApprovalDelta]]
+ """Ingests protoc ApprovalDeltas into protorpc ApprovalDeltas.
+
+ Args:
+ approval_deltas: the protoc ApprovalDeltas to ingest.
+ setter_id: The ID for the user setting the deltas.
+
+ Returns:
+ Sequence of (issue_id, approval_id, ApprovalDelta) tuples in the order
+ provided. The ApprovalDeltas ignore all OUTPUT_ONLY and masked fields.
+ The tuples are "delta_specifications;" they identify one requested change.
+
+ Raises:
+ InputException: if any fields in the approval_delta protos were invalid.
+ NoSuchProjectException: if the parent project of any ApprovalValue isn't
+ found.
+ NoSuchIssueException: if the issue of any ApprovalValue isn't found.
+ NoSuchUserException: if any user value was provided with an invalid email.
+ Note that users specified by ID are not checked for existence.
+ """
+ delta_specifications = []
+ set_on = int(time.time()) # Use the same timestamp for all deltas.
+ for approval_delta in approval_deltas:
+ approval_name = approval_delta.approval_value.name
+ # TODO(crbug/monorail/8173): Aggregate errors.
+ project_id, issue_id, approval_id = rnc.IngestApprovalValueName(
+ self.cnxn, approval_name, self.services)
+
+ if not approval_delta.HasField('update_mask'):
+ raise exceptions.InputException(
+ '`update_mask` must be set for %s delta.' % approval_name)
+ elif not approval_delta.update_mask.IsValidForDescriptor(
+ issue_objects_pb2.ApprovalValue.DESCRIPTOR):
+ raise exceptions.InputException(
+ 'Invalid `update_mask` for %s delta.' % approval_name)
+ filtered_value = issue_objects_pb2.ApprovalValue()
+ approval_delta.update_mask.MergeMessage(
+ approval_delta.approval_value,
+ filtered_value,
+ replace_message_field=True,
+ replace_repeated_field=True)
+ status = _APPROVAL_STATUS_INGEST[filtered_value.status]
+ # Approvers
+ # No autocreate.
+ # A user may try to remove all existing approvers [a, b] and add another
+ # approver [c]. If they mis-type `c` and we auto-create `c` instead of
+ # raising error, this would cause the ApprovalValue to be editable by no
+ # one but site admins.
+ approver_ids_add = rnc.IngestUserNames(
+ self.cnxn, filtered_value.approvers, self.services, autocreate=False)
+ approver_ids_remove = rnc.IngestUserNames(
+ self.cnxn,
+ approval_delta.approvers_remove,
+ self.services,
+ autocreate=False)
+
+ # Field Values.
+ config = self.services.config.GetProjectConfig(self.cnxn, project_id)
+ approval_fds_by_id = {
+ fd.field_id: fd
+ for fd in config.field_defs
+ if fd.field_type is tracker_pb2.FieldTypes.APPROVAL_TYPE
+ }
+ if approval_id not in approval_fds_by_id:
+ raise exceptions.InputException(
+ 'Approval not found in project for %s' % approval_name)
+
+ sub_fvs_add, add_enums = self._IngestFieldValues(
+ filtered_value.field_values, config, approval_id_filter=approval_id)
+ sub_fvs_remove, remove_enums = self._IngestFieldValues(
+ approval_delta.field_vals_remove,
+ config,
+ approval_id_filter=approval_id)
+ labels_add = []
+ labels_remove = []
+ field_helpers.ShiftEnumFieldsIntoLabels(
+ labels_add, labels_remove, add_enums, remove_enums, config)
+ assert len(add_enums) == 0 # ShiftEnumFieldsIntoLabels clears all enums.
+ assert len(remove_enums) == 0
+ delta = tbo.MakeApprovalDelta(
+ status,
+ setter_id,
+ approver_ids_add,
+ approver_ids_remove,
+ sub_fvs_add,
+ sub_fvs_remove, [],
+ labels_add,
+ labels_remove,
+ set_on=set_on)
+ delta_specifications.append((issue_id, approval_id, delta))
+ return delta_specifications
+
+ def IngestIssue(self, issue, project_id):
+ # type: (api_proto.issue_objects_pb2.Issue, int) -> proto.tracker_pb2.Issue
+ """Ingest a protoc Issue into a protorpc Issue.
+
+ Args:
+ issue: the protoc issue to ingest.
+ project_id: The project into which we're ingesting `issue`.
+
+ Returns:
+ protorpc version of issue, ignoring all OUTPUT_ONLY fields.
+
+ Raises:
+ InputException: if any fields in the 'issue' proto were invalid.
+ NoSuchProjectException: if 'project_id' is not found.
+ """
+ # Get config first. We can't ingest the issue if the project isn't found.
+ config = self.services.config.GetProjectConfig(self.cnxn, project_id)
+ ingestedDict = {
+ 'project_id': project_id,
+ 'summary': issue.summary
+ }
+ with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
+ self._ExtractOwner(issue, ingestedDict, err_agg)
+
+ # Extract ccs.
+ try:
+ ingestedDict['cc_ids'] = rnc.IngestUserNames(
+ self.cnxn, [cc.user for cc in issue.cc_users], self.services,
+ autocreate=True)
+ except exceptions.InputException as e:
+ err_agg.AddErrorMessage('Error ingesting cc_users: {}', e)
+
+ # Extract status.
+ if issue.HasField('status') and issue.status.status:
+ ingestedDict['status'] = issue.status.status
+ else:
+ err_agg.AddErrorMessage('Status is required when creating an issue')
+
+ # Extract components.
+ try:
+ project_comp_ids = rnc.IngestComponentDefNames(
+ self.cnxn, [cv.component for cv in issue.components], self.services)
+ ingestedDict['component_ids'] = [
+ comp_id for (_pid, comp_id) in project_comp_ids]
+ except (exceptions.InputException, exceptions.NoSuchProjectException,
+ exceptions.NoSuchComponentException) as e:
+ err_agg.AddErrorMessage('Error ingesting components: {}', e)
+
+ # Extract labels and field values.
+ ingestedDict['labels'] = [lv.label for lv in issue.labels]
+ try:
+ ingestedDict['field_values'], enums = self._IngestFieldValues(
+ issue.field_values, config)
+ field_helpers.ShiftEnumFieldsIntoLabels(
+ ingestedDict['labels'], [], enums, [], config)
+ assert len(
+ enums) == 0 # ShiftEnumFieldsIntoLabels must clear all enums.
+ except exceptions.InputException as e:
+ err_agg.AddErrorMessage(e.message)
+
+ # Ingest merged, blocking/blocked_on.
+ self._ExtractIssueRefs(issue, ingestedDict, err_agg)
+ return tracker_pb2.Issue(**ingestedDict)
+
+ def _IngestFieldValues(self, field_values, config, approval_id_filter=None):
+ # type: (Sequence[api_proto.issue_objects.FieldValue],
+ # proto.tracker_pb2.ProjectIssueConfig, Optional[int]) ->
+ # Tuple[Sequence[proto.tracker_pb2.FieldValue],
+ # Mapping[int, Sequence[str]]]
+ """Returns protorpc FieldValues for the given protoc FieldValues.
+
+ Raises exceptions if any field could not be parsed for any reasons such as
+ unsupported field type, non-existent field, field from different
+ projects, or fields with mismatched parent approvals.
+
+ Args:
+ field_values: protoc FieldValues to ingest.
+ config: ProjectIssueConfig for the FieldValues we're ingesting.
+ approval_id_filter: an approval_id, including any FieldValues that does
+ not have this approval as a parent will trigger InputException.
+
+ Returns:
+ A pair 1) Ingested FieldValues. 2) A mapping of field ids to values
+ for ENUM_TYPE fields in 'field_values.'
+
+ Raises:
+ InputException: if any fields_values could not be parsed for any reasons
+ such as unsupported field type, non-existent field, or field from
+ different projects.
+ """
+ fds_by_id = {fd.field_id: fd for fd in config.field_defs}
+ enums = {}
+ ingestedFieldValues = []
+ with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
+ for fv in field_values:
+ try:
+ project_id, fd_id = rnc.IngestFieldDefName(
+ self.cnxn, fv.field, self.services)
+ fd = fds_by_id[fd_id]
+ # Raise if field does not belong to approval_id_filter (if provided).
+ if (approval_id_filter is not None and
+ fd.approval_id != approval_id_filter):
+ approval_name = rnc.ConvertApprovalDefNames(
+ self.cnxn, [approval_id_filter], project_id,
+ self.services)[approval_id_filter]
+ err_agg.AddErrorMessage(
+ 'Field {} does not belong to approval {}', fv.field,
+ approval_name)
+ continue
+ if fd.field_type == tracker_pb2.FieldTypes.ENUM_TYPE:
+ enums.setdefault(fd_id, []).append(fv.value)
+ else:
+ ingestedFieldValues.append(self._IngestFieldValue(fv, fd))
+ except (exceptions.InputException, exceptions.NoSuchProjectException,
+ exceptions.NoSuchFieldDefException, ValueError) as e:
+ err_agg.AddErrorMessage(
+ 'Could not ingest value ({}) for FieldDef ({}): {}', fv.value,
+ fv.field, e)
+ except exceptions.NoSuchUserException as e:
+ err_agg.AddErrorMessage(
+ 'User ({}) not found when ingesting user field: {}', fv.value,
+ fv.field)
+ except KeyError as e:
+ err_agg.AddErrorMessage('Field {} is not in this project', fv.field)
+ return ingestedFieldValues, enums
+
+ def _IngestFieldValue(self, field_value, field_def):
+ # type: (api_proto.issue_objects.FieldValue, proto.tracker_pb2.FieldDef) ->
+ # proto.tracker_pb2.FieldValue
+ """Ingest a protoc FieldValue into a protorpc FieldValue.
+
+ Args:
+ field_value: protoc FieldValue to ingest.
+ field_def: protorpc FieldDef associated with 'field_value'.
+ BOOL_TYPE and APPROVAL_TYPE are ignored.
+ Enum values are not allowed. They must be ingested as labels.
+
+ Returns:
+ Ingested protorpc FieldValue.
+
+ Raises:
+ InputException if 'field_def' is USER_TYPE and 'field_value' does not
+ have a valid formatted resource name.
+ NoSuchUserException if specified user in field does not exist.
+ ValueError if 'field_value' could not be parsed for 'field_def'.
+ """
+ assert field_def.field_type != tracker_pb2.FieldTypes.ENUM_TYPE
+ if field_def.field_type == tracker_pb2.FieldTypes.USER_TYPE:
+ return self._ParseOneUserFieldValue(field_value.value, field_def.field_id)
+ fv = field_helpers.ParseOneFieldValue(
+ self.cnxn, self.services.user, field_def, field_value.value)
+ # ParseOneFieldValue currently ignores parsing errors, although it has TODOs
+ # to raise them.
+ if not fv:
+ raise ValueError('Could not parse %s' % field_value.value)
+ return fv
+
+ def _ParseOneUserFieldValue(self, value, field_id):
+ # type: (str, int) -> proto.tracker_pb2.FieldValue
+ """Replacement for the obsolete user parsing in ParseOneFieldValue."""
+ user_id = rnc.IngestUserName(self.cnxn, value, self.services)
+ return tbo.MakeFieldValue(field_id, None, None, user_id, None, None, False)
+
+ def _ExtractOwner(self, issue, ingestedDict, err_agg):
+ # type: (api_proto.issue_objects_pb2.Issue, Dict[str, Any], ErrorAggregator)
+ # -> None
+ """Fills 'owner' into `ingestedDict`, if it can be extracted."""
+ if issue.HasField('owner'):
+ try:
+ # Unlike for cc's, we require owner be an existing user, thus call we
+ # do not autocreate.
+ ingestedDict['owner_id'] = rnc.IngestUserName(
+ self.cnxn, issue.owner.user, self.services, autocreate=False)
+ except exceptions.InputException as e:
+ err_agg.AddErrorMessage(
+ 'Error ingesting owner ({}): {}', issue.owner.user, e)
+ except exceptions.NoSuchUserException as e:
+ err_agg.AddErrorMessage(
+ 'User ({}) not found when ingesting owner', e)
+ else:
+ ingestedDict['owner_id'] = framework_constants.NO_USER_SPECIFIED
+
+ def _ExtractIssueRefs(self, issue, ingestedDict, err_agg):
+ # type: (api_proto.issue_objects_pb2.Issue, Dict[str, Any], ErrorAggregator)
+ # -> None
+ """Fills issue relationships into `ingestedDict` from `issue`."""
+ if issue.HasField('merged_into_issue_ref'):
+ try:
+ merged_into_ref = self._IngestIssueRef(issue.merged_into_issue_ref)
+ if isinstance(merged_into_ref, tracker_pb2.DanglingIssueRef):
+ ingestedDict['merged_into_external'] = (
+ merged_into_ref.ext_issue_identifier)
+ else:
+ ingestedDict['merged_into'] = merged_into_ref
+ except exceptions.InputException as e:
+ err_agg.AddErrorMessage(
+ 'Error ingesting ref {}: {}', issue.merged_into_issue_ref, e)
+ try:
+ iids, dangling_refs = self._IngestIssueRefs(issue.blocked_on_issue_refs)
+ ingestedDict['blocked_on_iids'] = iids
+ ingestedDict['dangling_blocked_on_refs'] = dangling_refs
+ except exceptions.InputException as e:
+ err_agg.AddErrorMessage(e.message)
+ try:
+ iids, dangling_refs = self._IngestIssueRefs(issue.blocking_issue_refs)
+ ingestedDict['blocking_iids'] = iids
+ ingestedDict['dangling_blocking_refs'] = dangling_refs
+ except exceptions.InputException as e:
+ err_agg.AddErrorMessage(e.message)
+
+ def _IngestIssueRefs(self, issue_refs):
+ # type: (api_proto.issue_objects.IssueRf) ->
+ # Tuple[Sequence[int], Sequence[tracker_pb2.DanglingIssueRef]]
+ """Given protoc IssueRefs, returns issue_ids and DanglingIssueRefs."""
+ issue_ids = []
+ external_refs = []
+ with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
+ for ref in issue_refs:
+ try:
+ ingested_ref = self._IngestIssueRef(ref)
+ if isinstance(ingested_ref, tracker_pb2.DanglingIssueRef):
+ external_refs.append(ingested_ref)
+ else:
+ issue_ids.append(ingested_ref)
+ except (exceptions.InputException, exceptions.NoSuchIssueException,
+ exceptions.NoSuchProjectException) as e:
+ err_agg.AddErrorMessage('Error ingesting ref {}: {}', ref, e)
+
+ return issue_ids, external_refs
+
+ def _IngestIssueRef(self, issue_ref):
+ # type: (api_proto.issue_objects.IssueRef) ->
+ # Union[int, tracker_pb2.DanglingIssueRef]
+ """Given a protoc IssueRef, returns an issue id or DanglingIssueRef."""
+ if issue_ref.issue and issue_ref.ext_identifier:
+ raise exceptions.InputException(
+ 'IssueRefs MUST NOT have both `issue` and `ext_identifier`')
+ if issue_ref.issue:
+ return rnc.IngestIssueName(self.cnxn, issue_ref.issue, self.services)
+ if issue_ref.ext_identifier:
+ # TODO(crbug.com/monorail/7208): Handle ingestion/conversion of CodeSite
+ # refs. We may be able to avoid ever needing to ingest them.
+ return tracker_pb2.DanglingIssueRef(
+ ext_issue_identifier=issue_ref.ext_identifier
+ )
+ raise exceptions.InputException(
+ 'IssueRefs MUST have one of `issue` and `ext_identifier`')
+
+ def IngestIssuesListColumns(self, issues_list_columns):
+ # type: (Sequence[proto.issue_objects_pb2.IssuesListColumn] -> str
+ """Ingest a list of protoc IssueListColumns and returns a string."""
+ return ' '.join([col.column for col in issues_list_columns])
+
+ def _ComputeIssuesListColumns(self, columns):
+ # type: (string) -> Sequence[api_proto.issue_objects_pb2.IssuesListColumn]
+ """Convert string representation of columns to protoc IssuesListColumns"""
+ return [
+ issue_objects_pb2.IssuesListColumn(column=col)
+ for col in columns.split()
+ ]
+
+ def IngestNotifyType(self, notify):
+ # type: (issue_pb.NotifyType) -> bool
+ """Ingest a NotifyType to boolean."""
+ if (notify == issues_pb2.NotifyType.Value('NOTIFY_TYPE_UNSPECIFIED') or
+ notify == issues_pb2.NotifyType.Value('EMAIL')):
+ return True
+ elif notify == issues_pb2.NotifyType.Value('NO_NOTIFICATION'):
+ return False
+
+ # Users
+
+ def ConvertUser(self, user):
+ # type: (protorpc.User) -> api_proto.user_objects_pb2.User
+ """Convert a protorpc User into a protoc User.
+
+ Args:
+ user: protorpc User object.
+
+ Returns:
+ The protoc User object.
+ """
+ return self.ConvertUsers([user.user_id])[user.user_id]
+
+
+ # TODO(crbug/monorail/7238): Make this take in a full User object and
+ # return a Sequence, rather than a map, after hotlist users are converted.
+ def ConvertUsers(self, user_ids):
+ # type: (Sequence[int]) -> Map(int, api_proto.user_objects_pb2.User)
+ """Convert list of protorpc Users into list of protoc Users.
+
+ Args:
+ user_ids: List of User IDs.
+
+ Returns:
+ Dict of User IDs to User protos for given user_ids that could be found.
+ """
+ user_ids_to_names = {}
+
+ # Get display names
+ users_by_id = self.services.user.GetUsersByIDs(self.cnxn, user_ids)
+ (display_names_by_id,
+ display_emails_by_id) = framework_bizobj.CreateUserDisplayNamesAndEmails(
+ self.cnxn, self.services, self.user_auth, users_by_id.values())
+
+ for user_id, user in users_by_id.items():
+ name = rnc.ConvertUserNames([user_id]).get(user_id)
+
+ display_name = display_names_by_id.get(user_id)
+ display_email = display_emails_by_id.get(user_id)
+ availability = framework_helpers.GetUserAvailability(user)
+ availability_message, _availability_status = availability
+
+ user_ids_to_names[user_id] = user_objects_pb2.User(
+ name=name,
+ display_name=display_name,
+ email=display_email,
+ availability_message=availability_message)
+
+ return user_ids_to_names
+
+ def ConvertProjectStars(self, user_id, projects):
+ # type: (int, Collection[protorpc.Project]) ->
+ # Collection[api_proto.user_objects_pb2.ProjectStar]
+ """Convert list of protorpc Projects into protoc ProjectStars.
+
+ Args:
+ user_id: The user the ProjectStar is associated with.
+ projects: All starred projects.
+
+ Returns:
+ List of ProjectStar messages.
+ """
+ api_project_stars = []
+ for proj in projects:
+ name = rnc.ConvertProjectStarName(
+ self.cnxn, user_id, proj.project_id, self.services)
+ star = user_objects_pb2.ProjectStar(name=name)
+ api_project_stars.append(star)
+ return api_project_stars
+
+ # Field Defs
+
+ def ConvertFieldDefs(self, field_defs, project_id):
+ # type: (Sequence[proto.tracker_pb2.FieldDef], int) ->
+ # Sequence[api_proto.project_objects_pb2.FieldDef]
+ """Convert sequence of protorpc FieldDefs to protoc FieldDefs.
+
+ Args:
+ field_defs: List of protorpc FieldDefs
+ project_id: ID of the Project that is ancestor to all given
+ `field_defs`.
+
+ Returns:
+ Sequence of protoc FieldDef in the same order they are given in
+ `field_defs`. In the event any field_def or the referenced approval_id
+ in `field_defs` is not found, they will be omitted from the result.
+ """
+ field_ids = [fd.field_id for fd in field_defs]
+ resource_names_dict = rnc.ConvertFieldDefNames(
+ self.cnxn, field_ids, project_id, self.services)
+ parent_approval_ids = [
+ fd.approval_id for fd in field_defs if fd.approval_id is not None
+ ]
+ approval_names_dict = rnc.ConvertApprovalDefNames(
+ self.cnxn, parent_approval_ids, project_id, self.services)
+
+ api_fds = []
+ for fd in field_defs:
+ # Skip over approval fields, they have their separate ApprovalDef
+ if fd.field_type == tracker_pb2.FieldTypes.APPROVAL_TYPE:
+ continue
+ if fd.field_id not in resource_names_dict:
+ continue
+
+ name = resource_names_dict.get(fd.field_id)
+ display_name = fd.field_name
+ docstring = fd.docstring
+ field_type = self._ConvertFieldDefType(fd.field_type)
+ applicable_issue_type = fd.applicable_type
+ admins = rnc.ConvertUserNames(fd.admin_ids).values()
+ editors = rnc.ConvertUserNames(fd.editor_ids).values()
+ traits = self._ComputeFieldDefTraits(fd)
+ approval_parent = approval_names_dict.get(fd.approval_id)
+
+ enum_settings = None
+ if field_type == project_objects_pb2.FieldDef.Type.Value('ENUM'):
+ enum_settings = project_objects_pb2.FieldDef.EnumTypeSettings(
+ choices=self._GetEnumFieldChoices(fd))
+
+ int_settings = None
+ if field_type == project_objects_pb2.FieldDef.Type.Value('INT'):
+ int_settings = project_objects_pb2.FieldDef.IntTypeSettings(
+ min_value=fd.min_value, max_value=fd.max_value)
+
+ str_settings = None
+ if field_type == project_objects_pb2.FieldDef.Type.Value('STR'):
+ str_settings = project_objects_pb2.FieldDef.StrTypeSettings(
+ regex=fd.regex)
+
+ user_settings = None
+ if field_type == project_objects_pb2.FieldDef.Type.Value('USER'):
+ user_settings = project_objects_pb2.FieldDef.UserTypeSettings(
+ role_requirements=self._ConvertRoleRequirements(fd.needs_member),
+ notify_triggers=self._ConvertNotifyTriggers(fd.notify_on),
+ grants_perm=fd.grants_perm,
+ needs_perm=fd.needs_perm)
+
+ date_settings = None
+ if field_type == project_objects_pb2.FieldDef.Type.Value('DATE'):
+ date_settings = project_objects_pb2.FieldDef.DateTypeSettings(
+ date_action=self._ConvertDateAction(fd.date_action))
+
+ api_fd = project_objects_pb2.FieldDef(
+ name=name,
+ display_name=display_name,
+ docstring=docstring,
+ type=field_type,
+ applicable_issue_type=applicable_issue_type,
+ admins=admins,
+ traits=traits,
+ approval_parent=approval_parent,
+ enum_settings=enum_settings,
+ int_settings=int_settings,
+ str_settings=str_settings,
+ user_settings=user_settings,
+ date_settings=date_settings,
+ editors=editors)
+ api_fds.append(api_fd)
+ return api_fds
+
+ def _ConvertDateAction(self, date_action):
+ # type: (proto.tracker_pb2.DateAction) ->
+ # api_proto.project_objects_pb2.FieldDef.DateTypeSettings.DateAction
+ """Convert protorpc DateAction to protoc
+ FieldDef.DateTypeSettings.DateAction"""
+ if date_action == tracker_pb2.DateAction.NO_ACTION:
+ return project_objects_pb2.FieldDef.DateTypeSettings.DateAction.Value(
+ 'NO_ACTION')
+ elif date_action == tracker_pb2.DateAction.PING_OWNER_ONLY:
+ return project_objects_pb2.FieldDef.DateTypeSettings.DateAction.Value(
+ 'NOTIFY_OWNER')
+ elif date_action == tracker_pb2.DateAction.PING_PARTICIPANTS:
+ return project_objects_pb2.FieldDef.DateTypeSettings.DateAction.Value(
+ 'NOTIFY_PARTICIPANTS')
+ else:
+ raise ValueError('Unsupported DateAction Value')
+
+ def _ConvertRoleRequirements(self, needs_member):
+ # type: (bool) ->
+ # api_proto.project_objects_pb2.FieldDef.
+ # UserTypeSettings.RoleRequirements
+ """Convert protorpc RoleRequirements to protoc
+ FieldDef.UserTypeSettings.RoleRequirements"""
+
+ proto_user_settings = project_objects_pb2.FieldDef.UserTypeSettings
+ if needs_member:
+ return proto_user_settings.RoleRequirements.Value('PROJECT_MEMBER')
+ else:
+ return proto_user_settings.RoleRequirements.Value('NO_ROLE_REQUIREMENT')
+
+ def _ConvertNotifyTriggers(self, notify_trigger):
+ # type: (proto.tracker_pb2.NotifyTriggers) ->
+ # api_proto.project_objects_pb2.FieldDef.UserTypeSettings.NotifyTriggers
+ """Convert protorpc NotifyTriggers to protoc
+ FieldDef.UserTypeSettings.NotifyTriggers"""
+ if notify_trigger == tracker_pb2.NotifyTriggers.NEVER:
+ return project_objects_pb2.FieldDef.UserTypeSettings.NotifyTriggers.Value(
+ 'NEVER')
+ elif notify_trigger == tracker_pb2.NotifyTriggers.ANY_COMMENT:
+ return project_objects_pb2.FieldDef.UserTypeSettings.NotifyTriggers.Value(
+ 'ANY_COMMENT')
+ else:
+ raise ValueError('Unsupported NotifyTriggers Value')
+
+ def _ConvertFieldDefType(self, field_type):
+ # type: (proto.tracker_pb2.FieldTypes) ->
+ # api_proto.project_objects_pb2.FieldDef.Type
+ """Convert protorpc FieldType to protoc FieldDef.Type
+
+ Args:
+ field_type: protorpc FieldType
+
+ Returns:
+ Corresponding protoc FieldDef.Type
+
+ Raises:
+ ValueError if input `field_type` has no suitable supported FieldDef.Type,
+ or input `field_type` is not a recognized enum option.
+ """
+ if field_type == tracker_pb2.FieldTypes.ENUM_TYPE:
+ return project_objects_pb2.FieldDef.Type.Value('ENUM')
+ elif field_type == tracker_pb2.FieldTypes.INT_TYPE:
+ return project_objects_pb2.FieldDef.Type.Value('INT')
+ elif field_type == tracker_pb2.FieldTypes.STR_TYPE:
+ return project_objects_pb2.FieldDef.Type.Value('STR')
+ elif field_type == tracker_pb2.FieldTypes.USER_TYPE:
+ return project_objects_pb2.FieldDef.Type.Value('USER')
+ elif field_type == tracker_pb2.FieldTypes.DATE_TYPE:
+ return project_objects_pb2.FieldDef.Type.Value('DATE')
+ elif field_type == tracker_pb2.FieldTypes.URL_TYPE:
+ return project_objects_pb2.FieldDef.Type.Value('URL')
+ else:
+ raise ValueError(
+ 'Unsupported tracker_pb2.FieldType enum. Boolean types '
+ 'are unsupported and approval types are found in ApprovalDefs')
+
+ def _ComputeFieldDefTraits(self, field_def):
+ # type: (proto.tracker_pb2.FieldDef) ->
+ # Sequence[api_proto.project_objects_pb2.FieldDef.Traits]
+ """Compute sequence of FieldDef.Traits for a given protorpc FieldDef."""
+ trait_protos = []
+ if field_def.is_required:
+ trait_protos.append(project_objects_pb2.FieldDef.Traits.Value('REQUIRED'))
+ if field_def.is_niche:
+ trait_protos.append(
+ project_objects_pb2.FieldDef.Traits.Value('DEFAULT_HIDDEN'))
+ if field_def.is_multivalued:
+ trait_protos.append(
+ project_objects_pb2.FieldDef.Traits.Value('MULTIVALUED'))
+ if field_def.is_phase_field:
+ trait_protos.append(project_objects_pb2.FieldDef.Traits.Value('PHASE'))
+ if field_def.is_restricted_field:
+ trait_protos.append(
+ project_objects_pb2.FieldDef.Traits.Value('RESTRICTED'))
+ return trait_protos
+
+ def _GetEnumFieldChoices(self, field_def):
+ # type: (proto.tracker_pb2.FieldDef) ->
+ # Sequence[Choice]
+ """Get sequence of choices for an enum field
+
+ Args:
+ field_def: protorpc FieldDef
+
+ Returns:
+ Sequence of valid Choices for enum field `field_def`.
+
+ Raises:
+ ValueError if input `field_def` is not an enum type field.
+ """
+ if field_def.field_type != tracker_pb2.FieldTypes.ENUM_TYPE:
+ raise ValueError('Cannot get value from label for non-enum-type field')
+
+ config = self.services.config.GetProjectConfig(
+ self.cnxn, field_def.project_id)
+ value_docstr_tuples = tracker_helpers._GetEnumFieldValuesAndDocstrings(
+ field_def, config)
+
+ return [
+ Choice(value=value, docstring=docstring)
+ for value, docstring in value_docstr_tuples
+ ]
+
+ # Field Values
+
+ def _GetNonApprovalFieldValues(self, field_values, project_id):
+ # type: (Sequence[proto.tracker_pb2.FieldValue], int) ->
+ # Sequence[proto.tracker_pb2.FieldValue]
+ """Filter out field values that belong to an approval field."""
+ config = self.services.config.GetProjectConfig(self.cnxn, project_id)
+ approval_fd_ids = set(
+ [fd.field_id for fd in config.field_defs if fd.approval_id])
+
+ return [fv for fv in field_values if fv.field_id not in approval_fd_ids]
+
+ def ConvertFieldValues(self, field_values, project_id, phases):
+ # type: (Sequence[proto.tracker_pb2.FieldValue], int,
+ # Sequence[proto.tracker_pb2.Phase]) ->
+ # Sequence[api_proto.issue_objects_pb2.FieldValue]
+ """Convert sequence of field_values to protoc FieldValues.
+
+ This method does not handle enum_type fields.
+
+ Args:
+ field_values: List of FieldValues
+ project_id: ID of the Project that is ancestor to all given
+ `field_values`.
+ phases: List of Phases
+
+ Returns:
+ Sequence of protoc FieldValues in the same order they are given in
+ `field_values`. In the event any field_values in `field_values` are not
+ found, they will be omitted from the result.
+ """
+ phase_names_by_id = {phase.phase_id: phase.name for phase in phases}
+ field_ids = [fv.field_id for fv in field_values]
+ resource_names_dict = rnc.ConvertFieldDefNames(
+ self.cnxn, field_ids, project_id, self.services)
+
+ api_fvs = []
+ for fv in field_values:
+ if fv.field_id not in resource_names_dict:
+ continue
+
+ name = resource_names_dict.get(fv.field_id)
+ value = self._ComputeFieldValueString(fv)
+ derivation = self._ComputeFieldValueDerivation(fv)
+ phase = phase_names_by_id.get(fv.phase_id)
+ api_item = issue_objects_pb2.FieldValue(
+ field=name, value=value, derivation=derivation, phase=phase)
+ api_fvs.append(api_item)
+
+ return api_fvs
+
+ def _ComputeFieldValueString(self, field_value):
+ # type: (proto.tracker_pb2.FieldValue) -> str
+ """Convert a FieldValue's value to a string."""
+ if field_value is None:
+ raise exceptions.InputException('No FieldValue specified')
+ elif field_value.int_value is not None:
+ return str(field_value.int_value)
+ elif field_value.str_value is not None:
+ return field_value.str_value
+ elif field_value.user_id is not None:
+ return rnc.ConvertUserNames([field_value.user_id
+ ]).get(field_value.user_id)
+ elif field_value.date_value is not None:
+ return str(field_value.date_value)
+ elif field_value.url_value is not None:
+ return field_value.url_value
+ else:
+ raise exceptions.InputException('FieldValue must have at least one value')
+
+ def _ComputeFieldValueDerivation(self, field_value):
+ # type: (proto.tracker_pb2.FieldValue) ->
+ # api_proto.issue_objects_pb2.Issue.Derivation
+ """Convert a FieldValue's 'derived' to a protoc Issue.Derivation.
+
+ Args:
+ field_value: protorpc FieldValue
+
+ Returns:
+ Issue.Derivation of given `field_value`
+ """
+ if field_value.derived:
+ return issue_objects_pb2.Derivation.Value('RULE')
+ else:
+ return issue_objects_pb2.Derivation.Value('EXPLICIT')
+
+ # Approval Def
+
+ def ConvertApprovalDefs(self, approval_defs, project_id):
+ # type: (Sequence[proto.tracker_pb2.ApprovalDef], int) ->
+ # Sequence[api_proto.project_objects_pb2.ApprovalDef]
+ """Convert sequence of protorpc ApprovalDefs to protoc ApprovalDefs.
+
+ Args:
+ approval_defs: List of protorpc ApprovalDefs
+ project_id: ID of the Project the approval_defs belong to.
+
+ Returns:
+ Sequence of protoc ApprovalDefs in the same order they are given in
+ in `approval_defs`. In the event any approval_def in `approval_defs`
+ are not found, they will be omitted from the result.
+ """
+ approval_ids = set([ad.approval_id for ad in approval_defs])
+ resource_names_dict = rnc.ConvertApprovalDefNames(
+ self.cnxn, approval_ids, project_id, self.services)
+
+ # Get matching field defs, needed to fill out protoc ApprovalDefs
+ config = self.services.config.GetProjectConfig(self.cnxn, project_id)
+ fd_by_id = {}
+ for fd in config.field_defs:
+ if (fd.field_type == tracker_pb2.FieldTypes.APPROVAL_TYPE and
+ fd.field_id in approval_ids):
+ fd_by_id[fd.field_id] = fd
+
+ all_users = tbo.UsersInvolvedInApprovalDefs(
+ approval_defs, fd_by_id.values())
+ user_resource_names_dict = rnc.ConvertUserNames(all_users)
+
+ api_ads = []
+ for ad in approval_defs:
+ if (ad.approval_id not in resource_names_dict or
+ ad.approval_id not in fd_by_id):
+ continue
+ matching_fd = fd_by_id.get(ad.approval_id)
+ name = resource_names_dict.get(ad.approval_id)
+ display_name = matching_fd.field_name
+ docstring = matching_fd.docstring
+ survey = ad.survey
+ approvers = [
+ user_resource_names_dict.get(approver_id)
+ for approver_id in ad.approver_ids
+ ]
+ admins = [
+ user_resource_names_dict.get(admin_id)
+ for admin_id in matching_fd.admin_ids
+ ]
+
+ api_ad = project_objects_pb2.ApprovalDef(
+ name=name,
+ display_name=display_name,
+ docstring=docstring,
+ survey=survey,
+ approvers=approvers,
+ admins=admins)
+ api_ads.append(api_ad)
+ return api_ads
+
+ def ConvertApprovalValues(self, approval_values, field_values, phases,
+ issue_id=None, project_id=None):
+ # type: (Sequence[proto.tracker_pb2.ApprovalValue],
+ # Sequence[proto.tracker_pb2.FieldValue],
+ # Sequence[proto.tracker_pb2.Phase], Optional[int], Optional[int]) ->
+ # Sequence[api_proto.issue_objects_pb2.ApprovalValue]
+ """Convert sequence of approval_values to protoc ApprovalValues.
+
+ `approval_values` may belong to a template or an issue. If they belong to a
+ template, `project_id` should be given for the project the template is in.
+ If these are issue `approval_values` `issue_id` should be given`.
+ So, one of `issue_id` or `project_id` must be provided.
+ If both are given, we ignore `project_id` and assume the `approval_values`
+ belong to an issue.
+
+ Args:
+ approval_values: List of ApprovalValues.
+ field_values: List of FieldValues that may belong to the approval_values.
+ phases: List of Phases that may be associated with the approval_values.
+ issue_id: ID of the Issue that the `approval_values` belong to.
+ project_id: ID of the Project that the `approval_values`
+ template belongs to.
+
+ Returns:
+ Sequence of protoc ApprovalValues in the same order they are given in
+ in `approval_values`. In the event any approval definitions in
+ `approval_values` are not found, they will be omitted from the result.
+
+ Raises:
+ InputException if neither `issue_id` nor `project_id` is given.
+ """
+
+ approval_ids = [av.approval_id for av in approval_values]
+ resource_names_dict = {}
+ if issue_id is not None:
+ # Only issue approval_values have resource names.
+ resource_names_dict = rnc.ConvertApprovalValueNames(
+ self.cnxn, issue_id, self.services)
+ project_id = self.services.issue.GetIssue(self.cnxn, issue_id).project_id
+ elif project_id is None:
+ raise exceptions.InputException(
+ 'One `issue_id` or `project_id` must be given.')
+
+ phase_names_by_id = {phase.phase_id: phase.name for phase in phases}
+ ad_names_dict = rnc.ConvertApprovalDefNames(
+ self.cnxn, approval_ids, project_id, self.services)
+
+ # Organize the field values by the approval values they are
+ # associated with.
+ config = self.services.config.GetProjectConfig(self.cnxn, project_id)
+ fds_by_id = {fd.field_id: fd for fd in config.field_defs}
+ fvs_by_parent_approvals = collections.defaultdict(list)
+ for fv in field_values:
+ fd = fds_by_id.get(fv.field_id)
+ if fd and fd.approval_id:
+ fvs_by_parent_approvals[fd.approval_id].append(fv)
+
+ api_avs = []
+ for av in approval_values:
+ # We only skip missing approval names if we are converting issue approval
+ # values.
+ if issue_id is not None and av.approval_id not in resource_names_dict:
+ continue
+
+ name = resource_names_dict.get(av.approval_id)
+ approval_def = ad_names_dict.get(av.approval_id)
+ approvers = rnc.ConvertUserNames(av.approver_ids).values()
+ status = self._ComputeApprovalValueStatus(av.status)
+ setter = rnc.ConvertUserName(av.setter_id)
+ phase = phase_names_by_id.get(av.phase_id)
+
+ field_values = self.ConvertFieldValues(
+ fvs_by_parent_approvals[av.approval_id], project_id, phases)
+
+ api_item = issue_objects_pb2.ApprovalValue(
+ name=name,
+ approval_def=approval_def,
+ approvers=approvers,
+ status=status,
+ setter=setter,
+ field_values=field_values,
+ phase=phase)
+ if av.set_on:
+ api_item.set_time.FromSeconds(av.set_on)
+ api_avs.append(api_item)
+
+ return api_avs
+
+ def _ComputeApprovalValueStatus(self, status):
+ # type: (proto.tracker_pb2.ApprovalStatus) ->
+ # api_proto.issue_objects_pb2.Issue.ApprovalStatus
+ """Convert a protorpc ApprovalStatus to a protoc Issue.ApprovalStatus."""
+ try:
+ return _APPROVAL_STATUS_CONVERT[status]
+ except KeyError:
+ raise ValueError('Unrecognized tracker_pb2.ApprovalStatus enum')
+
+ # Projects
+
+ def ConvertIssueTemplates(self, project_id, templates):
+ # type: (int, Sequence[proto.tracker_pb2.TemplateDef]) ->
+ # Sequence[api_proto.project_objects_pb2.IssueTemplate]
+ """Convert a Sequence of TemplateDefs to protoc IssueTemplates.
+
+ Args:
+ project_id: ID of the Project the templates belong to.
+ templates: Sequence of TemplateDef protorpc objects.
+
+ Returns:
+ Sequence of protoc IssueTemplate in the same order they are given in
+ `templates`. In the rare event that any templates are not found,
+ they will be omitted from the result.
+ """
+ api_templates = []
+
+ resource_names_dict = rnc.ConvertTemplateNames(
+ self.cnxn, project_id, [template.template_id for template in templates],
+ self.services)
+
+ for template in templates:
+ if template.template_id not in resource_names_dict:
+ continue
+ name = resource_names_dict.get(template.template_id)
+ summary_must_be_edited = template.summary_must_be_edited
+ template_privacy = self._ComputeTemplatePrivacy(template)
+ default_owner = self._ComputeTemplateDefaultOwner(template)
+ component_required = template.component_required
+ admins = rnc.ConvertUserNames(template.admin_ids).values()
+ issue = self._FillIssueFromTemplate(template, project_id)
+ approval_values = self.ConvertApprovalValues(
+ template.approval_values, template.field_values, template.phases,
+ project_id=project_id)
+ api_templates.append(
+ project_objects_pb2.IssueTemplate(
+ name=name,
+ display_name=template.name,
+ issue=issue,
+ approval_values=approval_values,
+ summary_must_be_edited=summary_must_be_edited,
+ template_privacy=template_privacy,
+ default_owner=default_owner,
+ component_required=component_required,
+ admins=admins))
+
+ return api_templates
+
+ def _FillIssueFromTemplate(self, template, project_id):
+ # type: (proto.tracker_pb2.TemplateDef, int) ->
+ # api_proto.issue_objects_pb2.Issue
+ """Convert a TemplateDef to its embedded protoc Issue.
+
+ IssueTemplate does not set the following fields:
+ name
+ reporter
+ cc_users
+ blocked_on_issue_refs
+ blocking_issue_refs
+ create_time
+ close_time
+ modify_time
+ component_modify_time
+ status_modify_time
+ owner_modify_time
+ attachment_count
+ star_count
+
+ Args:
+ template: TemplateDef protorpc objects.
+ project_id: ID of the Project the template belongs to.
+
+ Returns:
+ protoc Issue filled with data from given `template`.
+ """
+ summary = template.summary
+ state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
+ status = issue_objects_pb2.Issue.StatusValue(
+ status=template.status,
+ derivation=issue_objects_pb2.Derivation.Value('EXPLICIT'))
+ owner = None
+ if template.owner_id is not None:
+ owner = issue_objects_pb2.Issue.UserValue(
+ user=rnc.ConvertUserNames([template.owner_id]).get(template.owner_id))
+ labels = self.ConvertLabels(template.labels, [], project_id)
+ components_dict = rnc.ConvertComponentDefNames(
+ self.cnxn, template.component_ids, project_id, self.services)
+ components = []
+ for component_resource_name in components_dict.values():
+ components.append(
+ issue_objects_pb2.Issue.ComponentValue(
+ component=component_resource_name,
+ derivation=issue_objects_pb2.Derivation.Value('EXPLICIT')))
+ non_approval_fvs = self._GetNonApprovalFieldValues(
+ template.field_values, project_id)
+ field_values = self.ConvertFieldValues(
+ non_approval_fvs, project_id, template.phases)
+ field_values.extend(
+ self.ConvertEnumFieldValues(template.labels, [], project_id))
+ phases = self._ComputePhases(template.phases)
+
+ filled_issue = issue_objects_pb2.Issue(
+ summary=summary,
+ state=state,
+ status=status,
+ owner=owner,
+ labels=labels,
+ components=components,
+ field_values=field_values,
+ phases=phases)
+ return filled_issue
+
+ def _ComputeTemplatePrivacy(self, template):
+ # type: (proto.tracker_pb2.TemplateDef) ->
+ # api_proto.project_objects_pb2.IssueTemplate.TemplatePrivacy
+ """Convert a protorpc TemplateDef to its protoc TemplatePrivacy."""
+ if template.members_only:
+ return project_objects_pb2.IssueTemplate.TemplatePrivacy.Value(
+ 'MEMBERS_ONLY')
+ else:
+ return project_objects_pb2.IssueTemplate.TemplatePrivacy.Value('PUBLIC')
+
+ def _ComputeTemplateDefaultOwner(self, template):
+ # type: (proto.tracker_pb2.TemplateDef) ->
+ # api_proto.project_objects_pb2.IssueTemplate.DefaultOwner
+ """Convert a protorpc TemplateDef to its protoc DefaultOwner."""
+ if template.owner_defaults_to_member:
+ return project_objects_pb2.IssueTemplate.DefaultOwner.Value(
+ 'PROJECT_MEMBER_REPORTER')
+ else:
+ return project_objects_pb2.IssueTemplate.DefaultOwner.Value(
+ 'DEFAULT_OWNER_UNSPECIFIED')
+
+ def _ComputePhases(self, phases):
+ # type: (proto.tracker_pb2.TemplateDef) -> Sequence[str]
+ """Convert a protorpc TemplateDef to its sorted string phases."""
+ sorted_phases = sorted(phases, key=lambda phase: phase.rank)
+ return [phase.name for phase in sorted_phases]
+
+ def ConvertLabels(self, labels, derived_labels, project_id):
+ # type: (Sequence[str], Sequence[str], int) ->
+ # Sequence[api_proto.issue_objects_pb2.Issue.LabelValue]
+ """Convert string labels to LabelValues for non-enum-field labels
+
+ Args:
+ labels: Sequence of string labels
+ project_id: ID of the Project these labels belong to.
+
+ Return:
+ Sequence of protoc IssueValues for given `labels` that
+ do not represent enum field values.
+ """
+ config = self.services.config.GetProjectConfig(self.cnxn, project_id)
+ non_fd_labels, non_fd_der_labels = tbo.ExplicitAndDerivedNonMaskedLabels(
+ labels, derived_labels, config)
+ api_labels = []
+ for label in non_fd_labels:
+ api_labels.append(
+ issue_objects_pb2.Issue.LabelValue(
+ label=label,
+ derivation=issue_objects_pb2.Derivation.Value('EXPLICIT')))
+ for label in non_fd_der_labels:
+ api_labels.append(
+ issue_objects_pb2.Issue.LabelValue(
+ label=label,
+ derivation=issue_objects_pb2.Derivation.Value('RULE')))
+ return api_labels
+
+ def ConvertEnumFieldValues(self, labels, derived_labels, project_id):
+ # type: (Sequence[str], Sequence[str], int) ->
+ # Sequence[api_proto.issue_objects_pb2.FieldValue]
+ """Convert string labels to FieldValues for enum-field labels
+
+ Args:
+ labels: Sequence of string labels
+ project_id: ID of the Project these labels belong to.
+
+ Return:
+ Sequence of protoc FieldValues only for given `labels` that
+ represent enum field values.
+ """
+ config = self.services.config.GetProjectConfig(self.cnxn, project_id)
+ enum_ids_by_name = {
+ fd.field_name.lower(): fd.field_id
+ for fd in config.field_defs
+ if fd.field_type is tracker_pb2.FieldTypes.ENUM_TYPE
+ and not fd.is_deleted
+ }
+ resource_names_dict = rnc.ConvertFieldDefNames(
+ self.cnxn, enum_ids_by_name.values(), project_id, self.services)
+
+ api_fvs = []
+
+ labels_by_prefix = tbo.LabelsByPrefix(labels, enum_ids_by_name.keys())
+ for lower_field_name, values in labels_by_prefix.items():
+ field_id = enum_ids_by_name.get(lower_field_name)
+ resource_name = resource_names_dict.get(field_id)
+ if not resource_name:
+ continue
+ api_fvs.extend(
+ [
+ issue_objects_pb2.FieldValue(
+ field=resource_name,
+ value=value,
+ derivation=issue_objects_pb2.Derivation.Value(
+ 'EXPLICIT')) for value in values
+ ])
+
+ der_labels_by_prefix = tbo.LabelsByPrefix(
+ derived_labels, enum_ids_by_name.keys())
+ for lower_field_name, values in der_labels_by_prefix.items():
+ field_id = enum_ids_by_name.get(lower_field_name)
+ resource_name = resource_names_dict.get(field_id)
+ if not resource_name:
+ continue
+ api_fvs.extend(
+ [
+ issue_objects_pb2.FieldValue(
+ field=resource_name,
+ value=value,
+ derivation=issue_objects_pb2.Derivation.Value('RULE'))
+ for value in values
+ ])
+
+ return api_fvs
+
+ def ConvertProject(self, project):
+ # type: (proto.project_pb2.Project) ->
+ # api_proto.project_objects_pb2.Project
+ """Convert a protorpc Project to its protoc Project."""
+
+ return project_objects_pb2.Project(
+ name=rnc.ConvertProjectName(
+ self.cnxn, project.project_id, self.services),
+ display_name=project.project_name,
+ summary=project.summary,
+ thumbnail_url=project_helpers.GetThumbnailUrl(project.logo_gcs_id))
+
+ def ConvertProjects(self, projects):
+ # type: (Sequence[proto.project_pb2.Project]) ->
+ # Sequence[api_proto.project_objects_pb2.Project]
+ """Convert a Sequence of protorpc Projects to protoc Projects."""
+ return [self.ConvertProject(proj) for proj in projects]
+
+ def ConvertProjectConfig(self, project_config):
+ # type: (proto.tracker_pb2.ProjectIssueConfig) ->
+ # api_proto.project_objects_pb2.ProjectConfig
+ """Convert protorpc ProjectIssueConfig to protoc ProjectConfig."""
+ project = self.services.project.GetProject(
+ self.cnxn, project_config.project_id)
+ project_grid_config = project_objects_pb2.ProjectConfig.GridViewConfig(
+ default_x_attr=project_config.default_x_attr,
+ default_y_attr=project_config.default_y_attr)
+ template_names = rnc.ConvertTemplateNames(
+ self.cnxn, project_config.project_id, [
+ project_config.default_template_for_developers,
+ project_config.default_template_for_users
+ ], self.services)
+ return project_objects_pb2.ProjectConfig(
+ name=rnc.ConvertProjectConfigName(
+ self.cnxn, project_config.project_id, self.services),
+ exclusive_label_prefixes=project_config.exclusive_label_prefixes,
+ member_default_query=project_config.member_default_query,
+ default_sort=project_config.default_sort_spec,
+ default_columns=self._ComputeIssuesListColumns(
+ project_config.default_col_spec),
+ project_grid_config=project_grid_config,
+ member_default_template=template_names.get(
+ project_config.default_template_for_developers),
+ non_members_default_template=template_names.get(
+ project_config.default_template_for_users),
+ revision_url_format=project.revision_url_format,
+ custom_issue_entry_url=project_config.custom_issue_entry_url)
+
+ def CreateProjectMember(self, cnxn, project_id, user_id, role):
+ # type: (MonorailContext, int, int, str) ->
+ # api_proto.project_objects_pb2.ProjectMember
+ """Creates a ProjectMember object from specified parameters.
+
+ Args:
+ cnxn: MonorailConnection object.
+ project_id: ID of the Project the User is a member of.
+ user_id: ID of the user who is a member.
+ role: str specifying the user's role based on a ProjectRole value.
+
+ Return:
+ A protoc ProjectMember object.
+ """
+ name = rnc.ConvertProjectMemberName(
+ cnxn, project_id, user_id, self.services)
+ return project_objects_pb2.ProjectMember(
+ name=name,
+ role=project_objects_pb2.ProjectMember.ProjectRole.Value(role))
+
+ def ConvertLabelDefs(self, label_defs, project_id):
+ # type: (Sequence[proto.tracker_pb2.LabelDef], int) ->
+ # Sequence[api_proto.project_objects_pb2.LabelDef]
+ """Convert protorpc LabelDefs to protoc LabelDefs"""
+ resource_names_dict = rnc.ConvertLabelDefNames(
+ self.cnxn, [ld.label for ld in label_defs], project_id, self.services)
+
+ api_lds = []
+ for ld in label_defs:
+ state = project_objects_pb2.LabelDef.LabelDefState.Value('ACTIVE')
+ if ld.deprecated:
+ state = project_objects_pb2.LabelDef.LabelDefState.Value('DEPRECATED')
+ api_lds.append(
+ project_objects_pb2.LabelDef(
+ name=resource_names_dict.get(ld.label),
+ value=ld.label,
+ docstring=ld.label_docstring,
+ state=state))
+ return api_lds
+
+ def ConvertStatusDefs(self, status_defs, project_id):
+ # type: (Sequence[proto.tracker_pb2.StatusDef], int) ->
+ # Sequence[api_proto.project_objects_pb2.StatusDef]
+ """Convert protorpc StatusDefs to protoc StatusDefs
+
+ Args:
+ status_defs: Sequence of StatusDefs.
+ project_id: ID of the Project these belong to.
+
+ Returns:
+ Sequence of protoc StatusDefs in the same order they are given in
+ `status_defs`.
+ """
+ resource_names_dict = rnc.ConvertStatusDefNames(
+ self.cnxn, [sd.status for sd in status_defs], project_id, self.services)
+ config = self.services.config.GetProjectConfig(self.cnxn, project_id)
+ mergeable_statuses = set(config.statuses_offer_merge)
+
+ # Rank is only surfaced as positional value in well_known_statuses
+ rank_by_status = {}
+ for rank, sd in enumerate(config.well_known_statuses):
+ rank_by_status[sd.status] = rank
+
+ api_sds = []
+ for sd in status_defs:
+ state = project_objects_pb2.StatusDef.StatusDefState.Value('ACTIVE')
+ if sd.deprecated:
+ state = project_objects_pb2.StatusDef.StatusDefState.Value('DEPRECATED')
+
+ if sd.means_open:
+ status_type = project_objects_pb2.StatusDef.StatusDefType.Value('OPEN')
+ else:
+ if sd.status in mergeable_statuses:
+ status_type = project_objects_pb2.StatusDef.StatusDefType.Value(
+ 'MERGED')
+ else:
+ status_type = project_objects_pb2.StatusDef.StatusDefType.Value(
+ 'CLOSED')
+
+ api_sd = project_objects_pb2.StatusDef(
+ name=resource_names_dict.get(sd.status),
+ value=sd.status,
+ type=status_type,
+ rank=rank_by_status[sd.status],
+ docstring=sd.status_docstring,
+ state=state,
+ )
+ api_sds.append(api_sd)
+ return api_sds
+
+ def ConvertComponentDef(self, component_def):
+ # type: (proto.tracker_pb2.ComponentDef) ->
+ # api_proto.project_objects.ComponentDef
+ """Convert a protorpc ComponentDef to a protoc ComponentDef."""
+ return self.ConvertComponentDefs([component_def],
+ component_def.project_id)[0]
+
+ def ConvertComponentDefs(self, component_defs, project_id):
+ # type: (Sequence[proto.tracker_pb2.ComponentDef], int) ->
+ # Sequence[api_proto.project_objects.ComponentDef]
+ """Convert sequence of protorpc ComponentDefs to protoc ComponentDefs
+
+ Args:
+ component_defs: Sequence of protoc ComponentDefs.
+ project_id: ID of the Project these belong to.
+
+ Returns:
+ Sequence of protoc ComponentDefs in the same order they are given in
+ `component_defs`.
+ """
+ resource_names_dict = rnc.ConvertComponentDefNames(
+ self.cnxn, [cd.component_id for cd in component_defs], project_id,
+ self.services)
+ involved_user_ids = tbo.UsersInvolvedInComponents(component_defs)
+ user_resource_names_dict = rnc.ConvertUserNames(involved_user_ids)
+
+ all_label_ids = set()
+ for cd in component_defs:
+ all_label_ids.update(cd.label_ids)
+
+ # If this becomes a performance issue, we should add bulk look up.
+ labels_by_id = {
+ label_id: self.services.config.LookupLabel(
+ self.cnxn, project_id, label_id) for label_id in all_label_ids
+ }
+
+ api_cds = []
+ for cd in component_defs:
+ state = project_objects_pb2.ComponentDef.ComponentDefState.Value('ACTIVE')
+ if cd.deprecated:
+ state = project_objects_pb2.ComponentDef.ComponentDefState.Value(
+ 'DEPRECATED')
+
+ api_cd = project_objects_pb2.ComponentDef(
+ name=resource_names_dict.get(cd.component_id),
+ value=cd.path,
+ docstring=cd.docstring,
+ state=state,
+ admins=[
+ user_resource_names_dict.get(admin_id)
+ for admin_id in cd.admin_ids
+ ],
+ ccs=[user_resource_names_dict.get(cc_id) for cc_id in cd.cc_ids],
+ creator=user_resource_names_dict.get(cd.creator_id),
+ modifier=user_resource_names_dict.get(cd.modifier_id),
+ create_time=timestamp_pb2.Timestamp(seconds=cd.created),
+ modify_time=timestamp_pb2.Timestamp(seconds=cd.modified),
+ labels=[labels_by_id[label_id] for label_id in cd.label_ids],
+ )
+ api_cds.append(api_cd)
+ return api_cds
+
+ def ConvertProjectSavedQueries(self, saved_queries, project_id):
+ # type: (Sequence[proto.tracker_pb2.SavedQuery], int) ->
+ # Sequence(api_proto.project_objects.ProjectSavedQuery)
+ """Convert sequence of protorpc SavedQueries to protoc ProjectSavedQueries
+
+ Args:
+ saved_queries: Sequence of SavedQueries.
+ project_id: ID of the Project these belong to.
+
+ Returns:
+ Sequence of protoc ProjectSavedQueries in the same order they are given in
+ `saved_queries`. In the event any items in `saved_queries` are not found
+ or don't belong to the project, they will be omitted from the result.
+ """
+ resource_names_dict = rnc.ConvertProjectSavedQueryNames(
+ self.cnxn, [sq.query_id for sq in saved_queries], project_id,
+ self.services)
+ api_psqs = []
+ for sq in saved_queries:
+ if sq.query_id not in resource_names_dict:
+ continue
+
+ # TODO(crbug/monorail/7756): Remove base_query_id, avoid confusions.
+ # Until then we have to expand the query by including base_query_id.
+ # base_query_id can only be in the set of DEFAULT_CANNED_QUERIES.
+ if sq.base_query_id:
+ query = '{} {}'.format(tbo.GetBuiltInQuery(sq.base_query_id), sq.query)
+ else:
+ query = sq.query
+
+ api_psqs.append(
+ project_objects_pb2.ProjectSavedQuery(
+ name=resource_names_dict.get(sq.query_id),
+ display_name=sq.name,
+ query=query))
+ return api_psqs
diff --git a/api/v3/frontend_servicer.py b/api/v3/frontend_servicer.py
new file mode 100644
index 0000000..7374f1b
--- /dev/null
+++ b/api/v3/frontend_servicer.py
@@ -0,0 +1,107 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from api import resource_name_converters as rnc
+from api.v3 import monorail_servicer
+from api.v3.api_proto import frontend_pb2
+from api.v3.api_proto import project_objects_pb2
+from api.v3.api_proto import frontend_prpc_pb2
+from businesslogic import work_env
+
+
+class FrontendServicer(monorail_servicer.MonorailServicer):
+ """Handle frontend specific API requests.
+ Each API request is implemented with a method as defined in the
+ .proto file. Each method does any request-specific validation, uses work_env
+ to safely operate on business objects, and returns a response proto.
+ """
+
+ DESCRIPTION = frontend_prpc_pb2.FrontendServiceDescription
+
+ @monorail_servicer.PRPCMethod
+ def GatherProjectEnvironment(self, mc, request):
+ # type: (MonorailContext, GatherProjectEnvironmentRequest) ->
+ # GatherProjectEnvironmentResponse
+ """pRPC API method that implements GatherProjectEnvironment.
+
+ Raises:
+ InputException if the project resource name provided is invalid.
+ NoSuchProjectException if the parent project is not found.
+ PermissionException if user is not allowed to view this project.
+ """
+
+ project_id = rnc.IngestProjectName(mc.cnxn, request.parent, self.services)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ project = we.GetProject(project_id)
+ project_config = we.GetProjectConfig(project_id)
+
+ api_project = self.converter.ConvertProject(project)
+ api_project_config = self.converter.ConvertProjectConfig(project_config)
+ api_status_defs = self.converter.ConvertStatusDefs(
+ project_config.well_known_statuses, project_id)
+ api_label_defs = self.converter.ConvertLabelDefs(
+ project_config.well_known_labels, project_id)
+ api_component_defs = self.converter.ConvertComponentDefs(
+ project_config.component_defs, project_id)
+ api_field_defs = self.converter.ConvertFieldDefs(
+ project_config.field_defs, project_id)
+ api_approval_defs = self.converter.ConvertApprovalDefs(
+ project_config.approval_defs, project_id)
+ saved_queries = self.services.features.GetCannedQueriesByProjectID(
+ mc.cnxn, project_id)
+ api_sqs = self.converter.ConvertProjectSavedQueries(
+ saved_queries, project_id)
+
+ return frontend_pb2.GatherProjectEnvironmentResponse(
+ project=api_project,
+ project_config=api_project_config,
+ statuses=api_status_defs,
+ well_known_labels=api_label_defs,
+ components=api_component_defs,
+ fields=api_field_defs,
+ approval_fields=api_approval_defs,
+ saved_queries=api_sqs)
+
+ @monorail_servicer.PRPCMethod
+ def GatherProjectMembershipsForUser(self, mc, request):
+ # type: (MonorailContext, GatherProjectMembershipsForUserRequest) ->
+ # GatherProjectMembershipsForUserResponse
+ """pRPC API method that implements GatherProjectMembershipsForUser.
+
+ Raises:
+ NoSuchUserException if the user is not found.
+ InputException if the user resource name is invalid.
+ """
+
+ user_id = rnc.IngestUserName(mc.cnxn, request.user, self.services)
+
+ project_memberships = []
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ owner, committer, contributor = we.GatherProjectMembershipsForUser(
+ user_id)
+
+ for project_id in owner:
+ project_member = self.converter.CreateProjectMember(
+ mc.cnxn, project_id, user_id, 'OWNER')
+ project_memberships.append(project_member)
+
+ for project_id in committer:
+ project_member = self.converter.CreateProjectMember(
+ mc.cnxn, project_id, user_id, 'COMMITTER')
+ project_memberships.append(project_member)
+
+ for project_id in contributor:
+ project_member = self.converter.CreateProjectMember(
+ mc.cnxn, project_id, user_id, 'CONTRIBUTOR')
+ project_memberships.append(project_member)
+
+ return frontend_pb2.GatherProjectMembershipsForUserResponse(
+ project_memberships=project_memberships)
diff --git a/api/v3/hotlists_servicer.py b/api/v3/hotlists_servicer.py
new file mode 100644
index 0000000..2ea2a31
--- /dev/null
+++ b/api/v3/hotlists_servicer.py
@@ -0,0 +1,266 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from google.protobuf import empty_pb2
+
+from api import resource_name_converters as rnc
+from api.v3 import monorail_servicer
+from api.v3.api_proto import feature_objects_pb2
+from api.v3.api_proto import hotlists_pb2
+from api.v3.api_proto import hotlists_prpc_pb2
+from businesslogic import work_env
+from framework import exceptions
+from features import features_constants
+from tracker import tracker_constants
+
+
+class HotlistsServicer(monorail_servicer.MonorailServicer):
+ """Handle API requests related to Hotlist objects.
+ Each API request is implemented with a method as defined in the
+ .proto file. Each method does any request-specific validation, uses work_env
+ to safely operate on business objects, and returns a response proto.
+ """
+ # NOTE(crbug/monorail/7614): Until the referenced cleanup is complete,
+ # all servicer methods that are scoped to a single Project need to call
+ # mc.LookupLoggedInUserPerms.
+ # Methods in this file do not because hotlists can span projects.
+
+ DESCRIPTION = hotlists_prpc_pb2.HotlistsServiceDescription
+
+ @monorail_servicer.PRPCMethod
+ def ListHotlistItems(self, mc, request):
+ # type: (MonorailContext, ListHotlistItemsRequest) ->
+ # ListHotlistItemsResponse
+ """pRPC API method that implements ListHotlistItems.
+
+ Raises:
+ NoSuchHotlistException if the hotlist is not found.
+ PermissionException if the user is not allowed to view the hotlist.
+ InputException if the request.page_token is invalid, the request does
+ not match the previous request that provided the given page_token, or
+ the page_size is a negative value.
+ """
+ hotlist_id = rnc.IngestHotlistName(request.parent)
+ if request.page_size < 0:
+ raise exceptions.InputException('`page_size` cannot be negative.')
+ page_size = request.page_size
+ if (not request.page_size or
+ request.page_size > features_constants.DEFAULT_RESULTS_PER_PAGE):
+ page_size = features_constants.DEFAULT_RESULTS_PER_PAGE
+
+ # TODO(crbug/monorail/7104): take start from request.page_token
+ start = 0
+ sort_spec = request.order_by.replace(',', ' ')
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ list_result = we.ListHotlistItems(
+ hotlist_id, page_size, start,
+ tracker_constants.ALL_ISSUES_CAN, sort_spec, '')
+
+ # TODO(crbug/monorail/7104): plug in next_page_token when it's been
+ # implemented.
+ next_page_token = ''
+ return hotlists_pb2.ListHotlistItemsResponse(
+ items=self.converter.ConvertHotlistItems(hotlist_id, list_result.items),
+ next_page_token=next_page_token)
+
+
+ @monorail_servicer.PRPCMethod
+ def RerankHotlistItems(self, mc, request):
+ # type: (MonorailContext, RerankHotlistItemsRequest) -> Empty
+ """pRPC API method that implements RerankHotlistItems.
+
+ Raises:
+ NoSuchHotlistException if the hotlist is not found.
+ PermissionException if the user is not allowed to rerank the hotlist.
+ InputException if request.target_position is invalid or
+ request.hotlist_items is empty or contains invalid items.
+ NoSuchIssueException if hotlist item does not exist.
+ """
+
+ hotlist_id = rnc.IngestHotlistName(request.name)
+ moved_issue_ids = rnc.IngestHotlistItemNames(
+ mc.cnxn, request.hotlist_items, self.services)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.RerankHotlistItems(
+ hotlist_id, moved_issue_ids, request.target_position)
+
+ return empty_pb2.Empty()
+
+
+ @monorail_servicer.PRPCMethod
+ def RemoveHotlistItems(self, mc, request):
+ # type: (MonorailContext, RemoveHotlistItemsRequest) -> Empty
+ """pPRC API method that implements RemoveHotlistItems.
+
+ Raises:
+ NoSuchHotlistException if the hotlist is not found.
+ PermissionException if the user is not allowed to edit the hotlist.
+ InputException if the items to be removed are not found in the hotlist.
+ """
+
+ hotlist_id = rnc.IngestHotlistName(request.parent)
+ remove_issue_ids = rnc.IngestIssueNames(
+ mc.cnxn, request.issues, self.services)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.RemoveHotlistItems(hotlist_id, remove_issue_ids)
+
+ return empty_pb2.Empty()
+
+
+ @monorail_servicer.PRPCMethod
+ def AddHotlistItems(self, mc, request):
+ # type: (MonorailContext, AddHotlistItemsRequest) -> Empty
+ """pRPC API method that implements AddHotlistItems.
+
+ Raises:
+ NoSuchHotlistException if the hotlist is not found.
+ PermissionException if the user is not allowed to edit the hotlist.
+ InputException if the request.target_position is invalid or the given
+ list of issues to add is empty or invalid.
+ """
+ hotlist_id = rnc.IngestHotlistName(request.parent)
+ new_issue_ids = rnc.IngestIssueNames(mc.cnxn, request.issues, self.services)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.AddHotlistItems(hotlist_id, new_issue_ids, request.target_position)
+
+ return empty_pb2.Empty()
+
+
+ @monorail_servicer.PRPCMethod
+ def RemoveHotlistEditors(self, mc, request):
+ # type: (MonorailContext, RemoveHotlistEditorsRequest) -> Empty
+ """pPRC API method that implements RemoveHotlistEditors.
+
+ Raises:
+ NoSuchHotlistException if the hotlist is not found.
+ PermissionException if the user is not allowed to edit the hotlist.
+ InputException if the editors to be removed are not found in the hotlist.
+ """
+
+ hotlist_id = rnc.IngestHotlistName(request.name)
+ remove_user_ids = rnc.IngestUserNames(
+ mc.cnxn, request.editors, self.services)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.RemoveHotlistEditors(hotlist_id, remove_user_ids)
+
+ return empty_pb2.Empty()
+
+
+ @monorail_servicer.PRPCMethod
+ def GetHotlist(self, mc, request):
+ # type: (MonorailContext, GetHotlistRequest) -> Hotlist
+ """pRPC API method that implements GetHotlist.
+
+ Raises:
+ InputException if the given name does not have a valid format.
+ NoSuchHotlistException if the hotlist is not found.
+ PermissionException if the user is not allowed to view the hotlist.
+ """
+
+ hotlist_id = rnc.IngestHotlistName(request.name)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ hotlist = we.GetHotlist(hotlist_id)
+
+ return self.converter.ConvertHotlist(hotlist)
+
+ @monorail_servicer.PRPCMethod
+ def GatherHotlistsForUser(self, mc, request):
+ # type: (MonorailContext, GatherHotlistsForUserRequest)
+ # -> GatherHotlistsForUserResponse
+ """pRPC API method that implements GatherHotlistsForUser.
+
+ Raises:
+ NoSuchUserException if the user is not found.
+ InputException if some request parameters are invalid.
+ """
+
+ user_id = rnc.IngestUserName(mc.cnxn, request.user, self.services)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ hotlists = we.ListHotlistsByUser(user_id)
+
+ return hotlists_pb2.GatherHotlistsForUserResponse(
+ hotlists=self.converter.ConvertHotlists(hotlists))
+
+ @monorail_servicer.PRPCMethod
+ def UpdateHotlist(self, mc, request):
+ # type: (MonorailContext, UpdateHotlistRequest) -> UpdateHotlistResponse
+ """pRPC API method that implements UpdateHotlist.
+
+ Raises:
+ NoSuchHotlistException if the hotlist is not found.
+ PermissionException if the user is not allowed to make this update.
+ InputException if some request parameters are required and missing or
+ invalid.
+ """
+ if not request.update_mask:
+ raise exceptions.InputException('No paths given in `update_mask`.')
+ if not request.hotlist:
+ raise exceptions.InputException('No `hotlist` param given.')
+
+ if not request.update_mask.IsValidForDescriptor(
+ feature_objects_pb2.Hotlist.DESCRIPTOR):
+ raise exceptions.InputException('Invalid `update_mask` for `hotlist`')
+
+ hotlist_id = rnc.IngestHotlistName(request.hotlist.name)
+
+ update_args = {}
+ hotlist = request.hotlist
+ for path in request.update_mask.paths:
+ if path == 'display_name':
+ update_args['hotlist_name'] = hotlist.display_name
+ elif path == 'owner':
+ owner_id = rnc.IngestUserName(mc.cnxn, hotlist.owner, self.services)
+ update_args['owner_id'] = owner_id
+ elif path == 'editors':
+ add_editor_ids = rnc.IngestUserNames(
+ mc.cnxn, hotlist.editors, self.services)
+ update_args['add_editor_ids'] = add_editor_ids
+ elif path == 'summary':
+ update_args['summary'] = hotlist.summary
+ elif path == 'description':
+ update_args['description'] = hotlist.description
+ elif path == 'hotlist_privacy':
+ update_args['is_private'] = (
+ hotlist.hotlist_privacy == feature_objects_pb2.Hotlist
+ .HotlistPrivacy.Value('PRIVATE'))
+ elif path == 'default_columns':
+ update_args[
+ 'default_col_spec'] = self.converter.IngestIssuesListColumns(
+ hotlist.default_columns)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.UpdateHotlist(hotlist_id, **update_args)
+ hotlist = we.GetHotlist(hotlist_id, use_cache=False)
+
+ return self.converter.ConvertHotlist(hotlist)
+
+ @monorail_servicer.PRPCMethod
+ def DeleteHotlist(self, mc, request):
+ # type: (MonorailContext, GetHotlistRequest) -> Empty
+ """pRPC API method that implements DeleteHotlist.
+
+ Raises:
+ InputException if the given name does not have a valid format.
+ NoSuchHotlistException if the hotlist is not found.
+ PermissionException if the user is not allowed to delete the hotlist.
+ """
+
+ hotlist_id = rnc.IngestHotlistName(request.name)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.DeleteHotlist(hotlist_id)
+
+ return empty_pb2.Empty()
diff --git a/api/v3/issues_servicer.py b/api/v3/issues_servicer.py
new file mode 100644
index 0000000..ebd545b
--- /dev/null
+++ b/api/v3/issues_servicer.py
@@ -0,0 +1,396 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import re
+
+from api import resource_name_converters as rnc
+from api.v3 import api_constants
+from api.v3 import converters
+from api.v3 import monorail_servicer
+from api.v3 import paginator
+from api.v3.api_proto import issues_pb2
+from api.v3.api_proto import issue_objects_pb2
+from api.v3.api_proto import issues_prpc_pb2
+from businesslogic import work_env
+from framework import exceptions
+
+# We accept only the following filter, and only on ListComments.
+# If we accept more complex filters in the future, introduce a library.
+_APPROVAL_DEF_FILTER_RE = re.compile(
+ r'approval = "(?P<approval_name>%s)"$' % rnc.APPROVAL_DEF_NAME_PATTERN)
+
+
+class IssuesServicer(monorail_servicer.MonorailServicer):
+ """Handle API requests related to Issue objects.
+ Each API request is implemented with a method as defined in the
+ .proto file that does any request-specific validation, uses work_env
+ to safely operate on business objects, and returns a response proto.
+ """
+
+ DESCRIPTION = issues_prpc_pb2.IssuesServiceDescription
+
+ @monorail_servicer.PRPCMethod
+ def GetIssue(self, mc, request):
+ # type: (MonorailContext, GetIssueRequest) -> Issue
+ """pRPC API method that implements GetIssue.
+
+ Raises:
+ InputException: the given name does not have a valid format.
+ NoSuchIssueException: the issue is not found.
+ PermissionException the user is not allowed to view the issue.
+ """
+ issue_id = rnc.IngestIssueName(mc.cnxn, request.name, self.services)
+ with work_env.WorkEnv(mc, self.services) as we:
+ # TODO(crbug/monorail/7614): Eliminate the need to do this lookup.
+ project = we.GetProjectByName(rnc.IngestProjectFromIssue(request.name))
+ mc.LookupLoggedInUserPerms(project)
+ issue = we.GetIssue(issue_id, allow_viewing_deleted=True)
+ return self.converter.ConvertIssue(issue)
+
+ @monorail_servicer.PRPCMethod
+ def BatchGetIssues(self, mc, request):
+ # type: (MonorailContext, BatchGetIssuesRequest) -> BatchGetIssuesResponse
+ """pRPC API method that implements BatchGetIssues.
+
+ Raises:
+ InputException: If `names` is formatted incorrectly. Or if a parent
+ collection in `names` does not match the value in `parent`.
+ NoSuchIssueException: If any of the given issues do not exist.
+ PermissionException If the requester does not have permission to view one
+ (or more) of the given issues.
+ """
+ if len(request.names) > api_constants.MAX_BATCH_ISSUES:
+ raise exceptions.InputException(
+ 'Requesting %d issues when the allowed maximum is %d issues.' %
+ (len(request.names), api_constants.MAX_BATCH_ISSUES))
+ if request.parent:
+ parent_match = rnc._GetResourceNameMatch(
+ request.parent, rnc.PROJECT_NAME_RE)
+ parent_project = parent_match.group('project_name')
+ with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
+ for name in request.names:
+ try:
+ name_match = rnc._GetResourceNameMatch(name, rnc.ISSUE_NAME_RE)
+ issue_project = name_match.group('project')
+ if issue_project != parent_project:
+ err_agg.AddErrorMessage(
+ '%s is not a child issue of %s.' % (name, request.parent))
+ except exceptions.InputException as e:
+ err_agg.AddErrorMessage(e.message)
+ with work_env.WorkEnv(mc, self.services) as we:
+ # NOTE(crbug/monorail/7614): Until the referenced cleanup is complete,
+ # all servicer methods that are scoped to a single Project need to call
+ # mc.LookupLoggedInUserPerms.
+ # This method does not because it may be scoped to multiple projects.
+ issue_ids = rnc.IngestIssueNames(mc.cnxn, request.names, self.services)
+ issues_by_iid = we.GetIssuesDict(issue_ids)
+ return issues_pb2.BatchGetIssuesResponse(
+ issues=self.converter.ConvertIssues(
+ [issues_by_iid[issue_id] for issue_id in issue_ids]))
+
+ @monorail_servicer.PRPCMethod
+ def SearchIssues(self, mc, request):
+ # type: (MonorailContext, SearchIssuesRequest) -> SearchIssuesResponse
+ """pRPC API method that implements SearchIssue.
+
+ Raises:
+ InputException: if any given names in `projects` are invalid or if the
+ search query uses invalid syntax (ie: unmatched parentheses).
+ """
+ page_size = paginator.CoercePageSize(
+ request.page_size, api_constants.MAX_ISSUES_PER_PAGE)
+ pager = paginator.Paginator(
+ page_size=page_size,
+ order_by=request.order_by,
+ query=request.query,
+ projects=request.projects)
+
+ project_names = []
+ for resource_name in request.projects:
+ match = rnc._GetResourceNameMatch(resource_name, rnc.PROJECT_NAME_RE)
+ project_names.append(match.group('project_name'))
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ # NOTE(crbug/monorail/7614): Until the referenced cleanup is complete,
+ # all servicer methods that are scoped to a single Project need to call
+ # mc.LookupLoggedInUserPerms.
+ # This method does not because it may be scoped to multiple projects.
+ list_result = we.SearchIssues(
+ request.query, project_names, mc.auth.user_id, page_size,
+ pager.GetStart(request.page_token), request.order_by)
+
+ return issues_pb2.SearchIssuesResponse(
+ issues=self.converter.ConvertIssues(list_result.items),
+ next_page_token=pager.GenerateNextPageToken(list_result.next_start))
+
+ @monorail_servicer.PRPCMethod
+ def ListComments(self, mc, request):
+ # type: (MonorailContext, ListCommentsRequest) -> ListCommentsResponse
+ """pRPC API method that implements ListComments.
+
+ Raises:
+ InputException: the given name format or page_size are not valid.
+ NoSuchIssueException: the parent is not found.
+ PermissionException: the user is not allowed to view the parent.
+ """
+ issue_id = rnc.IngestIssueName(mc.cnxn, request.parent, self.services)
+ page_size = paginator.CoercePageSize(
+ request.page_size, api_constants.MAX_COMMENTS_PER_PAGE)
+ pager = paginator.Paginator(
+ parent=request.parent, page_size=page_size, filter_str=request.filter)
+ approval_id = None
+ if request.filter:
+ match = _APPROVAL_DEF_FILTER_RE.match(request.filter)
+ if match:
+ approval_id = rnc.IngestApprovalDefName(
+ mc.cnxn, match.group('approval_name'), self.services)
+ if not match:
+ raise exceptions.InputException(
+ 'Filtering other than approval not supported.')
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ # TODO(crbug/monorail/7614): Eliminate the need to do this lookup.
+ project = we.GetProjectByName(rnc.IngestProjectFromIssue(request.parent))
+ mc.LookupLoggedInUserPerms(project)
+ list_result = we.SafeListIssueComments(
+ issue_id, page_size, pager.GetStart(request.page_token),
+ approval_id=approval_id)
+ return issues_pb2.ListCommentsResponse(
+ comments=self.converter.ConvertComments(issue_id, list_result.items),
+ next_page_token=pager.GenerateNextPageToken(list_result.next_start))
+
+ @monorail_servicer.PRPCMethod
+ def ListApprovalValues(self, mc, request):
+ # type: (MonorailContext, ListApprovalValuesRequest) ->
+ # ListApprovalValuesResponse
+ """pRPC API method that implements ListApprovalValues.
+
+ Raises:
+ InputException: the given parent does not have a valid format.
+ NoSuchIssueException: the parent issue is not found.
+ PermissionException the user is not allowed to view the parent issue.
+ """
+ issue_id = rnc.IngestIssueName(mc.cnxn, request.parent, self.services)
+ with work_env.WorkEnv(mc, self.services) as we:
+ # TODO(crbug/monorail/7614): Eliminate the need to do this lookup.
+ project = we.GetProjectByName(rnc.IngestProjectFromIssue(request.parent))
+ mc.LookupLoggedInUserPerms(project)
+ issue = we.GetIssue(issue_id)
+
+ api_avs = self.converter.ConvertApprovalValues(issue.approval_values,
+ issue.field_values, issue.phases, issue_id=issue_id)
+
+ return issues_pb2.ListApprovalValuesResponse(approval_values=api_avs)
+
+ @monorail_servicer.PRPCMethod
+ def MakeIssueFromTemplate(self, _mc, _request):
+ # type: (MonorailContext, MakeIssueFromTemplateRequest) -> Issue
+ """pRPC API method that implements MakeIssueFromTemplate.
+
+ Raises:
+ TODO(crbug/monorail/7197): Document errors when implemented
+ """
+ # Phase 1: Gather info
+ # Get project id and template name from template resource name.
+ # Get template pb.
+ # Make tracker_pb2.IssueDelta from request.template_issue_delta, share
+ # code with v3/ModifyIssue
+
+ # with work_env.WorkEnv(mc, self.services) as we:
+ # project = ... get project from template.
+ # mc.LookupLoggedInUserPerms(project)
+ # created_issue = we.MakeIssueFromTemplate(template, description, delta)
+
+ # Return newly created API issue.
+ # return converters.ConvertIssue(created_issue)
+
+ return issue_objects_pb2.Issue()
+
+ @monorail_servicer.PRPCMethod
+ def MakeIssue(self, mc, request):
+ # type: (MonorailContext, MakeIssueRequest) -> Issue
+ """pRPC API method that implements MakeIssue.
+
+ Raises:
+ InputException if any given names do not have a valid format or if any
+ fields in the requested issue were invalid.
+ NoSuchProjectException if no project exists with the given parent.
+ FilterRuleException if proposed issue values violate any filter rules
+ that shows error.
+ PermissionException if user lacks sufficient permissions.
+ """
+ project_id = rnc.IngestProjectName(mc.cnxn, request.parent, self.services)
+ with work_env.WorkEnv(mc, self.services) as we:
+ # TODO(crbug/monorail/7614): Eliminate the need to do this lookup.
+ project = we.GetProject(project_id)
+ mc.LookupLoggedInUserPerms(project)
+
+ ingested_issue = self.converter.IngestIssue(
+ request.issue, project_id)
+ send_email = self.converter.IngestNotifyType(request.notify_type)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ created_issue = we.MakeIssue(
+ ingested_issue, request.description, send_email)
+ starred_issue = we.StarIssue(created_issue, True)
+
+ return self.converter.ConvertIssue(starred_issue)
+
+ @monorail_servicer.PRPCMethod
+ def ModifyIssues(self, mc, request):
+ # type: (MonorailContext, ModifyIssuesRequest) -> ModifyIssuesResponse
+ """pRPC API method that implements ModifyIssues.
+
+ Raises:
+ InputException if any given names do not have a valid format or if any
+ fields in the requested issue were invalid.
+ NoSuchIssueException if some issues weren't found.
+ NoSuchProjectException if no project was found for some given issues.
+ FilterRuleException if proposed issue changes violate any filter rules
+ that shows error.
+ PermissionException if user lacks sufficient permissions.
+ """
+ if not request.deltas:
+ return issues_pb2.ModifyIssuesResponse()
+ if len(request.deltas) > api_constants.MAX_MODIFY_ISSUES:
+ raise exceptions.InputException(
+ 'Requesting %d updates when the allowed maximum is %d updates.' %
+ (len(request.deltas), api_constants.MAX_MODIFY_ISSUES))
+ impacted_issues_count = 0
+ for delta in request.deltas:
+ impacted_issues_count += (
+ len(delta.blocked_on_issues_remove) +
+ len(delta.blocking_issues_remove) +
+ len(delta.issue.blocking_issue_refs) +
+ len(delta.issue.blocked_on_issue_refs))
+ if 'merged_into_issue_ref' in delta.update_mask.paths:
+ impacted_issues_count += 1
+ if impacted_issues_count > api_constants.MAX_MODIFY_IMPACTED_ISSUES:
+ raise exceptions.InputException(
+ 'Updates include %d impacted issues when the allowed maximum is %d.' %
+ (impacted_issues_count, api_constants.MAX_MODIFY_IMPACTED_ISSUES))
+ iid_delta_pairs = self.converter.IngestIssueDeltas(request.deltas)
+ with work_env.WorkEnv(mc, self.services) as we:
+ issues = we.ModifyIssues(
+ iid_delta_pairs,
+ attachment_uploads=self.converter.IngestAttachmentUploads(
+ request.uploads),
+ comment_content=request.comment_content,
+ send_email=self.converter.IngestNotifyType(request.notify_type))
+
+ return issues_pb2.ModifyIssuesResponse(
+ issues=self.converter.ConvertIssues(issues))
+
+ @monorail_servicer.PRPCMethod
+ def ModifyIssueApprovalValues(self, mc, request):
+ # type: (MonorailContext, ModifyIssueApprovalValuesRequest) ->
+ # ModifyIssueApprovalValuesResponse
+ """pRPC API method that implements ModifyIssueApprovalValues.
+
+ Raises:
+ InputException if any fields in the delta were invalid.
+ NoSuchIssueException: if the issue of any ApprovalValue isn't found.
+ NoSuchProjectException: if the parent project of any ApprovalValue isn't
+ found.
+ NoSuchUserException: if any user value provided isn't found.
+ PermissionException if user lacks sufficient permissions.
+ # TODO(crbug/monorail/7925): Not all of these are yet thrown.
+ """
+ if len(request.deltas) > api_constants.MAX_MODIFY_APPROVAL_VALUES:
+ raise exceptions.InputException(
+ 'Requesting %d updates when the allowed maximum is %d updates.' %
+ (len(request.deltas), api_constants.MAX_MODIFY_APPROVAL_VALUES))
+ response = issues_pb2.ModifyIssueApprovalValuesResponse()
+ delta_specifications = self.converter.IngestApprovalDeltas(
+ request.deltas, mc.auth.user_id)
+ send_email = self.converter.IngestNotifyType(request.notify_type)
+ with work_env.WorkEnv(mc, self.services) as we:
+ # NOTE(crbug/monorail/7614): Until the referenced cleanup is complete,
+ # all servicer methods that are scoped to a single Project need to call
+ # mc.LookupLoggedInUserPerms.
+ # This method does not because it may be scoped to multiple projects.
+ issue_approval_values = we.BulkUpdateIssueApprovalsV3(
+ delta_specifications, request.comment_content, send_email=send_email)
+ api_avs = []
+ for issue, approval_value in issue_approval_values:
+ api_avs.extend(
+ self.converter.ConvertApprovalValues(
+ [approval_value],
+ issue.field_values,
+ issue.phases,
+ issue_id=issue.issue_id))
+ response.approval_values.extend(api_avs)
+ return response
+
+ @monorail_servicer.PRPCMethod
+ def ModifyCommentState(self, mc, request):
+ # type: (MonorailContext, ModifyCommentStateRequest) ->
+ # ModifyCommentStateResponse
+ """pRPC API method that implements ModifyCommentState.
+
+ We do not support changing between DELETED <-> SPAM. User must
+ undelete or unflag-as-spam first.
+
+ Raises:
+ NoSuchProjectException if the parent Project does not exist.
+ NoSuchIssueException: if the issue does not exist.
+ NoSuchCommentException: if the comment does not exist.
+ PermissionException if user lacks sufficient permissions.
+ ActionNotSupported if user requests unsupported state transitions.
+ """
+ (project_id, issue_id,
+ comment_num) = rnc.IngestCommentName(mc.cnxn, request.name, self.services)
+ with work_env.WorkEnv(mc, self.services) as we:
+ # TODO(crbug/monorail/7614): Eliminate the need to do this lookup.
+ project = we.GetProject(project_id)
+ mc.LookupLoggedInUserPerms(project)
+ issue = we.GetIssue(issue_id, use_cache=False)
+ comments_list = we.SafeListIssueComments(issue_id, 1, comment_num).items
+ try:
+ comment = comments_list[0]
+ except IndexError:
+ raise exceptions.NoSuchCommentException()
+
+ if request.state == issue_objects_pb2.IssueContentState.Value('ACTIVE'):
+ if comment.is_spam:
+ we.FlagComment(issue, comment, False)
+ elif comment.deleted_by != 0:
+ we.DeleteComment(issue, comment, delete=False)
+ else:
+ # No-op if already currently active
+ pass
+ elif request.state == issue_objects_pb2.IssueContentState.Value(
+ 'DELETED'):
+ if (not comment.deleted_by) and (not comment.is_spam):
+ we.DeleteComment(issue, comment, delete=True)
+ elif comment.deleted_by and not comment.is_spam:
+ # No-op if already deleted
+ pass
+ else:
+ raise exceptions.ActionNotSupported(
+ 'Cannot change comment state from spam to deleted.')
+ elif request.state == issue_objects_pb2.IssueContentState.Value('SPAM'):
+ if (not comment.deleted_by) and (not comment.is_spam):
+ we.FlagComment(issue, comment, True)
+ elif comment.is_spam:
+ # No-op if already spam
+ pass
+ else:
+ raise exceptions.ActionNotSupported(
+ 'Cannot change comment state from deleted to spam.')
+ else:
+ raise exceptions.ActionNotSupported('Unsupported target comment state.')
+
+ # FlagComment does not have side effect on comment, must refresh.
+ refreshed_comment = we.SafeListIssueComments(issue_id, 1,
+ comment_num).items[0]
+
+ converted_comment = self.converter.ConvertComments(
+ issue_id, [refreshed_comment])[0]
+ return issues_pb2.ModifyCommentStateResponse(comment=converted_comment)
diff --git a/api/v3/monorail_servicer.py b/api/v3/monorail_servicer.py
new file mode 100644
index 0000000..8f2e26e
--- /dev/null
+++ b/api/v3/monorail_servicer.py
@@ -0,0 +1,434 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import cgi
+import functools
+import logging
+import time
+import sys
+
+from google.oauth2 import id_token
+from google.auth.transport import requests as google_requests
+
+from google.appengine.api import oauth
+from google.appengine.api import users
+from google.appengine.api import app_identity
+from google.protobuf import json_format
+from components.prpc import codes
+from components.prpc import server
+
+from framework import monitoring
+
+import settings
+from api.v3 import converters
+from framework import authdata
+from framework import exceptions
+from framework import framework_constants
+from framework import monitoring
+from framework import monorailcontext
+from framework import ratelimiter
+from framework import permissions
+from framework import sql
+from framework import xsrf
+from services import client_config_svc
+from services import features_svc
+
+
+# Header for XSRF token to protect cookie-based auth users.
+XSRF_TOKEN_HEADER = 'x-xsrf-token'
+# Header for test account email. Only accepted for local dev server.
+TEST_ACCOUNT_HEADER = 'x-test-account'
+# Optional header to help us understand why certain calls were made.
+REASON_HEADER = 'x-reason'
+# Optional header to help prevent double updates.
+REQUEST_ID_HEADER = 'x-request-id'
+# Domain for service account emails.
+SERVICE_ACCOUNT_DOMAIN = 'gserviceaccount.com'
+
+
+def ConvertPRPCStatusToHTTPStatus(context):
+ """pRPC uses internal codes 0..16, but we want to report HTTP codes."""
+ return server._PRPC_TO_HTTP_STATUS.get(context._code, 500)
+
+
+def PRPCMethod(func):
+ @functools.wraps(func)
+ def wrapper(self, request, prpc_context, cnxn=None):
+ return self.Run(
+ func, request, prpc_context, cnxn=cnxn)
+
+ wrapper.wrapped = func
+ return wrapper
+
+
+class MonorailServicer(object):
+ """Abstract base class for API servicers.
+ """
+
+ def __init__(self, services, make_rate_limiter=True, xsrf_timeout=None):
+ self.services = services
+ if make_rate_limiter:
+ self.rate_limiter = ratelimiter.ApiRateLimiter()
+ else:
+ self.rate_limiter = None
+ # We allow subclasses to specify a different timeout. This allows the
+ # RefreshToken method to check the token with a longer expiration and
+ # generate a new one.
+ self.xsrf_timeout = xsrf_timeout or xsrf.TOKEN_TIMEOUT_SEC
+ self.converter = None
+
+ def Run(
+ self, handler, request, prpc_context,
+ cnxn=None, perms=None, start_time=None, end_time=None):
+ """Run a Do* method in an API context.
+
+ Args:
+ handler: API handler method to call with MonorailContext and request.
+ request: API Request proto object.
+ prpc_context: pRPC context object with status code.
+ cnxn: Optional connection to SQL database.
+ perms: PermissionSet passed in during testing.
+ start_time: Int timestamp passed in during testing.
+ end_time: Int timestamp passed in during testing.
+
+ Returns:
+ The response proto returned from the handler or None if that
+ method raised an exception that we handle.
+
+ Raises:
+ Only programming errors should be raised as exceptions. All
+ exceptions for permission checks and input validation that are
+ raised in the Do* method are converted into pRPC status codes.
+ """
+ start_time = start_time or time.time()
+ cnxn = cnxn or sql.MonorailConnection()
+ if self.services.cache_manager:
+ self.services.cache_manager.DoDistributedInvalidation(cnxn)
+
+ response = None
+ requester_auth = None
+ metadata = dict(prpc_context.invocation_metadata())
+ mc = monorailcontext.MonorailContext(self.services, cnxn=cnxn, perms=perms)
+ try:
+ self.AssertBaseChecks(request, metadata)
+ client_id, requester_auth = self.GetAndAssertRequesterAuth(
+ cnxn, metadata, self.services)
+ logging.info('request proto is:\n%r\n', request)
+ logging.info('requester is %r', requester_auth.email)
+ monitoring.IncrementAPIRequestsCount(
+ 'v3', client_id, client_email=requester_auth.email)
+
+ # TODO(crbug.com/monorail/8161)We pass in a None client_id for rate
+ # limiting because CheckStart and CheckEnd will track and limit requests
+ # per email and client_id separately.
+ # So if there are many site users one day, we may end up rate limiting our
+ # own site. With a None client_id we are only rate limiting by emails.
+ if self.rate_limiter:
+ self.rate_limiter.CheckStart(None, requester_auth.email, start_time)
+ mc.auth = requester_auth
+ if not perms:
+ # NOTE(crbug/monorail/7614): We rely on servicer methods to call
+ # to call LookupLoggedInUserPerms() with a project when they need to.
+ mc.LookupLoggedInUserPerms(None)
+
+ self.converter = converters.Converter(mc, self.services)
+ response = handler(self, mc, request)
+
+ except Exception as e:
+ if not self.ProcessException(e, prpc_context, mc):
+ raise e.__class__, e, sys.exc_info()[2]
+ finally:
+ if mc:
+ mc.CleanUp()
+ if self.rate_limiter and requester_auth and requester_auth.email:
+ end_time = end_time or time.time()
+ self.rate_limiter.CheckEnd(
+ None, requester_auth.email, end_time, start_time)
+ self.RecordMonitoringStats(start_time, request, response, prpc_context)
+
+ return response
+
+ def CheckIDToken(self, cnxn, metadata):
+ # type: (MonorailConnection, Mapping[str, str])
+ # -> Tuple[Optional[str], Optional[authdata.AuthData]]
+ """Authenticate user from an ID token.
+
+ Args:
+ cnxn: connection to the SQL database.
+ metadata: metadata sent by the client.
+
+ Returns:
+ The audience (AKA client_id) and a new AuthData object representing
+ the user making the request or (None, None) if no ID token was found.
+
+ Raises:
+ permissions.PermissionException: If the token is invalid, the client ID
+ is not allowlisted, or no user email was found in the ID token.
+ """
+ bearer = metadata.get('authorization')
+ if not bearer:
+ return None, None
+ if bearer.lower().startswith('bearer '):
+ token = bearer[7:]
+ else:
+ raise permissions.PermissionException('Invalid authorization token.')
+ # TODO(crbug.com/monorail/7724): Use cachecontrol module to cache
+ # certification used for verification.
+ request = google_requests.Request()
+
+ try:
+ id_info = id_token.verify_oauth2_token(token, request)
+ logging.info('ID token info: %r' % id_info)
+ except ValueError:
+ raise permissions.PermissionException(
+ 'Invalid bearer token.')
+
+ audience = id_info['aud']
+ email = id_info.get('email')
+ if not email:
+ raise permissions.PermissionException(
+ 'No email found in token info. '
+ 'Make sure requests are made with scopes `openid` and `email`')
+
+ auth_client_ids, service_account_emails = (
+ client_config_svc.GetClientConfigSvc().GetClientIDEmails())
+
+ if email.endswith(SERVICE_ACCOUNT_DOMAIN):
+ # For service accounts, the email must be allowlisted to call the
+ # API and we must confirm that the ID token was meant for
+ # Monorail by checking the audience.
+
+ # An API call to any <version>-dot-<service>-dot-<app_id>.appspot.com
+ # must have token audience of `https://<app_id>.appspot.com`
+ app_id = app_identity.get_application_id() # e.g. 'monorail-prod'
+ host = 'https://%s.appspot.com' % app_id
+ if audience != host:
+ raise permissions.PermissionException(
+ 'Invalid token audience: %s.' % audience)
+ if email not in service_account_emails:
+ raise permissions.PermissionException(
+ 'Account %s is not allowlisted' % email)
+ else:
+ # For users, the audience is the client_id of the site used to make
+ # the call to Monorail's API. The client_id must be allow-listed.
+ if audience not in auth_client_ids:
+ raise permissions.PermissionException(
+ 'Client %s is not allowlisted' % audience)
+
+ # We must confirm the client/email is allowlisted before we
+ # potentially auto-create the user account in Monorail.
+ return audience, authdata.AuthData.FromEmail(
+ cnxn, email, self.services, autocreate=True)
+
+ def GetAndAssertRequesterAuth(self, cnxn, metadata, services):
+ # type: (MonorailConnection, Mapping[str, str], Services ->
+ # Tuple[str, authdata.AuthData]
+ """Gets the requester identity and checks if the user has permission
+ to make the request.
+ Any users successfully authenticated with oauth must be allowlisted or
+ have accounts with the domains in api_allowed_email_domains.
+ Users identified using cookie-based auth must have valid XSRF tokens.
+ Test accounts ending with @example.com are only allowed in the
+ local_mode.
+
+ Args:
+ cnxn: connection to the SQL database.
+ metadata: metadata sent by the client.
+ services: connections to backend services.
+
+ Returns:
+ The client ID and a new AuthData object representing a signed in or
+ anonymous user.
+
+ Raises:
+ exceptions.NoSuchUserException: If the requester does not exist
+ permissions.BannedUserException: If the user has been banned from the site
+ permissions.PermissionException: If the user is not authorized with the
+ Monorail scope, is not allowlisted, and has an invalid token.
+ """
+ # TODO(monorail:6538): Move different authentication methods into separate
+ # functions.
+ requester_auth = None
+ client_id = None
+ # When running on localhost, allow request to specify test account.
+ if TEST_ACCOUNT_HEADER in metadata:
+ if not settings.local_mode:
+ raise exceptions.InputException(
+ 'x-test-account only accepted in local_mode')
+ # For local development, we accept any request.
+ # TODO(jrobbins): make this more realistic by requiring a fake XSRF token.
+ test_account = metadata[TEST_ACCOUNT_HEADER]
+ if not test_account.endswith('@example.com'):
+ raise exceptions.InputException(
+ 'test_account must end with @example.com')
+ logging.info('Using test_account: %r' % test_account)
+ requester_auth = authdata.AuthData.FromEmail(cnxn, test_account, services)
+
+ # Oauth2 ID token auth.
+ if not requester_auth:
+ client_id, requester_auth = self.CheckIDToken(cnxn, metadata)
+
+ if client_id is None:
+ # TODO(crbug.com/monorail/8160): For site users, we temporarily use
+ # the host as the client_id, until we implement auth in the frontend
+ # to make API requests with ID tokens that include client_ids.
+ client_id = 'https://%s.appspot.com' % app_identity.get_application_id()
+
+
+ # Cookie-based auth for signed in and anonymous users.
+ if not requester_auth:
+ # Check for signed in user
+ user = users.get_current_user()
+ if user:
+ logging.info('Using cookie user: %r', user.email())
+ requester_auth = authdata.AuthData.FromEmail(
+ cnxn, user.email(), services)
+ else:
+ # Create AuthData for anonymous user.
+ requester_auth = authdata.AuthData.FromEmail(cnxn, None, services)
+
+ # Cookie-based auth signed-in and anon users need to have the XSRF
+ # token validate.
+ try:
+ token = metadata.get(XSRF_TOKEN_HEADER)
+ xsrf.ValidateToken(
+ token, requester_auth.user_id, xsrf.XHR_SERVLET_PATH,
+ timeout=self.xsrf_timeout)
+ except xsrf.TokenIncorrect:
+ raise permissions.PermissionException(
+ 'Requester %s does not have permission to make this request.'
+ % requester_auth.email)
+
+ if permissions.IsBanned(requester_auth.user_pb, requester_auth.user_view):
+ raise permissions.BannedUserException(
+ 'The user %s has been banned from using this site' %
+ requester_auth.email)
+
+ return (client_id, requester_auth)
+
+ def AssertBaseChecks(self, request, metadata):
+ """Reject requests that we refuse to serve."""
+ # TODO(jrobbins): Add read_only check as an exception raised in sql.py.
+ if (settings.read_only and
+ not request.__class__.__name__.startswith(('Get', 'List'))):
+ raise permissions.PermissionException(
+ 'This request is not allowed in read-only mode')
+
+ if REASON_HEADER in metadata:
+ logging.info('Request reason: %r', metadata[REASON_HEADER])
+ if REQUEST_ID_HEADER in metadata:
+ # TODO(jrobbins): Ignore requests with duplicate request_ids.
+ logging.info('request_id: %r', metadata[REQUEST_ID_HEADER])
+
+ def ProcessException(self, e, prpc_context, mc):
+ """Return True if we convert an exception to a pRPC status code."""
+ logging.exception(e)
+ logging.info(e.message)
+ exc_type = type(e)
+ if exc_type == exceptions.NoSuchUserException:
+ prpc_context.set_code(codes.StatusCode.NOT_FOUND)
+ prpc_context.set_details('The user does not exist.')
+ elif exc_type == exceptions.NoSuchProjectException:
+ prpc_context.set_code(codes.StatusCode.NOT_FOUND)
+ prpc_context.set_details('The project does not exist.')
+ elif exc_type == exceptions.NoSuchTemplateException:
+ prpc_context.set_code(codes.StatusCode.NOT_FOUND)
+ prpc_context.set_details('The template does not exist.')
+ elif exc_type == exceptions.NoSuchIssueException:
+ prpc_context.set_code(codes.StatusCode.NOT_FOUND)
+ details = 'The issue does not exist.'
+ if e.message:
+ details = cgi.escape(e.message, quote=True)
+ prpc_context.set_details(details)
+ elif exc_type == exceptions.NoSuchIssueApprovalException:
+ prpc_context.set_code(codes.StatusCode.NOT_FOUND)
+ prpc_context.set_details('The issue approval does not exist.')
+ elif exc_type == exceptions.NoSuchCommentException:
+ prpc_context.set_code(codes.StatusCode.INVALID_ARGUMENT)
+ prpc_context.set_details('No such comment')
+ elif exc_type == exceptions.NoSuchComponentException:
+ prpc_context.set_code(codes.StatusCode.NOT_FOUND)
+ prpc_context.set_details('The component does not exist.')
+ elif exc_type == permissions.BannedUserException:
+ prpc_context.set_code(codes.StatusCode.PERMISSION_DENIED)
+ prpc_context.set_details('The requesting user has been banned.')
+ elif exc_type == permissions.PermissionException:
+ logging.info('perms is %r', mc.perms)
+ prpc_context.set_code(codes.StatusCode.PERMISSION_DENIED)
+ prpc_context.set_details('Permission denied.')
+ elif exc_type == exceptions.GroupExistsException:
+ prpc_context.set_code(codes.StatusCode.ALREADY_EXISTS)
+ prpc_context.set_details('The user group already exists.')
+ elif exc_type == features_svc.HotlistAlreadyExists:
+ prpc_context.set_code(codes.StatusCode.ALREADY_EXISTS)
+ prpc_context.set_details('A hotlist with that name already exists.')
+ elif exc_type == exceptions.ComponentDefAlreadyExists:
+ prpc_context.set_code(codes.StatusCode.ALREADY_EXISTS)
+ prpc_context.set_details('A component with that path already exists.')
+ elif exc_type == exceptions.ActionNotSupported:
+ prpc_context.set_code(codes.StatusCode.INVALID_ARGUMENT)
+ prpc_context.set_details('Requested action not supported.')
+ elif exc_type == exceptions.InvalidComponentNameException:
+ prpc_context.set_code(codes.StatusCode.INVALID_ARGUMENT)
+ prpc_context.set_details('That component name is invalid.')
+ elif exc_type == exceptions.FilterRuleException:
+ prpc_context.set_code(codes.StatusCode.INVALID_ARGUMENT)
+ prpc_context.set_details('Violates filter rule that should error.')
+ elif exc_type == exceptions.InputException:
+ prpc_context.set_code(codes.StatusCode.INVALID_ARGUMENT)
+ prpc_context.set_details(
+ 'Invalid arguments: %s' % cgi.escape(e.message, quote=True))
+ elif exc_type == exceptions.OverAttachmentQuota:
+ prpc_context.set_code(codes.StatusCode.RESOURCE_EXHAUSTED)
+ prpc_context.set_details(
+ 'The request would exceed the attachment quota limit.')
+ elif exc_type == ratelimiter.ApiRateLimitExceeded:
+ prpc_context.set_code(codes.StatusCode.PERMISSION_DENIED)
+ prpc_context.set_details('The requester has exceeded API quotas limit.')
+ elif exc_type == oauth.InvalidOAuthTokenError:
+ prpc_context.set_code(codes.StatusCode.UNAUTHENTICATED)
+ prpc_context.set_details(
+ 'The oauth token was not valid or must be refreshed.')
+ elif exc_type == xsrf.TokenIncorrect:
+ logging.info('Bad XSRF token: %r', e.message)
+ prpc_context.set_code(codes.StatusCode.INVALID_ARGUMENT)
+ prpc_context.set_details('Bad XSRF token.')
+ elif exc_type == exceptions.PageTokenException:
+ prpc_context.set_code(codes.StatusCode.INVALID_ARGUMENT)
+ prpc_context.set_details(
+ 'Page token invalid or incorrect for the accompanying request')
+ else:
+ prpc_context.set_code(codes.StatusCode.INTERNAL)
+ prpc_context.set_details('Potential programming error.')
+ return False # Re-raise any exception from programming errors.
+ return True # It if was one of the cases above, don't reraise.
+
+ def RecordMonitoringStats(
+ self, start_time, request, response, prpc_context, now=None):
+ """Record monitoring info about this request."""
+ now = now or time.time()
+ elapsed_ms = int((now - start_time) * 1000)
+ method_name = request.__class__.__name__
+ if method_name.endswith('Request'):
+ method_name = method_name[:-len('Request')]
+
+ fields = monitoring.GetCommonFields(
+ # pRPC uses its own statuses, but we report HTTP status codes.
+ ConvertPRPCStatusToHTTPStatus(prpc_context),
+ # Use the API name, not the request path, to prevent an explosion in
+ # possible field values.
+ 'monorail.v3.' + method_name)
+ monitoring.AddServerDurations(elapsed_ms, fields)
+ monitoring.IncrementServerResponseStatusCount(fields)
+ monitoring.AddServerRequesteBytes(
+ len(json_format.MessageToJson(request)), fields)
+ response_length = 0
+ if response:
+ response_length = len(json_format.MessageToJson(response))
+ monitoring.AddServerResponseBytes(response_length, fields)
diff --git a/api/v3/paginator.py b/api/v3/paginator.py
new file mode 100644
index 0000000..16e66fa
--- /dev/null
+++ b/api/v3/paginator.py
@@ -0,0 +1,91 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from framework import exceptions
+from framework import paginate
+from proto import secrets_pb2
+
+
+def CoercePageSize(page_size, max_size, default_size=None):
+ # type: (int, int, Optional[int]) -> int
+ """Validates page_size and coerces it to max_size if needed.
+
+ Args:
+ page_size: The page_size requested by the user.
+ max_size: the maximum page size allowed. Must be > 0.
+ Also used as default if default_size not provided
+ default_size: default size to use if page_size not provided. Must be > 0.
+
+ Returns:
+ The appropriate page size to use for the request, based on the parameters.
+ Specifically this means
+ - page_size if not greater than max_size
+ - max_size if page_size > max_size
+ - max_size if page_size is not provided and default_size is not provided
+ - default_size if page_size is not provided
+
+ Raises:
+ InputException: if page_size is negative.
+ """
+ # These are programming errors. They are not user input.
+ assert max_size > 0
+ assert default_size is None or default_size > 0
+
+ # Check for invalid user provided page_size.
+ if page_size and page_size < 0:
+ raise exceptions.InputException('`page_size` cannot be negative.')
+
+ if not page_size:
+ return default_size or max_size
+ if page_size > max_size:
+ return max_size
+ return page_size
+
+
+class Paginator(object):
+ """Class to manage API pagination.
+
+ Paginator handles the pagination tasks and info of a single List or
+ Search API method implementation, given the contents of the request.
+ """
+
+ def __init__(self, parent=None, page_size=None, order_by=None,
+ filter_str=None, query=None, projects=None):
+ # type: (Optional[str], Optional[int], Optional[str], Optional[str],
+ # Optional[str], Optional[Collection[str]]]) -> None
+ self.request_contents = secrets_pb2.ListRequestContents(
+ parent=parent, page_size=page_size, order_by=order_by,
+ filter=filter_str, query=query, projects=projects)
+
+ def GetStart(self, page_token):
+ # type: (Optional[str]) -> int
+ """Validates a request.page_token and returns the start index for it."""
+ if page_token:
+ # TODO(crbug.com/monorail/6758): Proto string fields are unicode types in
+ # python 2. In python 3 these unicode strings will be represented with
+ # string types. paginate.ValidateAndParsePageToken requires a string token
+ # during validation (compare_digest()). Once we move to python 3, we can
+ # remove this string casting.
+ token = str(page_token)
+ return paginate.ValidateAndParsePageToken(token, self.request_contents)
+ return 0
+
+ def GenerateNextPageToken(self, next_start):
+ # type: (Optional[int]) -> str
+ """Generates the `next_page_token` for the API response.
+
+ Args:
+ next_start: The start index of the next page, or None if no more results.
+
+ Returns:
+ A string clients can use to request the next page. Returns None if
+ next_start was None
+ """
+ if next_start is None:
+ return None
+ return paginate.GeneratePageToken(self.request_contents, next_start)
diff --git a/api/v3/permission_converters.py b/api/v3/permission_converters.py
new file mode 100644
index 0000000..6837438
--- /dev/null
+++ b/api/v3/permission_converters.py
@@ -0,0 +1,62 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from framework import permissions
+from framework import exceptions
+from api.v3.api_proto import permission_objects_pb2
+
+# Global dictionaries to map backend permission strings to
+# API Permission enum values.
+
+HOTLIST_PERMISSIONS_MAP = {
+ permissions.EDIT_HOTLIST:
+ permission_objects_pb2.Permission.Value('HOTLIST_EDIT'),
+ permissions.ADMINISTER_HOTLIST:
+ permission_objects_pb2.Permission.Value('HOTLIST_ADMINISTER')
+}
+
+FIELDDEF_PERMISSIONS_MAP = {
+ permissions.EDIT_FIELD_DEF:
+ permission_objects_pb2.Permission.Value('FIELD_DEF_EDIT'),
+ permissions.EDIT_FIELD_DEF_VALUE:
+ permission_objects_pb2.Permission.Value('FIELD_DEF_VALUE_EDIT')
+}
+
+# TODO(crbug/monorail/7339): Create a common _ConvertPermissions(permissions,
+# permissions_map)
+
+
+def ConvertHotlistPermissions(hotlist_permissions):
+ # type: (Sequence[str]) -> Sequence[permission_objects_pb2.Permission]
+ """Converts hotlist permission strings into protoc Permission enum values."""
+ api_permissions = []
+ for permission in hotlist_permissions:
+ api_permission = HOTLIST_PERMISSIONS_MAP.get(permission)
+ if not api_permission:
+ raise exceptions.InputException(
+ 'Unrecognized hotlist permission: %s' % permission)
+ api_permissions.append(api_permission)
+
+ return api_permissions
+
+
+def ConvertFieldDefPermissions(field_permissions):
+ # type: (Sequence[str]) -> Sequence[permission_objects_pb2.Permission]
+ """Converts field permission strings into protoc Permission enum values."""
+ api_permissions = []
+ for permission in field_permissions:
+ api_permission = FIELDDEF_PERMISSIONS_MAP.get(permission)
+ if not api_permission:
+ raise exceptions.InputException(
+ 'Unrecognized field permission: %s' % permission)
+ api_permissions.append(api_permission)
+
+ return api_permissions
+
+
+# TODO(crbug/monorail/7339): Implement all ConvertFooPermissions methods.
diff --git a/api/v3/permissions_servicer.py b/api/v3/permissions_servicer.py
new file mode 100644
index 0000000..d544478
--- /dev/null
+++ b/api/v3/permissions_servicer.py
@@ -0,0 +1,87 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from google.protobuf import empty_pb2
+
+from api import resource_name_converters as rnc
+from api.v3 import permission_converters as pc
+from api.v3 import monorail_servicer
+from api.v3.api_proto import permission_objects_pb2
+from api.v3.api_proto import permissions_pb2
+from api.v3.api_proto import permissions_prpc_pb2
+from businesslogic import work_env
+from framework import exceptions
+
+
+class PermissionsServicer(monorail_servicer.MonorailServicer):
+ """Handle API requests related to Permissions.
+ Each API request is implemented with a method as defined in the
+ .proto file. Each method does any request-specific validation, uses work_env
+ to safely operate on business objects, and returns a response proto.
+ """
+
+ DESCRIPTION = permissions_prpc_pb2.PermissionsServiceDescription
+
+ @monorail_servicer.PRPCMethod
+ def BatchGetPermissionSets(self, mc, request):
+ # type: (MonorailContext, BatchGetPermissionSetsRequest) ->
+ # BatchGetPermissionSetsResponse
+ """pRPC API method that implements BatchGetPermissionSets.
+
+ Raises:
+ InputException: if any name in request.names is not a valid resource name
+ or a permission string is not recognized.
+ PermissionException: if the requester does not have permission to
+ view one of the resources.
+ """
+ api_permission_sets = []
+ with work_env.WorkEnv(mc, self.services) as we:
+ for name in request.names:
+ api_permission_sets.append(self._GetPermissionSet(mc.cnxn, we, name))
+
+ return permissions_pb2.BatchGetPermissionSetsResponse(
+ permission_sets=api_permission_sets)
+
+ def _GetPermissionSet(self, cnxn, we, name):
+ # type: (sql.MonorailConnection, businesslogic.WorkEnv, str) ->
+ # permission_objects_pb2.PermissionSet
+ """Takes a resource name and returns the PermissionSet for the resource.
+
+ Args:
+ cnxn: MonorailConnection object to the database.
+ we: WorkEnv object to get the permission strings.
+ name: resource name of a resource we want a PermissionSet for.
+
+ Returns:
+ PermissionSet object.
+
+ Raises:
+ InputException: if request.name is not a valid resource name or a
+ permission string is not recognized.
+ PermissionException: if the requester does not have permission to
+ view the resource.
+ """
+ try:
+ hotlist_id = rnc.IngestHotlistName(name)
+ permissions = we.ListHotlistPermissions(hotlist_id)
+ api_permissions = pc.ConvertHotlistPermissions(permissions)
+ return permission_objects_pb2.PermissionSet(
+ resource=name, permissions=api_permissions)
+ except exceptions.InputException:
+ pass
+ try:
+ project_id, field_id = rnc.IngestFieldDefName(cnxn, name, self.services)
+ permissions = we.ListFieldDefPermissions(field_id, project_id)
+ api_permissions = pc.ConvertFieldDefPermissions(permissions)
+ return permission_objects_pb2.PermissionSet(
+ resource=name, permissions=api_permissions)
+ except exceptions.InputException:
+ pass
+ # TODO(crbug/monorail/7339): Add more try-except blocks for other
+ # resource types.
+ raise exceptions.InputException('invalid resource name')
diff --git a/api/v3/projects_servicer.py b/api/v3/projects_servicer.py
new file mode 100644
index 0000000..17d6f93
--- /dev/null
+++ b/api/v3/projects_servicer.py
@@ -0,0 +1,149 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from google.protobuf import empty_pb2
+
+from api import resource_name_converters as rnc
+from api.v3 import api_constants
+from api.v3 import monorail_servicer
+from api.v3 import paginator
+from api.v3.api_proto import projects_pb2
+from api.v3.api_proto import projects_prpc_pb2
+from businesslogic import work_env
+
+
+class ProjectsServicer(monorail_servicer.MonorailServicer):
+ """Handle API requests related to Project objects.
+ Each API request is implemented with a method as defined in the
+ .proto file. Each method does any request-specific validation, uses work_env
+ to safely operate on business objects, and returns a response proto.
+ """
+
+ DESCRIPTION = projects_prpc_pb2.ProjectsServiceDescription
+
+ @monorail_servicer.PRPCMethod
+ def ListIssueTemplates(self, mc, request):
+ # type: (MonorailContext, ListIssueTemplatesRequest) ->
+ # ListIssueTemplatesResponse
+ """pRPC API method that implements ListIssueTemplates.
+
+ Raises:
+ InputException if the request.parent is invalid.
+ NoSuchProjectException if no project exists with the given name.
+ """
+ project_id = rnc.IngestProjectName(mc.cnxn, request.parent, self.services)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ # TODO(crbug/monorail/7614): Eliminate the need to do this lookup.
+ project = we.GetProject(project_id)
+ mc.LookupLoggedInUserPerms(project)
+ templates = we.ListProjectTemplates(project_id)
+
+ return projects_pb2.ListIssueTemplatesResponse(
+ templates=self.converter.ConvertIssueTemplates(project_id, templates))
+
+ @monorail_servicer.PRPCMethod
+ def ListComponentDefs(self, mc, request):
+ # type: (MonorailContext, ListComponentDefsRequest) ->
+ # ListComponentDefsResponse
+ """pRPC API method that implements ListComponentDefs.
+
+ Raises:
+ InputException if the request.parent is invalid.
+ NoSuchProjectException if the parent project is not found.
+ """
+ project_id = rnc.IngestProjectName(mc.cnxn, request.parent, self.services)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ # TODO(crbug/monorail/7614): Eliminate the need to do this lookup.
+ project = we.GetProject(project_id)
+ mc.LookupLoggedInUserPerms(project)
+
+ page_size = paginator.CoercePageSize(
+ request.page_size, api_constants.MAX_COMPONENTS_PER_PAGE)
+ pager = paginator.Paginator(
+ parent=request.parent, page_size=page_size)
+ list_result = we.ListComponentDefs(
+ project_id, page_size, pager.GetStart(request.page_token))
+
+ api_component_defs = self.converter.ConvertComponentDefs(
+ list_result.items, project_id)
+
+ return projects_pb2.ListComponentDefsResponse(
+ component_defs=api_component_defs,
+ next_page_token=pager.GenerateNextPageToken(list_result.next_start))
+
+ @monorail_servicer.PRPCMethod
+ def CreateComponentDef(self, mc, request):
+ # type: (MonorailContext, CreateComponentDefRequest) ->
+ # ComponentDef
+ """pRPC API method that implements CreateComponentDef.
+
+ Raises:
+ InputException if the request is invalid.
+ NoSuchUserException if any given component admins or ccs do not exist.
+ NoSuchProjectException if the parent project does not exist.
+ PermissionException if the requester is not allowed to create
+ this component.
+ """
+ project_id = rnc.IngestProjectName(mc.cnxn, request.parent, self.services)
+ admin_ids = rnc.IngestUserNames(
+ mc.cnxn, request.component_def.admins, self.services)
+ cc_ids = rnc.IngestUserNames(
+ mc.cnxn, request.component_def.ccs, self.services)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ component_def = we.CreateComponentDef(
+ project_id, request.component_def.value,
+ request.component_def.docstring, admin_ids, cc_ids,
+ request.component_def.labels)
+
+ return self.converter.ConvertComponentDef(component_def)
+
+ @monorail_servicer.PRPCMethod
+ def DeleteComponentDef(self, mc, request):
+ # type: (MonorailContext, DeleteComponentDefRequest) -> Empty
+ """pRPC API method that implements DeleteComponentDef.
+
+ Raises:
+ InputException if the request in invalid.
+ NoSuchComponentException if the component does not exist.
+ PermissionException if the requester is not allowed to delete
+ this component.
+ NoSuchProjectException if the parent project does not exist.
+ """
+ project_id, component_id = rnc.IngestComponentDefNames(
+ mc.cnxn, [request.name], self.services)[0]
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.DeleteComponentDef(project_id, component_id)
+
+ return empty_pb2.Empty()
+
+ @monorail_servicer.PRPCMethod
+ def ListProjects(self, mc, _):
+ # type: (MonorailContext, ListProjectsRequest) -> ListProjectsResponse
+ """pRPC API method that implements ListProjects.
+
+ Raises:
+ InputException if the request.page_token is invalid or the request does
+ not match the previous request that provided the given page_token.
+ """
+ with work_env.WorkEnv(mc, self.services) as we:
+ # NOTE(crbug/monorail/7614): Until the referenced cleanup is complete,
+ # all servicer methods that are scoped to a single Project need to call
+ # mc.LookupLoggedInUserPerms.
+ # This method does not because it may be scoped to multiple projects.
+ allowed_project_ids = we.ListProjects()
+ projects_dict = we.GetProjects(allowed_project_ids)
+ projects = [projects_dict[proj_id] for proj_id in allowed_project_ids]
+
+ # TODO(crbug.com/monorail/7505): Add pagination logic.
+ return projects_pb2.ListProjectsResponse(
+ projects=self.converter.ConvertProjects(projects))
diff --git a/api/v3/test/__init__.py b/api/v3/test/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/api/v3/test/__init__.py
diff --git a/api/v3/test/converters_test.py b/api/v3/test/converters_test.py
new file mode 100644
index 0000000..1bbd12c
--- /dev/null
+++ b/api/v3/test/converters_test.py
@@ -0,0 +1,3254 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+"""Tests for converting internal protorpc to external protoc."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import copy
+import difflib
+import logging
+import unittest
+
+import mock
+from google.protobuf import field_mask_pb2
+from google.protobuf import timestamp_pb2
+
+from api import resource_name_converters as rnc
+from api.v3 import converters
+from api.v3.api_proto import feature_objects_pb2
+from api.v3.api_proto import issues_pb2
+from api.v3.api_proto import issue_objects_pb2
+from api.v3.api_proto import user_objects_pb2
+from api.v3.api_proto import project_objects_pb2
+from framework import authdata
+from framework import exceptions
+from framework import framework_constants
+from framework import framework_helpers
+from framework import monorailcontext
+from testing import fake
+from testing import testing_helpers
+from tracker import field_helpers
+from services import service_manager
+from proto import tracker_pb2
+from tracker import tracker_bizobj as tbo
+
+EXPLICIT_DERIVATION = issue_objects_pb2.Derivation.Value('EXPLICIT')
+RULE_DERIVATION = issue_objects_pb2.Derivation.Value('RULE')
+Choice = project_objects_pb2.FieldDef.EnumTypeSettings.Choice
+
+CURRENT_TIME = 12346.78
+
+
+class ConverterFunctionsTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ issue=fake.IssueService(),
+ project=fake.ProjectService(),
+ usergroup=fake.UserGroupService(),
+ user=fake.UserService(),
+ config=fake.ConfigService(),
+ template=fake.TemplateService(),
+ features=fake.FeaturesService())
+ self.cnxn = fake.MonorailConnection()
+ self.mc = monorailcontext.MonorailContext(self.services, cnxn=self.cnxn)
+ self.converter = converters.Converter(self.mc, self.services)
+ self.PAST_TIME = int(CURRENT_TIME - 1)
+ self.project_1 = self.services.project.TestAddProject(
+ 'proj', project_id=789)
+ self.project_2 = self.services.project.TestAddProject(
+ 'goose', project_id=788)
+ self.user_1 = self.services.user.TestAddUser('one@example.com', 111)
+ self.user_2 = self.services.user.TestAddUser('two@example.com', 222)
+ self.user_3 = self.services.user.TestAddUser('three@example.com', 333)
+ self.services.project.TestAddProjectMembers(
+ [self.user_1.user_id], self.project_1, 'CONTRIBUTOR_ROLE')
+
+ self.field_def_1_name = 'test_field_1'
+ self.field_def_1 = self._CreateFieldDef(
+ self.project_1.project_id,
+ self.field_def_1_name,
+ 'STR_TYPE',
+ admin_ids=[self.user_1.user_id],
+ is_required=True,
+ is_multivalued=True,
+ is_phase_field=True,
+ regex='abc')
+ self.field_def_2_name = 'test_field_2'
+ self.field_def_2 = self._CreateFieldDef(
+ self.project_1.project_id,
+ self.field_def_2_name,
+ 'INT_TYPE',
+ max_value=37,
+ is_niche=True)
+ self.field_def_3_name = 'days'
+ self.field_def_3 = self._CreateFieldDef(
+ self.project_1.project_id, self.field_def_3_name, 'ENUM_TYPE')
+ self.field_def_4_name = 'OS'
+ self.field_def_4 = self._CreateFieldDef(
+ self.project_1.project_id, self.field_def_4_name, 'ENUM_TYPE')
+ self.field_def_5_name = 'yellow'
+ self.field_def_5 = self._CreateFieldDef(
+ self.project_1.project_id, self.field_def_5_name, 'ENUM_TYPE')
+ self.field_def_7_name = 'redredred'
+ self.field_def_7 = self._CreateFieldDef(
+ self.project_1.project_id,
+ self.field_def_7_name,
+ 'ENUM_TYPE',
+ is_restricted_field=True,
+ editor_ids=[self.user_1.user_id])
+ self.field_def_8_name = 'dogandcat'
+ self.field_def_8 = self._CreateFieldDef(
+ self.project_1.project_id,
+ self.field_def_8_name,
+ 'USER_TYPE',
+ needs_member=True,
+ needs_perm='EDIT_PROJECT',
+ notify_on=tracker_pb2.NotifyTriggers.ANY_COMMENT)
+ self.field_def_9_name = 'catanddog'
+ self.field_def_9 = self._CreateFieldDef(
+ self.project_1.project_id,
+ self.field_def_9_name,
+ 'DATE_TYPE',
+ date_action_str='ping_owner_only')
+ self.field_def_10_name = 'url'
+ self.field_def_10 = self._CreateFieldDef(
+ self.project_1.project_id, self.field_def_10_name, 'URL_TYPE')
+ self.field_def_project2_name = 'lorem'
+ self.field_def_project2 = self._CreateFieldDef(
+ self.project_2.project_id, self.field_def_project2_name, 'ENUM_TYPE')
+ self.approval_def_1_name = 'approval_field_1'
+ self.approval_def_1_id = self._CreateFieldDef(
+ self.project_1.project_id,
+ self.approval_def_1_name,
+ 'APPROVAL_TYPE',
+ docstring='ad_1_docstring',
+ admin_ids=[self.user_1.user_id])
+ self.approval_def_1 = tracker_pb2.ApprovalDef(
+ approval_id=self.approval_def_1_id,
+ approver_ids=[self.user_2.user_id],
+ survey='approval_def_1 survey')
+ self.approval_def_2_name = 'approval_field_1'
+ self.approval_def_2_id = self._CreateFieldDef(
+ self.project_1.project_id,
+ self.approval_def_2_name,
+ 'APPROVAL_TYPE',
+ docstring='ad_2_docstring',
+ admin_ids=[self.user_1.user_id])
+ self.approval_def_2 = tracker_pb2.ApprovalDef(
+ approval_id=self.approval_def_2_id,
+ approver_ids=[self.user_2.user_id],
+ survey='approval_def_2 survey')
+ approval_defs = [self.approval_def_1, self.approval_def_2]
+ self.field_def_6_name = 'simonsays'
+ self.field_def_6 = self._CreateFieldDef(
+ self.project_1.project_id,
+ self.field_def_6_name,
+ 'STR_TYPE',
+ approval_id=self.approval_def_1_id)
+ self.dne_field_def_id = 999999
+ self.fv_1_value = u'some_string_field_value'
+ self.fv_1 = fake.MakeFieldValue(
+ field_id=self.field_def_1, str_value=self.fv_1_value, derived=False)
+ self.fv_1_derived = fake.MakeFieldValue(
+ field_id=self.field_def_1, str_value=self.fv_1_value, derived=True)
+ self.fv_6 = fake.MakeFieldValue(
+ field_id=self.field_def_6, str_value=u'touch-nose', derived=False)
+ self.phase_1_id = 123123
+ self.phase_1 = fake.MakePhase(self.phase_1_id, name='some phase name')
+ self.av_1 = fake.MakeApprovalValue(
+ self.approval_def_1_id,
+ setter_id=self.user_1.user_id,
+ set_on=self.PAST_TIME,
+ approver_ids=[self.user_2.user_id],
+ phase_id=self.phase_1_id)
+ self.av_2 = fake.MakeApprovalValue(
+ self.approval_def_1_id,
+ setter_id=self.user_1.user_id,
+ set_on=self.PAST_TIME,
+ approver_ids=[self.user_2.user_id])
+
+ self.issue_1 = fake.MakeTestIssue(
+ self.project_1.project_id,
+ 1,
+ 'sum',
+ 'New',
+ self.user_1.user_id,
+ cc_ids=[self.user_2.user_id],
+ derived_cc_ids=[self.user_3.user_id],
+ project_name=self.project_1.project_name,
+ star_count=1,
+ labels=['label-a', 'label-b', 'days-1'],
+ derived_owner_id=self.user_2.user_id,
+ derived_status='Fixed',
+ derived_labels=['label-derived', 'OS-mac', 'label-derived-2'],
+ component_ids=[1, 2],
+ merged_into_external='b/1',
+ derived_component_ids=[3, 4],
+ attachment_count=5,
+ field_values=[self.fv_1, self.fv_1_derived],
+ opened_timestamp=self.PAST_TIME,
+ modified_timestamp=self.PAST_TIME,
+ approval_values=[self.av_1],
+ phases=[self.phase_1])
+ self.issue_2 = fake.MakeTestIssue(
+ self.project_2.project_id,
+ 2,
+ 'sum2',
+ None,
+ None,
+ reporter_id=self.user_1.user_id,
+ project_name=self.project_2.project_name,
+ merged_into=self.issue_1.issue_id,
+ opened_timestamp=self.PAST_TIME,
+ modified_timestamp=self.PAST_TIME,
+ closed_timestamp=self.PAST_TIME,
+ derived_status='Fixed',
+ derived_owner_id=self.user_2.user_id,
+ is_spam=True)
+ self.services.issue.TestAddIssue(self.issue_1)
+ self.services.issue.TestAddIssue(self.issue_2)
+
+ self.template_0 = self.services.template.TestAddIssueTemplateDef(
+ 11110, self.project_1.project_id, 'template0')
+ self.template_1_label1_value = '2'
+ self.template_1_labels = [
+ 'pri-1', '{}-{}'.format(
+ self.field_def_3_name, self.template_1_label1_value)
+ ]
+ self.template_1 = self.services.template.TestAddIssueTemplateDef(
+ 11111,
+ self.project_1.project_id,
+ 'template1',
+ content='foobar',
+ summary='foo',
+ admin_ids=[self.user_2.user_id],
+ owner_id=self.user_1.user_id,
+ labels=self.template_1_labels,
+ component_ids=[654],
+ field_values=[self.fv_1],
+ approval_values=[self.av_1],
+ phases=[self.phase_1])
+ self.template_2 = self.services.template.TestAddIssueTemplateDef(
+ 11112,
+ self.project_1.project_id,
+ 'template2',
+ members_only=True,
+ owner_defaults_to_member=True)
+ self.template_3 = self.services.template.TestAddIssueTemplateDef(
+ 11113,
+ self.project_1.project_id,
+ 'template3',
+ field_values=[self.fv_1],
+ approval_values=[self.av_2],
+ )
+ self.dne_template = tracker_pb2.TemplateDef(
+ name='dne_template_name', template_id=11114)
+ self.labeldef_1 = tracker_pb2.LabelDef(
+ label='white-mountain',
+ label_docstring='test label doc string for white-mountain')
+ self.labeldef_2 = tracker_pb2.LabelDef(
+ label='yellow-submarine',
+ label_docstring='Submarine choice for yellow enum field')
+ self.labeldef_3 = tracker_pb2.LabelDef(
+ label='yellow-basket',
+ label_docstring='Basket choice for yellow enum field')
+ self.labeldef_4 = tracker_pb2.LabelDef(
+ label='yellow-tasket',
+ label_docstring='Deprecated tasket choice for yellow enum field',
+ deprecated=True)
+ self.labeldef_5 = tracker_pb2.LabelDef(
+ label='mont-blanc',
+ label_docstring='test label doc string for mont-blanc',
+ deprecated=True)
+ self.predefined_labels = [
+ self.labeldef_1, self.labeldef_2, self.labeldef_3, self.labeldef_4,
+ self.labeldef_5
+ ]
+ test_label_ids = {}
+ for index, ld in enumerate(self.predefined_labels):
+ test_label_ids[ld.label] = index
+ self.services.config.TestAddLabelsDict(test_label_ids)
+ self.status_1 = tracker_pb2.StatusDef(
+ status='New', means_open=True, status_docstring='status_1 docstring')
+ self.status_2 = tracker_pb2.StatusDef(
+ status='Duplicate',
+ means_open=False,
+ status_docstring='status_2 docstring')
+ self.status_3 = tracker_pb2.StatusDef(
+ status='Accepted',
+ means_open=True,
+ status_docstring='status_3_docstring')
+ self.status_4 = tracker_pb2.StatusDef(
+ status='Gibberish',
+ means_open=True,
+ status_docstring='status_4_docstring',
+ deprecated=True)
+ self.predefined_statuses = [
+ self.status_1, self.status_2, self.status_3, self.status_4
+ ]
+ self.component_def_1_path = 'foo'
+ self.component_def_1_id = self.services.config.CreateComponentDef(
+ self.cnxn, self.project_1.project_id, self.component_def_1_path,
+ 'cd1_docstring', False, [self.user_1.user_id], [self.user_2.user_id],
+ self.PAST_TIME, self.user_1.user_id, [0, 1, 2, 3, 4])
+ self.component_def_2_path = 'foo>bar'
+ self.component_def_2_id = self.services.config.CreateComponentDef(
+ self.cnxn, self.project_1.project_id, self.component_def_2_path,
+ 'cd2_docstring', True, [self.user_1.user_id], [self.user_2.user_id],
+ self.PAST_TIME, self.user_1.user_id, [])
+ self.services.config.UpdateConfig(
+ self.cnxn,
+ self.project_1,
+ statuses_offer_merge=[self.status_2.status],
+ excl_label_prefixes=['type', 'priority'],
+ default_template_for_developers=self.template_2.template_id,
+ default_template_for_users=self.template_1.template_id,
+ list_prefs=('ID Summary', 'ID', 'status', 'owner', 'owner:me'),
+ # UpdateConfig accepts tuples rather than protorpc *Defs
+ well_known_labels=[
+ (ld.label, ld.label_docstring, ld.deprecated)
+ for ld in self.predefined_labels
+ ],
+ approval_defs=[
+ (ad.approval_id, ad.approver_ids, ad.survey) for ad in approval_defs
+ ],
+ well_known_statuses=[
+ (sd.status, sd.status_docstring, sd.means_open, sd.deprecated)
+ for sd in self.predefined_statuses
+ ])
+ # base_query_id 2 equates to "is:open", defined in tracker_constants.
+ self.psq_1 = tracker_pb2.SavedQuery(
+ query_id=2, name='psq1 name', base_query_id=2, query='foo=bar')
+ self.psq_2 = tracker_pb2.SavedQuery(
+ query_id=3, name='psq2 name', query='fizz=buzz')
+ self.services.features.UpdateCannedQueries(
+ self.cnxn, self.project_1.project_id, [self.psq_1, self.psq_2])
+
+ def _CreateFieldDef(
+ self,
+ project_id,
+ field_name,
+ field_type_str,
+ docstring=None,
+ min_value=None,
+ max_value=None,
+ regex=None,
+ needs_member=None,
+ needs_perm=None,
+ grants_perm=None,
+ notify_on=None,
+ date_action_str=None,
+ admin_ids=None,
+ editor_ids=None,
+ is_required=False,
+ is_niche=False,
+ is_multivalued=False,
+ is_phase_field=False,
+ approval_id=None,
+ is_restricted_field=False):
+ """Calls CreateFieldDef with reasonable defaults, returns the ID."""
+ if admin_ids is None:
+ admin_ids = []
+ if editor_ids is None:
+ editor_ids = []
+ return self.services.config.CreateFieldDef(
+ self.cnxn,
+ project_id,
+ field_name,
+ field_type_str,
+ None,
+ None,
+ is_required,
+ is_niche,
+ is_multivalued,
+ min_value,
+ max_value,
+ regex,
+ needs_member,
+ needs_perm,
+ grants_perm,
+ notify_on,
+ date_action_str,
+ docstring,
+ admin_ids,
+ editor_ids,
+ is_phase_field=is_phase_field,
+ approval_id=approval_id,
+ is_restricted_field=is_restricted_field)
+
+ def _GetFieldDefById(self, project_id, fd_id):
+ config = self.services.config.GetProjectConfig(self.cnxn, project_id)
+ return [fd for fd in config.field_defs if fd.field_id == fd_id][0]
+
+ def _GetApprovalDefById(self, project_id, ad_id):
+ config = self.services.config.GetProjectConfig(self.cnxn, project_id)
+ return [ad for ad in config.approval_defs if ad.approval_id == ad_id][0]
+
+ def testConvertHotlist(self):
+ """We can convert a Hotlist."""
+ hotlist = fake.Hotlist(
+ 'Hotlist-Name',
+ 240,
+ default_col_spec='chicken goose',
+ is_private=False,
+ owner_ids=[111],
+ editor_ids=[222, 333],
+ summary='Hotlist summary',
+ description='Hotlist Description')
+ expected_api_hotlist = feature_objects_pb2.Hotlist(
+ name='hotlists/240',
+ display_name=hotlist.name,
+ owner= 'users/111',
+ summary=hotlist.summary,
+ description=hotlist.description,
+ editors=['users/222', 'users/333'],
+ hotlist_privacy=feature_objects_pb2.Hotlist.HotlistPrivacy.Value(
+ 'PUBLIC'),
+ default_columns=[
+ issue_objects_pb2.IssuesListColumn(column='chicken'),
+ issue_objects_pb2.IssuesListColumn(column='goose')
+ ])
+ self.converter.user_auth = authdata.AuthData.FromUser(
+ self.cnxn, self.user_1, self.services)
+ self.assertEqual(
+ expected_api_hotlist, self.converter.ConvertHotlist(hotlist))
+
+ def testConvertHotlist_DefaultValues(self):
+ """We can convert a Hotlist with some empty or default values."""
+ hotlist = fake.Hotlist(
+ 'Hotlist-Name',
+ 241,
+ is_private=True,
+ owner_ids=[111],
+ summary='Hotlist summary',
+ description='Hotlist Description',
+ default_col_spec='')
+ expected_api_hotlist = feature_objects_pb2.Hotlist(
+ name='hotlists/241',
+ display_name=hotlist.name,
+ owner='users/111',
+ summary=hotlist.summary,
+ description=hotlist.description,
+ hotlist_privacy=feature_objects_pb2.Hotlist.HotlistPrivacy.Value(
+ 'PRIVATE'))
+ self.converter.user_auth = authdata.AuthData.FromUser(
+ self.cnxn, self.user_1, self.services)
+ self.assertEqual(
+ expected_api_hotlist, self.converter.ConvertHotlist(hotlist))
+
+ def testConvertHotlists(self):
+ """We can convert several Hotlists."""
+ hotlists = [
+ fake.Hotlist(
+ 'Hotlist-Name',
+ 241,
+ owner_ids=[111],
+ summary='Hotlist summary',
+ description='Hotlist Description'),
+ fake.Hotlist(
+ 'Hotlist-Name',
+ 241,
+ owner_ids=[111],
+ summary='Hotlist summary',
+ description='Hotlist Description')
+ ]
+ self.assertEqual(2, len(self.converter.ConvertHotlists(hotlists)))
+
+ def testConvertHotlistItems(self):
+ """We can convert HotlistItems."""
+ hotlist_item_fields = [
+ (self.issue_1.issue_id, 21, 111, self.PAST_TIME, 'note2'),
+ (78900, 11, 222, self.PAST_TIME, 'note3'), # Does not exist.
+ (self.issue_2.issue_id, 1, 222, None, 'note1'),
+ ]
+ hotlist = fake.Hotlist(
+ 'Hotlist-Name', 241, hotlist_item_fields=hotlist_item_fields)
+ self.converter.user_auth = authdata.AuthData.FromUser(
+ self.cnxn, self.user_1, self.services)
+ api_items = self.converter.ConvertHotlistItems(
+ hotlist.hotlist_id, hotlist.items)
+ expected_create_time = timestamp_pb2.Timestamp()
+ expected_create_time.FromSeconds(self.PAST_TIME)
+ expected_items = [
+ feature_objects_pb2.HotlistItem(
+ name='hotlists/241/items/proj.1',
+ issue='projects/proj/issues/1',
+ rank=1,
+ adder= 'users/111',
+ create_time=expected_create_time,
+ note='note2'),
+ feature_objects_pb2.HotlistItem(
+ name='hotlists/241/items/goose.2',
+ issue='projects/goose/issues/2',
+ rank=0,
+ adder='users/222',
+ note='note1')
+ ]
+ self.assertEqual(api_items, expected_items)
+
+ def testConvertHotlistItems_Empty(self):
+ hotlist = fake.Hotlist('Hotlist-Name', 241)
+ self.converter.user_auth = authdata.AuthData.FromUser(
+ self.cnxn, self.user_1, self.services)
+ api_items = self.converter.ConvertHotlistItems(
+ hotlist.hotlist_id, hotlist.items)
+ self.assertEqual(api_items, [])
+
+ @mock.patch('tracker.attachment_helpers.SignAttachmentID')
+ def testConvertComments(self, mock_SignAttachmentID):
+ """We can convert comments."""
+ mock_SignAttachmentID.return_value = 2
+ attach = tracker_pb2.Attachment(
+ attachment_id=1,
+ mimetype='image/png',
+ filename='example.png',
+ filesize=12345)
+ deleted_attach = tracker_pb2.Attachment(
+ attachment_id=2,
+ mimetype='image/png',
+ filename='deleted_example.png',
+ filesize=67890,
+ deleted=True)
+ initial_comment = tracker_pb2.IssueComment(
+ project_id=self.issue_1.project_id,
+ issue_id=self.issue_1.issue_id,
+ user_id=self.issue_1.reporter_id,
+ timestamp=self.PAST_TIME,
+ content='initial description',
+ sequence=0,
+ is_description=True,
+ description_num='1',
+ attachments=[attach, deleted_attach])
+ deleted_comment = tracker_pb2.IssueComment(
+ project_id=self.issue_1.project_id,
+ issue_id=self.issue_1.issue_id,
+ timestamp=self.PAST_TIME,
+ deleted_by=self.issue_1.reporter_id,
+ sequence=1)
+ amendments = [
+ tracker_pb2.Amendment(
+ field=tracker_pb2.FieldID.SUMMARY, newvalue='new', oldvalue='old'),
+ tracker_pb2.Amendment(
+ field=tracker_pb2.FieldID.OWNER, added_user_ids=[111]),
+ tracker_pb2.Amendment(
+ field=tracker_pb2.FieldID.CC,
+ added_user_ids=[111],
+ removed_user_ids=[222]),
+ tracker_pb2.Amendment(
+ field=tracker_pb2.FieldID.CUSTOM,
+ custom_field_name='EstDays',
+ newvalue='12')
+ ]
+ amendments_comment = tracker_pb2.IssueComment(
+ project_id=self.issue_1.project_id,
+ issue_id=self.issue_1.issue_id,
+ user_id=self.issue_1.reporter_id,
+ timestamp=self.PAST_TIME,
+ content='some amendments',
+ sequence=2,
+ amendments=amendments,
+ importer_id=1, # Not used in conversion, so nothing to verify.
+ approval_id=self.approval_def_1_id)
+ inbound_spam_comment = tracker_pb2.IssueComment(
+ project_id=self.issue_1.project_id,
+ issue_id=self.issue_1.issue_id,
+ user_id=self.issue_1.reporter_id,
+ timestamp=self.PAST_TIME,
+ content='content',
+ sequence=3,
+ inbound_message='inbound message',
+ is_spam=True)
+ expected_0 = issue_objects_pb2.Comment(
+ name='projects/proj/issues/1/comments/0',
+ state=issue_objects_pb2.IssueContentState.Value('ACTIVE'),
+ type=issue_objects_pb2.Comment.Type.Value('DESCRIPTION'),
+ content='initial description',
+ commenter='users/111',
+ create_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ attachments=[
+ issue_objects_pb2.Comment.Attachment(
+ filename='example.png',
+ state=issue_objects_pb2.IssueContentState.Value('ACTIVE'),
+ size=12345,
+ media_type='image/png',
+ thumbnail_uri='attachment?aid=1&signed_aid=2&inline=1&thumb=1',
+ view_uri='attachment?aid=1&signed_aid=2&inline=1',
+ download_uri='attachment?aid=1&signed_aid=2'),
+ issue_objects_pb2.Comment.Attachment(
+ filename='deleted_example.png',
+ state=issue_objects_pb2.IssueContentState.Value('DELETED'),
+ media_type='image/png')
+ ])
+ expected_1 = issue_objects_pb2.Comment(
+ name='projects/proj/issues/1/comments/1',
+ state=issue_objects_pb2.IssueContentState.Value('DELETED'),
+ type=issue_objects_pb2.Comment.Type.Value('COMMENT'),
+ create_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME))
+ expected_2 = issue_objects_pb2.Comment(
+ name='projects/proj/issues/1/comments/2',
+ state=issue_objects_pb2.IssueContentState.Value('ACTIVE'),
+ type=issue_objects_pb2.Comment.Type.Value('COMMENT'),
+ content='some amendments',
+ commenter='users/111',
+ create_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ approval='projects/proj/approvalDefs/%d' % self.approval_def_1_id,
+ amendments=[
+ issue_objects_pb2.Comment.Amendment(
+ field_name='Summary', new_or_delta_value='new',
+ old_value='old'),
+ issue_objects_pb2.Comment.Amendment(
+ field_name='Owner', new_or_delta_value='o...@example.com'),
+ issue_objects_pb2.Comment.Amendment(
+ field_name='Cc',
+ new_or_delta_value='-t...@example.com o...@example.com'),
+ issue_objects_pb2.Comment.Amendment(
+ field_name='EstDays', new_or_delta_value='12')
+ ])
+ expected_3 = issue_objects_pb2.Comment(
+ name='projects/proj/issues/1/comments/3',
+ state=issue_objects_pb2.IssueContentState.Value('SPAM'),
+ type=issue_objects_pb2.Comment.Type.Value('COMMENT'),
+ content='content',
+ commenter='users/111',
+ create_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ inbound_message='inbound message')
+
+ comments = [
+ initial_comment, deleted_comment, amendments_comment,
+ inbound_spam_comment
+ ]
+ actual = self.converter.ConvertComments(self.issue_1.issue_id, comments)
+ self.assertEqual(actual, [expected_0, expected_1, expected_2, expected_3])
+
+ def testConvertComments_Empty(self):
+ """We can convert an empty list of comments."""
+ self.assertEqual(
+ self.converter.ConvertComments(self.issue_1.issue_id, []), [])
+
+ def testConvertIssue(self):
+ """We can convert a single issue."""
+ self.assertEqual(self.converter.ConvertIssue(self.issue_1),
+ self.converter.ConvertIssues([self.issue_1])[0])
+
+ def testConvertIssues(self):
+ """We can convert Issues."""
+ blocked_on_1 = fake.MakeTestIssue(
+ self.project_1.project_id,
+ 3,
+ 'sum3',
+ 'New',
+ self.user_1.user_id,
+ issue_id=301,
+ project_name=self.project_1.project_name,
+ )
+ blocked_on_2 = fake.MakeTestIssue(
+ self.project_2.project_id,
+ 4,
+ 'sum4',
+ 'New',
+ self.user_1.user_id,
+ issue_id=401,
+ project_name=self.project_2.project_name,
+ )
+ blocking = fake.MakeTestIssue(
+ self.project_2.project_id,
+ 5,
+ 'sum5',
+ 'New',
+ self.user_1.user_id,
+ issue_id=501,
+ project_name=self.project_2.project_name,
+ )
+ self.services.issue.TestAddIssue(blocked_on_1)
+ self.services.issue.TestAddIssue(blocked_on_2)
+ self.services.issue.TestAddIssue(blocking)
+
+ # Reversing natural ordering to ensure order is respected.
+ self.issue_1.blocked_on_iids = [
+ blocked_on_2.issue_id, blocked_on_1.issue_id
+ ]
+ self.issue_1.dangling_blocked_on_refs = [
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/555'),
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/2')
+ ]
+ self.issue_1.blocking_iids = [blocking.issue_id]
+ self.issue_1.dangling_blocking_refs = [
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/3')
+ ]
+
+ issues = [self.issue_1, self.issue_2]
+ expected_1 = issue_objects_pb2.Issue(
+ name='projects/proj/issues/1',
+ summary='sum',
+ state=issue_objects_pb2.IssueContentState.Value('ACTIVE'),
+ status=issue_objects_pb2.Issue.StatusValue(
+ derivation=EXPLICIT_DERIVATION, status='New'),
+ reporter='users/111',
+ owner=issue_objects_pb2.Issue.UserValue(
+ derivation=EXPLICIT_DERIVATION, user='users/111'),
+ cc_users=[
+ issue_objects_pb2.Issue.UserValue(
+ derivation=EXPLICIT_DERIVATION, user='users/222'),
+ issue_objects_pb2.Issue.UserValue(
+ derivation=RULE_DERIVATION, user='users/333')
+ ],
+ labels=[
+ issue_objects_pb2.Issue.LabelValue(
+ derivation=EXPLICIT_DERIVATION, label='label-a'),
+ issue_objects_pb2.Issue.LabelValue(
+ derivation=EXPLICIT_DERIVATION, label='label-b'),
+ issue_objects_pb2.Issue.LabelValue(
+ derivation=RULE_DERIVATION, label='label-derived'),
+ issue_objects_pb2.Issue.LabelValue(
+ derivation=RULE_DERIVATION, label='label-derived-2')
+ ],
+ components=[
+ issue_objects_pb2.Issue.ComponentValue(
+ derivation=EXPLICIT_DERIVATION,
+ component='projects/proj/componentDefs/1'),
+ issue_objects_pb2.Issue.ComponentValue(
+ derivation=EXPLICIT_DERIVATION,
+ component='projects/proj/componentDefs/2'),
+ issue_objects_pb2.Issue.ComponentValue(
+ derivation=RULE_DERIVATION,
+ component='projects/proj/componentDefs/3'),
+ issue_objects_pb2.Issue.ComponentValue(
+ derivation=RULE_DERIVATION,
+ component='projects/proj/componentDefs/4'),
+ ],
+ field_values=[
+ issue_objects_pb2.FieldValue(
+ derivation=EXPLICIT_DERIVATION,
+ field='projects/proj/fieldDefs/%d' % self.field_def_1,
+ value=self.fv_1_value,
+ ),
+ issue_objects_pb2.FieldValue(
+ derivation=RULE_DERIVATION,
+ field='projects/proj/fieldDefs/%d' % self.field_def_1,
+ value=self.fv_1_value,
+ ),
+ issue_objects_pb2.FieldValue(
+ derivation=EXPLICIT_DERIVATION,
+ field='projects/proj/fieldDefs/%d' % self.field_def_3,
+ value='1',
+ ),
+ issue_objects_pb2.FieldValue(
+ derivation=RULE_DERIVATION,
+ field='projects/proj/fieldDefs/%d' % self.field_def_4,
+ value='mac',
+ )
+ ],
+ merged_into_issue_ref=issue_objects_pb2.IssueRef(ext_identifier='b/1'),
+ blocked_on_issue_refs=[
+ issue_objects_pb2.IssueRef(issue='projects/goose/issues/4'),
+ issue_objects_pb2.IssueRef(issue='projects/proj/issues/3'),
+ issue_objects_pb2.IssueRef(ext_identifier='b/555'),
+ issue_objects_pb2.IssueRef(ext_identifier='b/2')
+ ],
+ blocking_issue_refs=[
+ issue_objects_pb2.IssueRef(issue='projects/goose/issues/5'),
+ issue_objects_pb2.IssueRef(ext_identifier='b/3')
+ ],
+ create_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ modify_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ component_modify_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ status_modify_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ owner_modify_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ star_count=1,
+ attachment_count=5,
+ phases=[self.phase_1.name])
+ expected_2 = issue_objects_pb2.Issue(
+ name='projects/goose/issues/2',
+ summary='sum2',
+ state=issue_objects_pb2.IssueContentState.Value('SPAM'),
+ status=issue_objects_pb2.Issue.StatusValue(
+ derivation=RULE_DERIVATION, status='Fixed'),
+ reporter='users/111',
+ owner=issue_objects_pb2.Issue.UserValue(
+ derivation=RULE_DERIVATION, user='users/222'),
+ merged_into_issue_ref=issue_objects_pb2.IssueRef(
+ issue='projects/proj/issues/1'),
+ create_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ close_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ modify_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ component_modify_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ status_modify_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ owner_modify_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME))
+ self.assertEqual(
+ self.converter.ConvertIssues(issues), [expected_1, expected_2])
+
+ def testConvertIssues_Empty(self):
+ """ConvertIssues works with no issues passed in."""
+ self.assertEqual(self.converter.ConvertIssues([]), [])
+
+ def testConvertIssues_NegativeAttachmentCount(self):
+ """Negative attachment counts are not set on issues."""
+ issue = fake.MakeTestIssue(
+ self.project_1.project_id,
+ 3,
+ 'sum',
+ 'New',
+ owner_id=None,
+ reporter_id=111,
+ attachment_count=-10,
+ project_name=self.project_1.project_name,
+ opened_timestamp=self.PAST_TIME,
+ modified_timestamp=self.PAST_TIME)
+ self.services.issue.TestAddIssue(issue)
+ expected_issue = issue_objects_pb2.Issue(
+ name='projects/proj/issues/3',
+ state=issue_objects_pb2.IssueContentState.Value('ACTIVE'),
+ summary='sum',
+ status=issue_objects_pb2.Issue.StatusValue(
+ derivation=EXPLICIT_DERIVATION, status='New'),
+ reporter='users/111',
+ create_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ modify_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ component_modify_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ status_modify_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ owner_modify_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ )
+ self.assertEqual(self.converter.ConvertIssues([issue]), [expected_issue])
+
+ def testConvertIssues_FilterApprovalFV(self):
+ issue = fake.MakeTestIssue(
+ self.project_1.project_id,
+ 3,
+ 'sum',
+ 'New',
+ owner_id=None,
+ reporter_id=111,
+ attachment_count=-10,
+ project_name=self.project_1.project_name,
+ opened_timestamp=self.PAST_TIME,
+ modified_timestamp=self.PAST_TIME,
+ field_values=[self.fv_1, self.fv_6])
+ self.services.issue.TestAddIssue(issue)
+ actual = self.converter.ConvertIssues([issue])[0]
+
+ expected_fv = issue_objects_pb2.FieldValue(
+ derivation=EXPLICIT_DERIVATION,
+ field='projects/proj/fieldDefs/%d' % self.field_def_1,
+ value=self.fv_1_value,
+ )
+ self.assertEqual(len(actual.field_values), 1)
+ self.assertEqual(actual.field_values[0], expected_fv)
+
+ def testConvertUser(self):
+ """We can convert a single User."""
+ self.user_1.vacation_message = 'non-empty-string'
+ self.converter.user_auth = authdata.AuthData.FromUser(
+ self.cnxn, self.user_1, self.services)
+
+ expected_user = user_objects_pb2.User(
+ name='users/111',
+ display_name='one@example.com',
+ email='one@example.com',
+ availability_message='non-empty-string')
+ self.assertEqual(self.converter.ConvertUser(self.user_1), expected_user)
+
+
+ def testConvertUsers(self):
+ user_deleted = self.services.user.TestAddUser(
+ '', framework_constants.DELETED_USER_ID)
+ self.user_1.vacation_message = 'non-empty-string'
+ user_ids = [self.user_1.user_id, user_deleted.user_id]
+ self.converter.user_auth = authdata.AuthData.FromUser(
+ self.cnxn, self.user_1, self.services)
+
+ expected_user_dict = {
+ self.user_1.user_id:
+ user_objects_pb2.User(
+ name='users/111',
+ display_name='one@example.com',
+ email='one@example.com',
+ availability_message='non-empty-string'),
+ user_deleted.user_id:
+ user_objects_pb2.User(
+ name='users/1',
+ display_name=framework_constants.DELETED_USER_NAME,
+ email='',
+ availability_message='User never visited'),
+ }
+ self.assertEqual(self.converter.ConvertUsers(user_ids), expected_user_dict)
+
+ def testConvertProjectStars(self):
+ expected_stars = [
+ user_objects_pb2.ProjectStar(name='users/111/projectStars/proj'),
+ user_objects_pb2.ProjectStar(name='users/111/projectStars/goose')
+ ]
+ self.assertEqual(
+ self.converter.ConvertProjectStars(
+ self.user_1.user_id, [self.project_1, self.project_2]),
+ expected_stars)
+
+ def _Issue(self, project_id, local_id):
+ issue = tracker_pb2.Issue(owner_id=0)
+ issue.project_name = 'proj-%d' % project_id
+ issue.project_id = project_id
+ issue.local_id = local_id
+ issue.issue_id = project_id * 100 + local_id
+ return issue
+
+ def testIngestAttachmentUploads(self):
+ up_1 = issues_pb2.AttachmentUpload(
+ filename='clown.gif', content='iTs prOUnOuNcED JIF')
+ up_2 = issues_pb2.AttachmentUpload(
+ filename='mowgli', content='cutest dog')
+
+ ingested = self.converter.IngestAttachmentUploads([up_1, up_2])
+ expected = [framework_helpers.AttachmentUpload(
+ 'clown.gif', 'iTs prOUnOuNcED JIF', 'image/gif'),
+ framework_helpers.AttachmentUpload(
+ 'mowgli', 'cutest dog', 'text/plain')]
+ self.assertEqual(ingested, expected)
+
+ def testtIngestAttachmentUploads_Invalid(self):
+ up_1 = issues_pb2.AttachmentUpload(filename='clown.gif')
+ up_2 = issues_pb2.AttachmentUpload(content='cutest dog')
+
+ with self.assertRaisesRegexp(
+ exceptions.InputException, 'Uploaded .+\nUploaded .+'):
+ self.converter.IngestAttachmentUploads([up_1, up_2])
+
+ def testIngestIssueDeltas(self):
+ # Set up.
+ self.services.project.TestAddProject('proj-780', project_id=780)
+ config = fake.MakeTestConfig(780, [], [])
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ issue_1 = self._Issue(780, 1)
+ self.services.issue.TestAddIssue(issue_1)
+ issue_2 = self._Issue(780, 2)
+ self.services.issue.TestAddIssue(issue_2)
+ comp_1 = fake.MakeTestComponentDef(780, 1)
+ comp_2 = fake.MakeTestComponentDef(780, 2)
+ fd_str = fake.MakeTestFieldDef(1, 780, tracker_pb2.FieldTypes.STR_TYPE)
+ fd_enum = fake.MakeTestFieldDef(
+ 2, 780, tracker_pb2.FieldTypes.ENUM_TYPE, field_name='Kingdom')
+ config = fake.MakeTestConfig(780, [], [])
+ config.component_defs = [comp_1, comp_2]
+ config.field_defs = [fd_str, fd_enum]
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ # Issue and delta that changes all things.
+ api_issue_all = issue_objects_pb2.Issue(
+ name='projects/proj-780/issues/1',
+ status=issue_objects_pb2.Issue.StatusValue(status='Fixed'),
+ owner=issue_objects_pb2.Issue.UserValue(user='users/111'),
+ summary='honk honk.',
+ cc_users=[issue_objects_pb2.Issue.UserValue(user='users/222')],
+ components=[
+ issue_objects_pb2.Issue.ComponentValue(
+ component='projects/proj-780/componentDefs/1')
+ ],
+ field_values=[
+ issue_objects_pb2.FieldValue(
+ field='projects/proj-780/fieldDefs/1', value='chicken'),
+ issue_objects_pb2.FieldValue(
+ field='projects/proj-780/fieldDefs/2', value='come')
+ ],
+ labels=[issue_objects_pb2.Issue.LabelValue(label='ready')])
+ mask_all = field_mask_pb2.FieldMask(
+ paths=[
+ 'status', 'owner', 'summary', 'cc_users', 'labels', 'components',
+ 'field_values'
+ ])
+ api_delta_all = issues_pb2.IssueDelta(
+ issue=api_issue_all,
+ update_mask=mask_all,
+ ccs_remove=['users/333'],
+ components_remove=['projects/proj-780/componentDefs/2'],
+ field_vals_remove=[
+ issue_objects_pb2.FieldValue(
+ field='projects/proj-780/fieldDefs/1', value='rooster'),
+ issue_objects_pb2.FieldValue(
+ field='projects/proj-780/fieldDefs/2', value='leave')
+ ],
+ labels_remove=['not-ready'])
+ exp_fvs_add = [
+ field_helpers.ParseOneFieldValue(
+ self.cnxn, self.services.user, fd_str, 'chicken')
+ ]
+ exp_fvs_remove = [
+ field_helpers.ParseOneFieldValue(
+ self.cnxn, self.services.user, fd_str, 'rooster')
+ ]
+ expected_delta_all = tracker_pb2.IssueDelta(
+ status='Fixed',
+ owner_id=111,
+ summary='honk honk.',
+ cc_ids_add=[222],
+ cc_ids_remove=[333],
+ comp_ids_add=[1],
+ comp_ids_remove=[2],
+ field_vals_add=exp_fvs_add,
+ field_vals_remove=exp_fvs_remove,
+ labels_add=['ready', 'Kingdom-come'],
+ labels_remove=['not-ready', 'Kingdom-leave'])
+
+ api_deltas = [api_delta_all]
+
+ # Issue with all fields, but an empty mask.
+ api_issue_all_masked = issue_objects_pb2.Issue(
+ name='projects/proj-780/issues/2',
+ status=issue_objects_pb2.Issue.StatusValue(status='Fixed'),
+ owner=issue_objects_pb2.Issue.UserValue(user='users/111'),
+ summary='honk honk.',
+ cc_users=[issue_objects_pb2.Issue.UserValue(user='users/222')],
+ components=[
+ issue_objects_pb2.Issue.ComponentValue(
+ component='projects/proj-780/componentDefs/1')
+ ],
+ field_values=[
+ issue_objects_pb2.FieldValue(
+ field='projects/proj-780/fieldDefs/1', value='chicken'),
+ issue_objects_pb2.FieldValue(
+ field='projects/proj-780/fieldDefs/2', value='come')
+ ],
+ labels=[issue_objects_pb2.Issue.LabelValue(label='ready')])
+ api_delta_all_masked = issues_pb2.IssueDelta(
+ issue=api_issue_all_masked,
+ update_mask=field_mask_pb2.FieldMask(paths=[]),
+ ccs_remove=['users/333'],
+ components_remove=['projects/proj-780/componentDefs/2'],
+ field_vals_remove=[
+ issue_objects_pb2.FieldValue(
+ field='projects/proj-780/fieldDefs/1', value='rooster'),
+ issue_objects_pb2.FieldValue(
+ field='projects/proj-780/fieldDefs/2', value='leave')
+ ],
+ labels_remove=['not-ready'])
+ expected_delta_all_masked = tracker_pb2.IssueDelta(
+ cc_ids_remove=[333],
+ comp_ids_remove=[2],
+ labels_remove=['not-ready', 'Kingdom-leave'],
+ field_vals_remove=exp_fvs_remove)
+
+ api_deltas.append(api_delta_all_masked)
+
+ actual = self.converter.IngestIssueDeltas(api_deltas)
+ expected = [(78001, expected_delta_all), (78002, expected_delta_all_masked)]
+ self.assertEqual(actual, expected)
+
+ def testIngestIssueDeltas_IssueRefs(self):
+ # Set up.
+ self.services.project.TestAddProject('proj-780', project_id=780)
+ issue = self._Issue(780, 1)
+ self.services.issue.TestAddIssue(issue)
+
+ bo_add = self._Issue(780, 2)
+ self.services.issue.TestAddIssue(bo_add)
+
+ b_add = self._Issue(780, 3)
+ self.services.issue.TestAddIssue(b_add)
+
+ bo_remove = self._Issue(780, 4)
+ self.services.issue.TestAddIssue(bo_remove)
+
+ b_remove = self._Issue(780, 5)
+ self.services.issue.TestAddIssue(b_remove)
+
+ # merge_remove tested in testIngestIssueDeltas_RemoveNonRepeated
+ merge_add = self._Issue(780, 6)
+ self.services.issue.TestAddIssue(merge_add)
+
+ api_issue = issue_objects_pb2.Issue(
+ name='projects/proj-780/issues/1',
+ blocked_on_issue_refs=[
+ issue_objects_pb2.IssueRef(issue='projects/proj-780/issues/2'),
+ issue_objects_pb2.IssueRef(ext_identifier='b/1')
+ ],
+ blocking_issue_refs=[
+ issue_objects_pb2.IssueRef(issue='projects/proj-780/issues/3'),
+ issue_objects_pb2.IssueRef(ext_identifier='b/2')
+ ],
+ merged_into_issue_ref=issue_objects_pb2.IssueRef(
+ issue='projects/proj-780/issues/6'))
+
+ api_delta = issues_pb2.IssueDelta(
+ issue=api_issue,
+ update_mask=field_mask_pb2.FieldMask(
+ paths=[
+ 'blocked_on_issue_refs', 'blocking_issue_refs',
+ 'merged_into_issue_ref'
+ ]),
+ blocked_on_issues_remove=[
+ issue_objects_pb2.IssueRef(issue='projects/proj-780/issues/4'),
+ issue_objects_pb2.IssueRef(ext_identifier='b/3')
+ ],
+ blocking_issues_remove=[
+ issue_objects_pb2.IssueRef(issue='projects/proj-780/issues/5'),
+ issue_objects_pb2.IssueRef(ext_identifier='b/4')
+ ])
+
+ expected_delta = tracker_pb2.IssueDelta(
+ blocked_on_add=[bo_add.issue_id],
+ blocked_on_remove=[bo_remove.issue_id],
+ blocking_add=[b_add.issue_id],
+ blocking_remove=[b_remove.issue_id],
+ ext_blocked_on_add=['b/1'],
+ ext_blocked_on_remove=['b/3'],
+ ext_blocking_add=['b/2'],
+ ext_blocking_remove=['b/4'],
+ merged_into=merge_add.issue_id)
+
+ # Test adding an external merged_into_issue.
+ api_issue_ext_merged = issue_objects_pb2.Issue(
+ name='projects/proj-780/issues/2',
+ merged_into_issue_ref=issue_objects_pb2.IssueRef(ext_identifier='b/1'))
+ api_delta_ext_merged = issues_pb2.IssueDelta(
+ issue=api_issue_ext_merged,
+ update_mask=field_mask_pb2.FieldMask(paths=['merged_into_issue_ref']))
+ expected_delta_ext_merged = tracker_pb2.IssueDelta(
+ merged_into_external='b/1')
+
+ # Test issue with empty mask.
+ issue_all_masked = self._Issue(780, 11)
+ self.services.issue.TestAddIssue(issue_all_masked)
+
+ api_issue_all_masked = copy.deepcopy(api_issue)
+ api_issue_all_masked.name = 'projects/proj-780/issues/11'
+ api_delta_all_masked = issues_pb2.IssueDelta(
+ issue=api_issue_all_masked, update_mask=field_mask_pb2.FieldMask())
+ expected_all_masked_delta = tracker_pb2.IssueDelta()
+
+ # Check results.
+ actual = self.converter.IngestIssueDeltas(
+ [api_delta, api_delta_ext_merged, api_delta_all_masked])
+
+ expected = [
+ (78001, expected_delta), (78002, expected_delta_ext_merged),
+ (78011, expected_all_masked_delta)
+ ]
+ self.assertEqual(actual, expected)
+
+ def testIngestIssueDeltas_OwnerAndOwnerDotUser(self):
+ # Set up.
+ self.services.project.TestAddProject('proj-780', project_id=780)
+ issue = self._Issue(780, 1)
+ self.services.issue.TestAddIssue(issue)
+
+ api_issue = issue_objects_pb2.Issue(
+ name='projects/proj-780/issues/1',
+ owner=issue_objects_pb2.Issue.UserValue(user='users/111')
+ )
+
+ # Expect ingest to work when update_mask has just 'owner'.
+ api_delta = issues_pb2.IssueDelta(
+ issue=api_issue,
+ update_mask=field_mask_pb2.FieldMask(paths=['owner'])
+ )
+ expected_delta = tracker_pb2.IssueDelta(owner_id=111)
+ expected = [(78001, expected_delta)]
+ actual = self.converter.IngestIssueDeltas([api_delta])
+ self.assertEqual(actual, expected)
+
+ # Expect ingest to also work when update_mask uses 'owner.user' instead.
+ api_delta = issues_pb2.IssueDelta(
+ issue=api_issue,
+ update_mask=field_mask_pb2.FieldMask(paths=['owner.user'])
+ )
+ actual = self.converter.IngestIssueDeltas([api_delta])
+ self.assertEqual(actual, expected)
+
+ def testIngestIssueDeltas_StatusAndStatusDotStatus(self):
+ # Set up.
+ self.services.project.TestAddProject('proj-780', project_id=780)
+ issue = self._Issue(780, 1)
+ self.services.issue.TestAddIssue(issue)
+
+ api_issue = issue_objects_pb2.Issue(
+ name='projects/proj-780/issues/1',
+ owner=issue_objects_pb2.Issue.UserValue(user='users/111'),
+ status=issue_objects_pb2.Issue.StatusValue(status='New')
+ )
+
+ # Expect ingest to work when update_mask has just 'status'.
+ api_delta = issues_pb2.IssueDelta(
+ issue=api_issue,
+ update_mask=field_mask_pb2.FieldMask(paths=['status'])
+ )
+ expected_delta = tracker_pb2.IssueDelta(status='New')
+ expected = [(78001, expected_delta)]
+ actual = self.converter.IngestIssueDeltas([api_delta])
+ self.assertEqual(actual, expected)
+
+ # Expect ingest to also work when update_mask uses 'status.status' instead.
+ api_delta = issues_pb2.IssueDelta(
+ issue=api_issue,
+ update_mask=field_mask_pb2.FieldMask(paths=['status.status'])
+ )
+ actual = self.converter.IngestIssueDeltas([api_delta])
+ self.assertEqual(actual, expected)
+
+ def testIngestIssueDeltas_RemoveNonRepeated(self):
+ # Set up.
+ self.services.project.TestAddProject('proj-780', project_id=780)
+ issue_1 = self._Issue(780, 1)
+ self.services.issue.TestAddIssue(issue_1)
+ issue_2 = self._Issue(780, 2)
+ self.services.issue.TestAddIssue(issue_2)
+
+ # Check we can remove fields without specifying them in the
+ # issue, as long as they're specified in the FieldMask.
+ api_issue = issue_objects_pb2.Issue(
+ name='projects/proj-780/issues/1')
+ api_delta = issues_pb2.IssueDelta(
+ issue=api_issue,
+ update_mask=field_mask_pb2.FieldMask(
+ paths=[
+ 'owner.user', 'status.status', 'summary',
+ 'merged_into_issue_ref.issue'
+ ]))
+
+ # Check thet setting fields to '' result in same behavior as not
+ # explicitly setting the values at all.
+ api_issue_set = issue_objects_pb2.Issue(
+ name='projects/proj-780/issues/2',
+ summary='',
+ status=issue_objects_pb2.Issue.StatusValue(status=''),
+ owner=issue_objects_pb2.Issue.UserValue(user=''),
+ merged_into_issue_ref=issue_objects_pb2.IssueRef(issue=''))
+ api_delta_set = issues_pb2.IssueDelta(
+ issue=api_issue_set,
+ update_mask=field_mask_pb2.FieldMask(
+ paths=[
+ 'owner.user', 'status.status', 'summary',
+ 'merged_into_issue_ref.issue'
+ ]))
+
+ expected_delta = tracker_pb2.IssueDelta(
+ owner_id=framework_constants.NO_USER_SPECIFIED,
+ status='',
+ summary='',
+ merged_into=0)
+
+ actual = self.converter.IngestIssueDeltas([api_delta, api_delta_set])
+ expected = [(78001, expected_delta), (78002, expected_delta)]
+ self.assertEqual(actual, expected)
+
+ def testIngestIssueDeltas_InvalidMask(self):
+ self.services.project.TestAddProject('proj-780', project_id=780)
+ issue_1 = self._Issue(780, 1)
+ self.services.issue.TestAddIssue(issue_1)
+ issue_2 = self._Issue(780, 2)
+ self.services.issue.TestAddIssue(issue_2)
+ issue_3 = self._Issue(780, 3)
+ self.services.issue.TestAddIssue(issue_3)
+ api_deltas = []
+ err_msgs = []
+
+ api_issue_1 = issue_objects_pb2.Issue(name='projects/proj-780/issues/1')
+ api_delta_1 = issues_pb2.IssueDelta(issue=api_issue_1)
+ api_deltas.append(api_delta_1)
+ err_msgs.append(
+ '`update_mask` must be set for projects/proj-780/issues/1 delta.')
+
+ api_issue_2 = issue_objects_pb2.Issue(name='projects/proj-780/issues/2')
+ api_delta_2 = issues_pb2.IssueDelta(
+ issue=api_issue_2,
+ update_mask=field_mask_pb2.FieldMask()) # Empty but set is fine.
+ api_deltas.append(api_delta_2)
+
+ api_issue_3 = issue_objects_pb2.Issue(name='projects/proj-780/issues/3')
+ api_delta_3 = issues_pb2.IssueDelta(
+ issue=api_issue_3,
+ update_mask=field_mask_pb2.FieldMask(paths=['chicken']))
+ api_deltas.append(api_delta_3)
+ err_msgs.append(
+ 'Invalid `update_mask` for projects/proj-780/issues/3 delta.')
+
+ with self.assertRaisesRegexp(exceptions.InputException,
+ '\n'.join(err_msgs)):
+ self.converter.IngestIssueDeltas(api_deltas)
+
+ def testIngestIssueDeltas_OutputOnlyIgnored(self):
+ # Set up.
+ self.services.project.TestAddProject('proj-780', project_id=780)
+ issue_1 = self._Issue(780, 1)
+ self.services.issue.TestAddIssue(issue_1)
+ comp_1 = fake.MakeTestComponentDef(780, 1)
+ fd_str = fake.MakeTestFieldDef(1, 780, tracker_pb2.FieldTypes.STR_TYPE)
+ config = fake.MakeTestConfig(780, [], [])
+ config.component_defs = [comp_1]
+ config.field_defs = [fd_str]
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ api_issue = issue_objects_pb2.Issue(
+ name='projects/proj-780/issues/1',
+ owner=issue_objects_pb2.Issue.UserValue(
+ user='users/111',
+ derivation=issue_objects_pb2.Derivation.Value('RULE')),
+ status=issue_objects_pb2.Issue.StatusValue(
+ status='KingdomCome',
+ derivation=issue_objects_pb2.Derivation.Value('RULE')),
+ state=issue_objects_pb2.IssueContentState.Value('DELETED'),
+ reporter='users/222',
+ cc_users=[
+ issue_objects_pb2.Issue.UserValue(
+ user='users/333',
+ derivation=issue_objects_pb2.Derivation.Value('RULE'))
+ ],
+ labels=[
+ issue_objects_pb2.Issue.LabelValue(
+ label='wikipedia-sections',
+ derivation=issue_objects_pb2.Derivation.Value('RULE'))
+ ],
+ components=[
+ issue_objects_pb2.Issue.ComponentValue(
+ component='projects/proj-780/componentDefs/1',
+ derivation=issue_objects_pb2.Derivation.Value('RULE'))
+ ],
+ field_values=[
+ issue_objects_pb2.FieldValue(
+ field='projects/proj-780/fieldDefs/1',
+ value='bugs',
+ derivation=issue_objects_pb2.Derivation.Value('RULE'))
+ ],
+ create_time=timestamp_pb2.Timestamp(seconds=4044242),
+ close_time=timestamp_pb2.Timestamp(seconds=4044242),
+ modify_time=timestamp_pb2.Timestamp(seconds=4044242),
+ component_modify_time=timestamp_pb2.Timestamp(seconds=4044242),
+ status_modify_time=timestamp_pb2.Timestamp(seconds=4044242),
+ owner_modify_time=timestamp_pb2.Timestamp(seconds=4044242),
+ attachment_count=4,
+ star_count=2,
+ phases=['EarlyLife', 'CrimesBegin', 'CrimesContinue'])
+ paths_with_output_only = [
+ 'owner', 'status', 'state', 'reporter', 'cc_users', 'labels',
+ 'components', 'field_values', 'create_time', 'close_time',
+ 'modify_time', 'component_modify_time', 'status_modify_time',
+ 'owner_modify_time', 'attachment_count', 'star_count', 'phases']
+ api_delta = issues_pb2.IssueDelta(
+ issue=api_issue,
+ update_mask=field_mask_pb2.FieldMask(paths=paths_with_output_only))
+
+ expected_delta = tracker_pb2.IssueDelta(
+ # We ignore all Issue.*Value.derivation OUTPUT_ONLY fields.
+ owner_id=111,
+ status='KingdomCome',
+ cc_ids_add=[333],
+ labels_add=['wikipedia-sections'],
+ comp_ids_add=[1],
+ field_vals_add=[
+ field_helpers.ParseOneFieldValue(
+ self.cnxn, self.services.user, fd_str, 'bugs')
+ ])
+
+ actual = self.converter.IngestIssueDeltas([api_delta])
+ expected = [(78001, expected_delta)]
+ self.assertEqual(actual, expected)
+
+
+ def testIngestIssueDeltas_Empty(self):
+ actual = self.converter.IngestIssueDeltas([])
+ self.assertEqual(actual, [])
+
+ def testIngestIssueDeltas_InvalidValuesForFields(self):
+ # Set up.
+ self.services.project.TestAddProject('proj-780', project_id=780)
+ issue_1 = self._Issue(780, 1)
+ self.services.issue.TestAddIssue(issue_1)
+ fd_int = fake.MakeTestFieldDef(1, 780, tracker_pb2.FieldTypes.INT_TYPE)
+ fd_date = fake.MakeTestFieldDef(2, 780, tracker_pb2.FieldTypes.DATE_TYPE)
+ config = fake.MakeTestConfig(780, [], [])
+ config.field_defs = [fd_int, fd_date]
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ api_issue = issue_objects_pb2.Issue(
+ name='projects/proj-780/issues/1',
+ field_values=[
+ issue_objects_pb2.FieldValue(
+ field='projects/proj-780/fieldDefs/1',
+ value='NotAnInt',
+ derivation=issue_objects_pb2.Derivation.Value('RULE')),
+ issue_objects_pb2.FieldValue(
+ field='projects/proj-780/fieldDefs/2',
+ value='NoDate',
+ derivation=issue_objects_pb2.Derivation.Value('EXPLICIT')),
+ ],
+ )
+ api_delta = issues_pb2.IssueDelta(
+ issue=api_issue,
+ update_mask=field_mask_pb2.FieldMask(paths=['field_values']))
+ error_messages = [
+ r'Could not ingest value \(NotAnInt\) for FieldDef \(projects/proj-780/'
+ r'fieldDefs/1\): Could not parse NotAnInt',
+ r'Could not ingest value \(NoDate\) for FieldDef \(projects/proj-780/fi'
+ r'eldDefs/2\): Could not parse NoDate',
+ ]
+ error_messages_re = '\n'.join(error_messages)
+ with self.assertRaisesRegexp(exceptions.InputException, error_messages_re):
+ self.converter.IngestIssueDeltas([api_delta])
+
+ @mock.patch('time.time', mock.MagicMock(return_value=CURRENT_TIME))
+ def testIngestApprovalDeltas(self):
+ mask = field_mask_pb2.FieldMask(
+ paths=['approvers', 'status', 'setter', 'phase', 'set_time'])
+ av_name = (
+ 'projects/proj/issues/1/approvalValues/%d' % self.approval_def_1_id)
+ approval_delta = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(
+ name=av_name,
+ status=issue_objects_pb2.ApprovalValue.ApprovalStatus.Value('NA'),
+ approvers=['users/222', 'users/333'],
+ approval_def='ignored',
+ set_time=timestamp_pb2.Timestamp(), # Ignored.
+ setter='ignored',
+ phase='ignored'),
+ update_mask=mask,
+ approvers_remove=['users/222'])
+ actual = self.converter.IngestApprovalDeltas(
+ [approval_delta], self.user_1.user_id)
+ expected_delta = tracker_pb2.ApprovalDelta(
+ status=tracker_pb2.ApprovalStatus.NA,
+ setter_id=self.user_1.user_id,
+ set_on=int(CURRENT_TIME),
+ approver_ids_add=[222, 333],
+ approver_ids_remove=[222],
+ )
+ expected_delta_specifications = [
+ (self.issue_1.issue_id, self.approval_def_1_id, expected_delta)
+ ]
+ self.assertEqual(actual, expected_delta_specifications)
+
+ def testIngestApprovalDeltas_EmptyMask(self):
+ av_name = (
+ 'projects/proj/issues/1/approvalValues/%d' % self.approval_def_1_id)
+ # field_def_6 belongs to approval_def_1.
+ approval_fv = issue_objects_pb2.FieldValue(
+ field='projects/proj/fieldDefs/%d' % self.field_def_6, value=u'x')
+ approval_delta = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(
+ name=av_name,
+ status=issue_objects_pb2.ApprovalValue.ApprovalStatus.Value('NA'),
+ approvers=['users/222', 'users/333'],
+ approval_def='ignored',
+ field_values=[approval_fv],
+ set_time=timestamp_pb2.Timestamp(), # Ignored.
+ setter='ignored',
+ phase='ignored'),
+ update_mask=field_mask_pb2.FieldMask(),
+ approvers_remove=['users/222'])
+ actual = self.converter.IngestApprovalDeltas(
+ [approval_delta], self.user_1.user_id)
+ expected_delta = tracker_pb2.ApprovalDelta(approver_ids_remove=[222])
+ expected_delta_specifications = [
+ (self.issue_1.issue_id, self.approval_def_1_id, expected_delta)
+ ]
+ self.assertEqual(actual, expected_delta_specifications)
+
+ def testIngestApprovalDeltas_InvalidMask(self):
+ av_name = (
+ 'projects/proj/issues/1/approvalValues/%d' % self.approval_def_1_id)
+ approval_delta = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(name=av_name),
+ update_mask=field_mask_pb2.FieldMask(paths=['chicken']))
+ expected_err = 'Invalid `update_mask` for %s delta' % av_name
+ with self.assertRaisesRegexp(exceptions.InputException, expected_err):
+ self.converter.IngestApprovalDeltas([approval_delta], self.user_1.user_id)
+
+ def testIngestApprovalDeltas_FilterFieldValues(self):
+ av_name = (
+ 'projects/proj/issues/1/approvalValues/%d' % self.approval_def_1_id)
+
+ # field_def_6 belongs to approval_def_1, should be ingested.
+ approval_fv = issue_objects_pb2.FieldValue(
+ field='projects/proj/fieldDefs/%d' % self.field_def_6,
+ value=u'touch-nose',
+ derivation=RULE_DERIVATION, # Ignored.
+ )
+ # An enum field belonging to approval_def_1, should be ingested.
+ approval_enum_field_id = self._CreateFieldDef(
+ self.project_1.project_id,
+ 'approval2field',
+ 'ENUM_TYPE',
+ approval_id=self.approval_def_1_id)
+ approval_enum_fv = issue_objects_pb2.FieldValue(
+ field='projects/proj/fieldDefs/%d' % approval_enum_field_id,
+ value=u'enumval')
+ # Create field value that points to different approval, should raise error.
+ approval_2_fv = issue_objects_pb2.FieldValue(
+ field='projects/proj/fieldDefs/%d' % self.field_def_2, value=u'error')
+ av = issue_objects_pb2.ApprovalValue(
+ name=av_name, field_values=[approval_fv])
+ approval_delta = issues_pb2.ApprovalDelta(
+ update_mask=field_mask_pb2.FieldMask(paths=['field_values']),
+ approval_value=av,
+ field_vals_remove=[approval_enum_fv, approval_2_fv],
+ approvers_remove=['users/222'],
+ )
+ with self.assertRaisesRegexp(exceptions.InputException,
+ 'Field .* does not belong to approval .*'):
+ self.converter.IngestApprovalDeltas([approval_delta], self.user_1.user_id)
+
+ def testIngestApprovalDeltas_InvalidFieldValues(self):
+ av_name = (
+ 'projects/proj/issues/1/approvalValues/%d' % self.approval_def_1_id)
+ approval_fv = issue_objects_pb2.FieldValue(
+ field='projects/proj/fieldDefs/%d' % self.field_def_6,
+ value=u'touch-nose',
+ derivation=RULE_DERIVATION, # Ignored.
+ )
+ other_fv = issue_objects_pb2.FieldValue(
+ field='projects/proj/fieldDefs/%d' % self.field_def_1,
+ value=u'something',
+ )
+ # This does not exist, and should throw error.
+ dne_fv = issue_objects_pb2.FieldValue(
+ field='projects/proj/fieldDefs/404',
+ value=u'DoesNotExist',
+ )
+ av = issue_objects_pb2.ApprovalValue(
+ name=av_name, field_values=[other_fv, approval_fv, dne_fv])
+ approval_delta = issues_pb2.ApprovalDelta(
+ update_mask=field_mask_pb2.FieldMask(paths=['field_values']),
+ approval_value=av,
+ approvers_remove=['users/222'],
+ )
+ with self.assertRaisesRegexp(
+ exceptions.InputException,
+ 'Field projects/proj/fieldDefs/404 is not in this project'):
+ self.converter.IngestApprovalDeltas([approval_delta], self.user_1.user_id)
+
+ def testIngestApprovalDeltas_WrongProject(self):
+ approval_def_project2_name = 'project2_approval'
+ approval_def_project2_id = self._CreateFieldDef(
+ self.project_2.project_id,
+ approval_def_project2_name,
+ 'APPROVAL_TYPE',
+ docstring='project2_ad_docstring',
+ admin_ids=[self.user_1.user_id])
+ self.services.config.UpdateConfig(
+ self.cnxn,
+ self.project_2,
+ approval_defs=[
+ (approval_def_project2_id, [self.user_1.user_id], 'survey')
+ ])
+ wrong_project_av_name = (
+ 'projects/proj/issues/1/approvalValues/%d' % approval_def_project2_id)
+ approval_delta = issues_pb2.ApprovalDelta(
+ update_mask=field_mask_pb2.FieldMask(),
+ approval_value=issue_objects_pb2.ApprovalValue(
+ name=wrong_project_av_name))
+ with self.assertRaises(exceptions.InputException):
+ self.converter.IngestApprovalDeltas([approval_delta], self.user_1.user_id)
+
+ def testIngestApprovalDeltas_DoesNotExist(self):
+ dne_av_name = ('projects/proj/issues/1/approvalValues/404')
+ approval_delta = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(name=dne_av_name),
+ update_mask=field_mask_pb2.FieldMask())
+ with self.assertRaises(exceptions.InputException):
+ self.converter.IngestApprovalDeltas([approval_delta], self.user_1.user_id)
+
+ def testIngestApprovalDeltas_NonApproval(self):
+ """We fail if provided a non-approval Field ID in the resource name."""
+ dne_av_name = (
+ 'projects/proj/issues/1/approvalValues/%s' % self.field_def_1)
+ approval_delta = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(name=dne_av_name),
+ update_mask=field_mask_pb2.FieldMask())
+ with self.assertRaises(exceptions.InputException):
+ self.converter.IngestApprovalDeltas([approval_delta], self.user_1.user_id)
+
+ def testIngestApprovalDeltas_IssueDoesNotExist(self):
+ dne_av_name = (
+ 'projects/proj/issues/404/approvalValues/%d' % self.approval_def_1_id)
+ approval_delta = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(name=dne_av_name),
+ update_mask=field_mask_pb2.FieldMask())
+ with self.assertRaises(exceptions.NoSuchIssueException):
+ self.converter.IngestApprovalDeltas([approval_delta], self.user_1.user_id)
+
+ def testIngestApprovalDeltas_EmptyDelta(self):
+ av_name = (
+ 'projects/proj/issues/1/approvalValues/%d' % self.approval_def_1_id)
+ approval_delta = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(name=av_name),
+ update_mask=field_mask_pb2.FieldMask())
+
+ actual = self.converter.IngestApprovalDeltas(
+ [approval_delta], self.user_1.user_id)
+
+ expected_delta = tracker_pb2.ApprovalDelta()
+ expected_delta_specifications = [
+ (self.issue_1.issue_id, self.approval_def_1_id, expected_delta)
+ ]
+ self.assertEqual(actual, expected_delta_specifications)
+
+ def testIngestApprovalDeltas_InvalidName(self):
+ approval_delta = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(name='x'))
+ with self.assertRaises(exceptions.InputException):
+ self.converter.IngestApprovalDeltas([approval_delta], self.user_1.user_id)
+
+ def testIngestApprovalDeltas_NoName(self):
+ approval_delta = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(
+ status=issue_objects_pb2.ApprovalValue.ApprovalStatus.Value('NA')))
+ with self.assertRaises(exceptions.InputException):
+ self.converter.IngestApprovalDeltas([approval_delta], self.user_1.user_id)
+
+ def testIngestApprovalDeltas_NoStatus(self):
+ """Setter ID isn't set when status isn't set."""
+ av_name = (
+ 'projects/proj/issues/1/approvalValues/%d' % self.approval_def_1_id)
+ approval_delta = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(
+ name=av_name,
+ status=issue_objects_pb2.ApprovalValue.ApprovalStatus.Value('NA'),
+ approvers=['users/333']),
+ # Status left out of update mask.
+ update_mask=field_mask_pb2.FieldMask(paths=['approvers']),
+ approvers_remove=['users/222'])
+ actual = self.converter.IngestApprovalDeltas(
+ [approval_delta], self.user_1.user_id)
+ expected_delta = tracker_pb2.ApprovalDelta(
+ approver_ids_add=[333], approver_ids_remove=[222])
+ expected_delta_specifications = [
+ (self.issue_1.issue_id, self.approval_def_1_id, expected_delta)
+ ]
+ self.assertEqual(actual, expected_delta_specifications)
+
+ def testIngestApprovalDeltas_ApproverRemoveDoesNotExist(self):
+ av_name = (
+ 'projects/proj/issues/1/approvalValues/%d' % self.approval_def_1_id)
+ approval_delta = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(name=av_name),
+ update_mask=field_mask_pb2.FieldMask(),
+ approvers_remove=['users/nobody@404.com'])
+ with self.assertRaises(exceptions.NoSuchUserException):
+ self.converter.IngestApprovalDeltas([approval_delta], self.user_1.user_id)
+
+ def testIngestApprovalDeltas_ApproverAddDoesNotExist(self):
+ av_name = (
+ 'projects/proj/issues/1/approvalValues/%d' % self.approval_def_1_id)
+ approval_delta = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(
+ name=av_name, approvers=['users/nobody@404.com']),
+ update_mask=field_mask_pb2.FieldMask(paths=['approvers']))
+ with self.assertRaises(exceptions.NoSuchUserException):
+ self.converter.IngestApprovalDeltas([approval_delta], self.user_1.user_id)
+
+ def testIngestApprovalDeltas_FirstErrorRaised(self):
+ """Until we have error aggregation, we raise the first found error."""
+ av_name = (
+ 'projects/proj/issues/1/approvalValues/%d' % self.approval_def_1_id)
+ user_dne_delta = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(
+ name=av_name, approvers=['users/nobody@404.com']),
+ update_mask=field_mask_pb2.FieldMask(paths=['approvers']))
+ invalid_name_delta = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(name='garbage'))
+ with self.assertRaises(exceptions.NoSuchUserException):
+ self.converter.IngestApprovalDeltas(
+ [user_dne_delta, invalid_name_delta], self.user_1.user_id)
+
+ def testIngestApprovalDeltas_MultipleDeltasSameSetOn(self):
+ av_name = (
+ 'projects/proj/issues/1/approvalValues/%d' % self.approval_def_1_id)
+ delta_1 = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(
+ name=av_name,
+ status=issue_objects_pb2.ApprovalValue.ApprovalStatus.Value('NA'),
+ approvers=['users/222']),
+ update_mask=field_mask_pb2.FieldMask(paths=['approvers', 'status']))
+ # Change status, and also ensure we don't reuse the same mask across deltas
+ # Approvers should be ignored for delta_2 because it is not included in the
+ # mask.
+ delta_2 = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(
+ name=av_name,
+ status=issue_objects_pb2.ApprovalValue.ApprovalStatus.Value(
+ 'NOT_SET'),
+ approvers=['users/222']),
+ update_mask=field_mask_pb2.FieldMask(paths=['status']))
+ actual = self.converter.IngestApprovalDeltas(
+ [delta_1, delta_2], self.user_1.user_id)
+ self.assertEqual(len(actual), 2)
+ actual_iid_1, actual_approval_id_1, actual_delta_1 = actual[0]
+ actual_iid_2, actual_approval_id_2, actual_delta_2 = actual[1]
+ self.assertEqual(actual_iid_1, self.issue_1.issue_id)
+ self.assertEqual(actual_iid_2, self.issue_1.issue_id)
+ self.assertEqual(actual_approval_id_1, self.approval_def_1_id)
+ self.assertEqual(actual_approval_id_2, self.approval_def_1_id)
+
+ self.assertEqual(actual_delta_1.status, tracker_pb2.ApprovalStatus.NA)
+ self.assertEqual(actual_delta_2.status, tracker_pb2.ApprovalStatus.NOT_SET)
+ self.assertEqual(actual_delta_1.setter_id, self.user_1.user_id)
+ self.assertEqual(actual_delta_2.setter_id, self.user_1.user_id)
+ self.assertEqual(actual_delta_1.approver_ids_add, [222])
+ self.assertEqual(actual_delta_2.approver_ids_add, [])
+ # We don't patch time.time, so these would be different if the set_on wasn't
+ # passed in.
+ # Note: More ideal/correct unit test would create a mock that forces
+ # time.time to return an incremented value on its subsequent calls.
+ self.assertEqual(actual_delta_1.set_on, actual_delta_2.set_on)
+
+ def testIngestApprovalDeltas_DifferentProjects(self):
+ # Create an ApprovalDef for project2
+ approval_def_project2_name = 'project2_approval'
+ approval_def_project2_id = self._CreateFieldDef(
+ self.project_2.project_id,
+ approval_def_project2_name,
+ 'APPROVAL_TYPE',
+ docstring='project2_ad_docstring',
+ admin_ids=[self.user_1.user_id])
+ self.services.config.UpdateConfig(
+ self.cnxn,
+ self.project_2,
+ approval_defs=[
+ (approval_def_project2_id, [self.user_1.user_id], 'survey')
+ ])
+
+ # Define a field belonging to project_2's ApprovalDef.
+ project2_field_id = self._CreateFieldDef(
+ self.project_2.project_id,
+ 'approval2field',
+ 'STR_TYPE',
+ approval_id=approval_def_project2_id)
+ project2_fv = issue_objects_pb2.FieldValue(
+ field='projects/proj/fieldDefs/%d' % project2_field_id, value=u'p2')
+
+ # field_def_6 belongs to approval_def_1.
+ project1_fv = issue_objects_pb2.FieldValue(
+ field='projects/proj/fieldDefs/%d' % self.field_def_6,
+ value=u'touch-nose',
+ )
+
+ # Both ApprovalValues are provided both FieldValues, and we expect them
+ # to only include the FieldValues appropriate to their respective approvals.
+ project2_av_name = (
+ 'projects/%s/issues/2/approvalValues/%d' %
+ (self.project_2.project_name, approval_def_project2_id))
+ project2_delta = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(
+ name=project2_av_name, field_values=[project1_fv, project2_fv]),
+ update_mask=field_mask_pb2.FieldMask(paths=['field_values']))
+
+ project1_av_name = (
+ 'projects/proj/issues/1/approvalValues/%d' % self.approval_def_1_id)
+ project1_delta = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(
+ name=project1_av_name, field_values=[project1_fv, project2_fv]),
+ update_mask=field_mask_pb2.FieldMask(paths=['field_values']))
+
+ with self.assertRaisesRegexp(
+ exceptions.InputException,
+ 'Field projects/proj/fieldDefs/%d is not in this project' %
+ self.field_def_6):
+ self.converter.IngestApprovalDeltas(
+ [project2_delta, project1_delta], self.user_1.user_id)
+
+ def testIngestIssue(self):
+ ingest = issue_objects_pb2.Issue(
+ summary='sum',
+ status=issue_objects_pb2.Issue.StatusValue(
+ status='new', derivation=RULE_DERIVATION),
+ owner=issue_objects_pb2.Issue.UserValue(
+ derivation=EXPLICIT_DERIVATION, user='users/111'),
+ cc_users=[
+ issue_objects_pb2.Issue.UserValue(
+ derivation=EXPLICIT_DERIVATION, user='users/new@user.com'),
+ issue_objects_pb2.Issue.UserValue(
+ derivation=RULE_DERIVATION, user='users/333')
+ ],
+ components=[
+ issue_objects_pb2.Issue.ComponentValue(
+ component='projects/proj/componentDefs/%d' %
+ self.component_def_1_id),
+ issue_objects_pb2.Issue.ComponentValue(
+ component='projects/proj/componentDefs/%d' %
+ self.component_def_2_id),
+ ],
+ labels=[
+ issue_objects_pb2.Issue.LabelValue(
+ derivation=EXPLICIT_DERIVATION, label='a'),
+ issue_objects_pb2.Issue.LabelValue(
+ derivation=EXPLICIT_DERIVATION, label='key-explicit'),
+ issue_objects_pb2.Issue.LabelValue(
+ derivation=RULE_DERIVATION, label='derived1'),
+ issue_objects_pb2.Issue.LabelValue(
+ derivation=RULE_DERIVATION, label='key-derived')
+ ],
+ field_values=[
+ issue_objects_pb2.FieldValue(
+ derivation=EXPLICIT_DERIVATION,
+ field='projects/proj/fieldDefs/%d' % self.field_def_1,
+ value='multivalue1',
+ ),
+ issue_objects_pb2.FieldValue(
+ derivation=RULE_DERIVATION,
+ field='projects/proj/fieldDefs/%d' % self.field_def_1,
+ value='multivalue2',
+ ),
+ issue_objects_pb2.FieldValue(
+ derivation=EXPLICIT_DERIVATION,
+ field='projects/proj/fieldDefs/%d' % self.field_def_3,
+ value='1',
+ ),
+ issue_objects_pb2.FieldValue(
+ derivation=RULE_DERIVATION,
+ field='projects/proj/fieldDefs/%d' % self.field_def_4,
+ value='mac',
+ ),
+ issue_objects_pb2.FieldValue(
+ field='projects/proj/fieldDefs/%d' % self.field_def_2,
+ value='38', # Max value not checked.
+ ),
+ issue_objects_pb2.FieldValue( # Multivalue not checked.
+ field='projects/proj/fieldDefs/%d' % self.field_def_2,
+ value='0' # Confirm we ingest 0 rather than None.
+ ),
+ issue_objects_pb2.FieldValue(
+ field='projects/proj/fieldDefs/%d' % self.field_def_8,
+ value='users/111',
+ ),
+ issue_objects_pb2.FieldValue(
+ field='projects/proj/fieldDefs/%d' % self.field_def_8,
+ value='users/404', # User lookup not attempted.
+ ),
+ issue_objects_pb2.FieldValue(
+ field='projects/proj/fieldDefs/%d' % self.field_def_9,
+ value='2020-01-01',
+ ),
+ issue_objects_pb2.FieldValue(
+ field='projects/proj/fieldDefs/%d' % self.field_def_9,
+ value='2100-01-01',
+ ),
+ issue_objects_pb2.FieldValue(
+ field='projects/proj/fieldDefs/%d' % self.field_def_9,
+ value='1000-01-01',
+ ),
+ issue_objects_pb2.FieldValue(
+ field='projects/proj/fieldDefs/%d' % self.field_def_10,
+ value='garbage',
+ ),
+ ],
+ merged_into_issue_ref=issue_objects_pb2.IssueRef(ext_identifier='b/1'),
+ blocked_on_issue_refs=[
+ # Reversing natural ordering to ensure order is respected.
+ issue_objects_pb2.IssueRef(issue='projects/goose/issues/4'),
+ issue_objects_pb2.IssueRef(issue='projects/proj/issues/3'),
+ issue_objects_pb2.IssueRef(ext_identifier='b/555'),
+ issue_objects_pb2.IssueRef(ext_identifier='b/2')
+ ],
+ blocking_issue_refs=[
+ issue_objects_pb2.IssueRef(issue='projects/goose/issues/5'),
+ issue_objects_pb2.IssueRef(ext_identifier='b/3')
+ ],
+ # All the following fields should be ignored.
+ name='projects/proj/issues/1',
+ state=issue_objects_pb2.IssueContentState.Value('SPAM'),
+ reporter='users/111',
+ create_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ modify_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ component_modify_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ status_modify_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ owner_modify_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ star_count=1,
+ attachment_count=5,
+ phases=[self.phase_1.name])
+
+ blocked_on_1 = fake.MakeTestIssue(
+ self.project_1.project_id,
+ 3,
+ 'sum3',
+ 'New',
+ self.user_1.user_id,
+ issue_id=301,
+ project_name=self.project_1.project_name,
+ )
+ blocked_on_2 = fake.MakeTestIssue(
+ self.project_2.project_id,
+ 4,
+ 'sum4',
+ 'New',
+ self.user_1.user_id,
+ issue_id=401,
+ project_name=self.project_2.project_name,
+ )
+ blocking = fake.MakeTestIssue(
+ self.project_2.project_id,
+ 5,
+ 'sum5',
+ 'New',
+ self.user_1.user_id,
+ issue_id=501,
+ project_name=self.project_2.project_name,
+ )
+ self.services.issue.TestAddIssue(blocked_on_1)
+ self.services.issue.TestAddIssue(blocked_on_2)
+ self.services.issue.TestAddIssue(blocking)
+
+ actual = self.converter.IngestIssue(ingest, self.project_1.project_id)
+
+ expected_cc1_id = self.services.user.LookupUserID(
+ self.cnxn, 'new@user.com', autocreate=False)
+ expected_field_values = [
+ tracker_pb2.FieldValue(
+ field_id=self.field_def_1,
+ str_value=u'multivalue1',
+ derived=False,
+ ),
+ tracker_pb2.FieldValue(
+ field_id=self.field_def_1,
+ str_value=u'multivalue2',
+ derived=False,
+ ),
+ tracker_pb2.FieldValue(
+ field_id=self.field_def_2, int_value=38, derived=False),
+ tracker_pb2.FieldValue(
+ field_id=self.field_def_2, int_value=0, derived=False),
+ tracker_pb2.FieldValue(
+ field_id=self.field_def_8, user_id=111, derived=False),
+ tracker_pb2.FieldValue(
+ field_id=self.field_def_8, user_id=404, derived=False),
+ tracker_pb2.FieldValue(
+ field_id=self.field_def_9, date_value=1577836800, derived=False),
+ tracker_pb2.FieldValue(
+ field_id=self.field_def_9, date_value=4102444800, derived=False),
+ tracker_pb2.FieldValue(
+ field_id=self.field_def_9, date_value=-30610224000, derived=False),
+ tracker_pb2.FieldValue(
+ field_id=self.field_def_10,
+ url_value=u'http://garbage',
+ derived=False),
+ ]
+ expected = tracker_pb2.Issue(
+ project_id=self.project_1.project_id,
+ summary=u'sum',
+ status=u'new',
+ owner_id=111,
+ cc_ids=[expected_cc1_id, 333],
+ component_ids=[self.component_def_1_id, self.component_def_2_id],
+ merged_into_external=u'b/1',
+ labels=[
+ u'a', u'key-explicit', u'derived1', u'key-derived', u'days-1',
+ u'OS-mac'
+ ],
+ field_values=expected_field_values,
+ blocked_on_iids=[blocked_on_2.issue_id, blocked_on_1.issue_id],
+ blocking_iids=[blocking.issue_id],
+ dangling_blocked_on_refs=[
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier=u'b/555'),
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier=u'b/2')
+ ],
+ dangling_blocking_refs=[
+ tracker_pb2.DanglingIssueRef(ext_issue_identifier=u'b/3')
+ ],
+ )
+ self.AssertProtosEqual(actual, expected)
+
+ def AssertProtosEqual(self, actual, expected):
+ """Asserts equal, printing a diff if not."""
+ # TODO(jessan): If others find this useful, move to a shared testing lib.
+ try:
+ self.assertEqual(actual, expected)
+ except AssertionError as e:
+ # Append a diff to the normal error message.
+ expected_str = str(expected).splitlines(1)
+ actual_str = str(actual).splitlines(1)
+ diff = difflib.unified_diff(actual_str, expected_str)
+ err_msg = '%s\nProto actual vs expected diff:\n %s' % (e, ''.join(diff))
+ raise AssertionError(err_msg)
+
+ def testIngestIssue_Minimal(self):
+ """Test IngestIssue with as few fields set as possible."""
+ minimal = issue_objects_pb2.Issue(
+ status=issue_objects_pb2.Issue.StatusValue(status='new')
+ )
+ expected = tracker_pb2.Issue(
+ project_id=self.project_1.project_id,
+ summary='', # Summary gets set to empty str on conversion.
+ status='new',
+ owner_id=0
+ )
+ actual = self.converter.IngestIssue(minimal, self.project_1.project_id)
+ self.assertEqual(actual, expected)
+
+ def testIngestIssue_NoSuchProject(self):
+ self.services.config.strict = True
+ ingest = issue_objects_pb2.Issue(
+ status=issue_objects_pb2.Issue.StatusValue(status='new'))
+ with self.assertRaises(exceptions.NoSuchProjectException):
+ self.converter.IngestIssue(ingest, -1)
+
+ def testIngestIssue_Errors(self):
+ invalid_issue_ref = issue_objects_pb2.IssueRef(
+ ext_identifier='b/1',
+ issue='projects/proj/issues/1')
+ ingest = issue_objects_pb2.Issue(
+ summary='sum',
+ owner=issue_objects_pb2.Issue.UserValue(
+ derivation=EXPLICIT_DERIVATION, user='users/nonexisting@user.com'),
+ cc_users=[
+ issue_objects_pb2.Issue.UserValue(
+ derivation=EXPLICIT_DERIVATION, user='invalidFormat1'),
+ issue_objects_pb2.Issue.UserValue(
+ derivation=RULE_DERIVATION, user='invalidFormat2')
+ ],
+ components=[
+ issue_objects_pb2.Issue.ComponentValue(
+ component='projects/proj/componentDefs/404')
+ ],
+ field_values=[
+ issue_objects_pb2.FieldValue(),
+ issue_objects_pb2.FieldValue(field='garbage'),
+ issue_objects_pb2.FieldValue(
+ field='projects/proj/fieldDefs/%d' % self.field_def_8,
+ value='users/nonexisting@user.com',
+ ),
+ ],
+ merged_into_issue_ref=invalid_issue_ref,
+ blocked_on_issue_refs=[
+ issue_objects_pb2.IssueRef(),
+ issue_objects_pb2.IssueRef(issue='projects/404/issues/1')
+ ],
+ blocking_issue_refs=[
+ issue_objects_pb2.IssueRef(issue='projects/proj/issues/404')
+ ],
+ )
+ error_messages = [
+ r'.+not found when ingesting owner',
+ r'.+cc_users: Invalid resource name: invalidFormat1.',
+ r'Status is required when creating an issue',
+ r'.+components: Component not found: 404.',
+ r'.+: Invalid resource name: .', r'.+: Invalid resource name: garbage.',
+ r'.+not found when ingesting user field:.+',
+ r'.+issue:.+[\n\r]+ext_identifier:.+[\n\r]+: IssueRefs MUST NOT have.+',
+ r'.+: IssueRefs MUST have one of.+',
+ r'.+issue:.+[\n\r]+: Project 404 not found.',
+ r'.+issue:.+[\n\r]+: Issue.+404.+not found'
+ ]
+ error_messages_re = '\n'.join(error_messages)
+ with self.assertRaisesRegexp(exceptions.InputException, error_messages_re):
+ self.converter.IngestIssue(ingest, self.project_1.project_id)
+
+ def testIngestIssuesListColumns(self):
+ columns = [
+ issue_objects_pb2.IssuesListColumn(column='chicken'),
+ issue_objects_pb2.IssuesListColumn(column='boiled-egg')
+ ]
+ self.assertEqual(
+ self.converter.IngestIssuesListColumns(columns), 'chicken boiled-egg')
+
+ def testIngestIssuesListColumns_Empty(self):
+ self.assertEqual(self.converter.IngestIssuesListColumns([]), '')
+
+ def test_ComputeIssuesListColumns(self):
+ """Can convert string to sequence of IssuesListColumns"""
+ expected_columns = [
+ issue_objects_pb2.IssuesListColumn(column='chicken'),
+ issue_objects_pb2.IssuesListColumn(column='boiled-egg')
+ ]
+ self.assertEqual(
+ expected_columns,
+ self.converter._ComputeIssuesListColumns('chicken boiled-egg'))
+
+ def test_ComputeIssuesListColumns_Empty(self):
+ """Can handle empty strings"""
+ self.assertEqual([], self.converter._ComputeIssuesListColumns(''))
+
+ def test_Conversion_IssuesListColumns(self):
+ """_Ingest and _Compute converts to and from each other"""
+ expected_columns = 'foo bar fizz buzz'
+ converted_columns = self.converter._ComputeIssuesListColumns(
+ expected_columns)
+ self.assertEqual(
+ expected_columns,
+ self.converter.IngestIssuesListColumns(converted_columns))
+
+ expected_columns = [
+ issue_objects_pb2.IssuesListColumn(column='foo'),
+ issue_objects_pb2.IssuesListColumn(column='bar'),
+ issue_objects_pb2.IssuesListColumn(column='fizz'),
+ issue_objects_pb2.IssuesListColumn(column='buzz')
+ ]
+ converted_columns = self.converter.IngestIssuesListColumns(expected_columns)
+ self.assertEqual(
+ expected_columns,
+ self.converter._ComputeIssuesListColumns(converted_columns))
+
+ def testIngestNotifyType(self):
+ notify = issues_pb2.NotifyType.Value('NOTIFY_TYPE_UNSPECIFIED')
+ actual = self.converter.IngestNotifyType(notify)
+ self.assertEqual(actual, True)
+ notify = issues_pb2.NotifyType.Value('EMAIL')
+ actual = self.converter.IngestNotifyType(notify)
+ self.assertEqual(actual, True)
+ notify = issues_pb2.NotifyType.Value('NO_NOTIFICATION')
+ actual = self.converter.IngestNotifyType(notify)
+ self.assertEqual(actual, False)
+
+ def test_GetNonApprovalFieldValues(self):
+ """It filters out field values that belong to approvals"""
+ expected_str = 'some_string_field_value'
+ fv_expected = fake.MakeFieldValue(
+ field_id=self.field_def_1, str_value=expected_str, derived=False)
+ actual = self.converter._GetNonApprovalFieldValues(
+ [fv_expected, self.fv_6], self.project_1.project_id)
+ self.assertEqual(len(actual), 1)
+ self.assertEqual(actual[0], fv_expected)
+
+ def test_GetNonApprovalFieldValues_Empty(self):
+ actual = self.converter._GetNonApprovalFieldValues(
+ [], self.project_1.project_id)
+ self.assertEqual(actual, [])
+
+ def testConvertFieldValues(self):
+ """It ignores field values referencing a non-existent field"""
+ expected_str = 'some_string_field_value'
+ fv = fake.MakeFieldValue(
+ field_id=self.field_def_1, str_value=expected_str, derived=False)
+ expected_name = rnc.ConvertFieldDefNames(
+ self.cnxn, [self.field_def_1], self.project_1.project_id,
+ self.services)[self.field_def_1]
+ expected_value = issue_objects_pb2.FieldValue(
+ field=expected_name,
+ value=expected_str,
+ derivation=EXPLICIT_DERIVATION,
+ phase=None)
+ output = self.converter.ConvertFieldValues(
+ [fv], self.project_1.project_id, [])
+ self.assertEqual([expected_value], output)
+
+ def testConvertFieldValues_Empty(self):
+ output = self.converter.ConvertFieldValues(
+ [], self.project_1.project_id, [])
+ self.assertEqual([], output)
+
+ def testConvertFieldValues_PreservesOrder(self):
+ """It ignores field values referencing a non-existent field"""
+ expected_str = 'some_string_field_value'
+ fv_1 = fake.MakeFieldValue(
+ field_id=self.field_def_1, str_value=expected_str, derived=False)
+ name_1 = rnc.ConvertFieldDefNames(
+ self.cnxn, [self.field_def_1], self.project_1.project_id,
+ self.services)[self.field_def_1]
+ expected_1 = issue_objects_pb2.FieldValue(
+ field=name_1,
+ value=expected_str,
+ derivation=EXPLICIT_DERIVATION,
+ phase=None)
+
+ expected_int = 111111
+ fv_2 = fake.MakeFieldValue(
+ field_id=self.field_def_2, int_value=expected_int, derived=True)
+ name_2 = rnc.ConvertFieldDefNames(
+ self.cnxn, [self.field_def_2], self.project_1.project_id,
+ self.services).get(self.field_def_2)
+ expected_2 = issue_objects_pb2.FieldValue(
+ field=name_2,
+ value=str(expected_int),
+ derivation=RULE_DERIVATION,
+ phase=None)
+ output = self.converter.ConvertFieldValues(
+ [fv_1, fv_2], self.project_1.project_id, [])
+ self.assertEqual([expected_1, expected_2], output)
+
+ def testConvertFieldValues_IgnoresNullFieldDefs(self):
+ """It ignores field values referencing a non-existent field"""
+ expected_str = 'some_string_field_value'
+ fv_1 = fake.MakeFieldValue(
+ field_id=self.field_def_1, str_value=expected_str, derived=False)
+ name_1 = rnc.ConvertFieldDefNames(
+ self.cnxn, [self.field_def_1], self.project_1.project_id,
+ self.services)[self.field_def_1]
+ expected_1 = issue_objects_pb2.FieldValue(
+ field=name_1,
+ value=expected_str,
+ derivation=EXPLICIT_DERIVATION,
+ phase=None)
+
+ fv_2 = fake.MakeFieldValue(
+ field_id=self.dne_field_def_id, int_value=111111, derived=True)
+ output = self.converter.ConvertFieldValues(
+ [fv_1, fv_2], self.project_1.project_id, [])
+ self.assertEqual([expected_1], output)
+
+ def test_ComputeFieldValueString_None(self):
+ with self.assertRaises(exceptions.InputException):
+ self.converter._ComputeFieldValueString(None)
+
+ def test_ComputeFieldValueString_INT_TYPE(self):
+ expected = 123158
+ fv = fake.MakeFieldValue(field_id=self.field_def_2, int_value=expected)
+ output = self.converter._ComputeFieldValueString(fv)
+ self.assertEqual(str(expected), output)
+
+ def test_ComputeFieldValueString_STR_TYPE(self):
+ expected = 'some_string_field_value'
+ fv = fake.MakeFieldValue(field_id=self.field_def_1, str_value=expected)
+ output = self.converter._ComputeFieldValueString(fv)
+ self.assertEqual(expected, output)
+
+ def test_ComputeFieldValueString_USER_TYPE(self):
+ user_id = self.user_1.user_id
+ expected = rnc.ConvertUserName(user_id)
+ fv = fake.MakeFieldValue(field_id=self.dne_field_def_id, user_id=user_id)
+ output = self.converter._ComputeFieldValueString(fv)
+ self.assertEqual(expected, output)
+
+ def test_ComputeFieldValueString_DATE_TYPE(self):
+ expected = 1234567890
+ fv = fake.MakeFieldValue(
+ field_id=self.dne_field_def_id, date_value=expected)
+ output = self.converter._ComputeFieldValueString(fv)
+ self.assertEqual(str(expected), output)
+
+ def test_ComputeFieldValueString_URL_TYPE(self):
+ expected = 'some URL'
+ fv = fake.MakeFieldValue(field_id=self.dne_field_def_id, url_value=expected)
+ output = self.converter._ComputeFieldValueString(fv)
+ self.assertEqual(expected, output)
+
+ def test_ComputeFieldValueDerivation_RULE(self):
+ expected = RULE_DERIVATION
+ fv = fake.MakeFieldValue(
+ field_id=self.field_def_1, str_value='something', derived=True)
+ output = self.converter._ComputeFieldValueDerivation(fv)
+ self.assertEqual(expected, output)
+
+ def test_ComputeFieldValueDerivation_EXPLICIT(self):
+ expected = EXPLICIT_DERIVATION
+ fv = fake.MakeFieldValue(
+ field_id=self.field_def_1, str_value='something', derived=False)
+ output = self.converter._ComputeFieldValueDerivation(fv)
+ self.assertEqual(expected, output)
+
+ def testConvertApprovalValues_Issue(self):
+ """We can convert issue approval_values."""
+ name = rnc.ConvertApprovalValueNames(
+ self.cnxn, self.issue_1.issue_id, self.services)[self.av_1.approval_id]
+ approval_def_name = rnc.ConvertApprovalDefNames(
+ self.cnxn, [self.approval_def_1_id], self.project_1.project_id,
+ self.services)[self.approval_def_1_id]
+ approvers = [rnc.ConvertUserName(self.user_2.user_id)]
+ status = issue_objects_pb2.ApprovalValue.ApprovalStatus.Value(
+ 'NOT_SET')
+ setter = rnc.ConvertUserName(self.user_1.user_id)
+ api_fvs = self.converter.ConvertFieldValues(
+ [self.fv_6], self.project_1.project_id, [self.phase_1])
+ # Check we can handle converting a None `set_on`.
+ self.av_1.set_on = None
+
+ output = self.converter.ConvertApprovalValues(
+ [self.av_1], [self.fv_1, self.fv_6], [self.phase_1],
+ issue_id=self.issue_1.issue_id)
+ expected = issue_objects_pb2.ApprovalValue(
+ name=name,
+ approval_def=approval_def_name,
+ approvers=approvers,
+ status=status,
+ setter=setter,
+ phase=self.phase_1.name,
+ field_values=api_fvs)
+ self.assertEqual([expected], output)
+
+ def testConvertApprovalValues_Templates(self):
+ """We can convert template approval_values."""
+ approval_def_name = rnc.ConvertApprovalDefNames(
+ self.cnxn, [self.approval_def_1_id], self.project_1.project_id,
+ self.services)[self.approval_def_1_id]
+ approvers = [rnc.ConvertUserName(self.user_2.user_id)]
+ status = issue_objects_pb2.ApprovalValue.ApprovalStatus.Value(
+ 'NOT_SET')
+ set_time = timestamp_pb2.Timestamp()
+ set_time.FromSeconds(self.PAST_TIME)
+ setter = rnc.ConvertUserName(self.user_1.user_id)
+ api_fvs = self.converter.ConvertFieldValues(
+ [self.fv_6], self.project_1.project_id, [self.phase_1])
+
+ output = self.converter.ConvertApprovalValues(
+ [self.av_1], [self.fv_1, self.fv_6], [self.phase_1],
+ project_id=self.project_1.project_id)
+ expected = issue_objects_pb2.ApprovalValue(
+ approval_def=approval_def_name,
+ approvers=approvers,
+ status=status,
+ set_time=set_time,
+ setter=setter,
+ phase=self.phase_1.name,
+ field_values=api_fvs)
+ self.assertEqual([expected], output)
+
+ def testConvertApprovalValues_NoPhase(self):
+ approval_def_name = rnc.ConvertApprovalDefNames(
+ self.cnxn, [self.approval_def_1_id], self.project_1.project_id,
+ self.services)[self.approval_def_1_id]
+ approvers = [rnc.ConvertUserName(self.user_2.user_id)]
+ status = issue_objects_pb2.ApprovalValue.ApprovalStatus.Value(
+ 'NOT_SET')
+ set_time = timestamp_pb2.Timestamp()
+ set_time.FromSeconds(self.PAST_TIME)
+ setter = rnc.ConvertUserName(self.user_1.user_id)
+ expected = issue_objects_pb2.ApprovalValue(
+ approval_def=approval_def_name,
+ approvers=approvers,
+ status=status,
+ set_time=set_time,
+ setter=setter)
+
+ output = self.converter.ConvertApprovalValues(
+ [self.av_1], [], [], project_id=self.project_1.project_id)
+ self.assertEqual([expected], output)
+
+ def testConvertApprovalValues_Empty(self):
+ output = self.converter.ConvertApprovalValues(
+ [], [], [], project_id=self.project_1.project_id)
+ self.assertEqual([], output)
+
+ def testConvertApprovalValues_IgnoresNullFieldDefs(self):
+ """It ignores approval values referencing a non-existent field"""
+ av = fake.MakeApprovalValue(self.dne_field_def_id)
+
+ output = self.converter.ConvertApprovalValues(
+ [av], [], [], issue_id=self.issue_1.issue_id)
+ self.assertEqual([], output)
+
+ def test_ComputeApprovalValueStatus_NOT_SET(self):
+ self.assertEqual(
+ self.converter._ComputeApprovalValueStatus(
+ tracker_pb2.ApprovalStatus.NOT_SET),
+ issue_objects_pb2.ApprovalValue.ApprovalStatus.Value(
+ 'NOT_SET'))
+
+ def test_ComputeApprovalValueStatus_NEEDS_REVIEW(self):
+ self.assertEqual(
+ self.converter._ComputeApprovalValueStatus(
+ tracker_pb2.ApprovalStatus.NEEDS_REVIEW),
+ issue_objects_pb2.ApprovalValue.ApprovalStatus.Value('NEEDS_REVIEW'))
+
+ def test_ComputeApprovalValueStatus_NA(self):
+ self.assertEqual(
+ self.converter._ComputeApprovalValueStatus(
+ tracker_pb2.ApprovalStatus.NA),
+ issue_objects_pb2.ApprovalValue.ApprovalStatus.Value('NA'))
+
+ def test_ComputeApprovalValueStatus_REVIEW_REQUESTED(self):
+ self.assertEqual(
+ self.converter._ComputeApprovalValueStatus(
+ tracker_pb2.ApprovalStatus.REVIEW_REQUESTED),
+ issue_objects_pb2.ApprovalValue.ApprovalStatus.Value(
+ 'REVIEW_REQUESTED'))
+
+ def test_ComputeApprovalValueStatus_REVIEW_STARTED(self):
+ self.assertEqual(
+ self.converter._ComputeApprovalValueStatus(
+ tracker_pb2.ApprovalStatus.REVIEW_STARTED),
+ issue_objects_pb2.ApprovalValue.ApprovalStatus.Value('REVIEW_STARTED'))
+
+ def test_ComputeApprovalValueStatus_NEED_INFO(self):
+ self.assertEqual(
+ self.converter._ComputeApprovalValueStatus(
+ tracker_pb2.ApprovalStatus.NEED_INFO),
+ issue_objects_pb2.ApprovalValue.ApprovalStatus.Value('NEED_INFO'))
+
+ def test_ComputeApprovalValueStatus_APPROVED(self):
+ self.assertEqual(
+ self.converter._ComputeApprovalValueStatus(
+ tracker_pb2.ApprovalStatus.APPROVED),
+ issue_objects_pb2.ApprovalValue.ApprovalStatus.Value('APPROVED'))
+
+ def test_ComputeApprovalValueStatus_NOT_APPROVED(self):
+ self.assertEqual(
+ self.converter._ComputeApprovalValueStatus(
+ tracker_pb2.ApprovalStatus.NOT_APPROVED),
+ issue_objects_pb2.ApprovalValue.ApprovalStatus.Value('NOT_APPROVED'))
+
+ def test_ComputeTemplatePrivacy_PUBLIC(self):
+ self.assertEqual(
+ self.converter._ComputeTemplatePrivacy(self.template_1),
+ project_objects_pb2.IssueTemplate.TemplatePrivacy.Value('PUBLIC'))
+
+ def test_ComputeTemplatePrivacy_MEMBERS_ONLY(self):
+ self.assertEqual(
+ self.converter._ComputeTemplatePrivacy(self.template_2),
+ project_objects_pb2.IssueTemplate.TemplatePrivacy.Value('MEMBERS_ONLY'))
+
+ def test_ComputeTemplateDefaultOwner_UNSPECIFIED(self):
+ self.assertEqual(
+ self.converter._ComputeTemplateDefaultOwner(self.template_1),
+ project_objects_pb2.IssueTemplate.DefaultOwner.Value(
+ 'DEFAULT_OWNER_UNSPECIFIED'))
+
+ def test_ComputeTemplateDefaultOwner_REPORTER(self):
+ self.assertEqual(
+ self.converter._ComputeTemplateDefaultOwner(self.template_2),
+ project_objects_pb2.IssueTemplate.DefaultOwner.Value(
+ 'PROJECT_MEMBER_REPORTER'))
+
+ def test_ComputePhases(self):
+ """It sorts by rank"""
+ phase1 = fake.MakePhase(123111, name='phase1name', rank=3)
+ phase2 = fake.MakePhase(123112, name='phase2name', rank=2)
+ phase3 = fake.MakePhase(123113, name='phase3name', rank=1)
+ expected = ['phase3name', 'phase2name', 'phase1name']
+ self.assertEqual(
+ self.converter._ComputePhases([phase1, phase2, phase3]), expected)
+
+ def test_ComputePhases_EMPTY(self):
+ self.assertEqual(self.converter._ComputePhases([]), [])
+
+ def test_FillIssueFromTemplate(self):
+ result = self.converter._FillIssueFromTemplate(
+ self.template_1, self.project_1.project_id)
+ self.assertFalse(result.name)
+ self.assertEqual(result.summary, self.template_1.summary)
+ self.assertEqual(
+ result.state, issue_objects_pb2.IssueContentState.Value('ACTIVE'))
+ self.assertEqual(result.status.status, 'New')
+ self.assertFalse(result.reporter)
+ self.assertEqual(result.owner.user, 'users/{}'.format(self.user_1.user_id))
+ self.assertEqual(len(result.cc_users), 0)
+ self.assertFalse(result.cc_users)
+ self.assertEqual(len(result.labels), 1)
+ self.assertEqual(result.labels[0].label, self.template_1.labels[0])
+ self.assertEqual(result.labels[0].derivation, EXPLICIT_DERIVATION)
+ self.assertEqual(len(result.components), 1)
+ self.assertEqual(
+ result.components[0].component, 'projects/{}/componentDefs/{}'.format(
+ self.project_1.project_name, self.template_1.component_ids[0]))
+ self.assertEqual(result.components[0].derivation, EXPLICIT_DERIVATION)
+ self.assertEqual(len(result.field_values), 2)
+ self.assertEqual(
+ result.field_values[0].field, 'projects/{}/fieldDefs/{}'.format(
+ self.project_1.project_name, self.field_def_1))
+ self.assertEqual(result.field_values[0].value, self.fv_1_value)
+ self.assertEqual(result.field_values[0].derivation, EXPLICIT_DERIVATION)
+ expected_name = rnc.ConvertFieldDefNames(
+ self.cnxn, [self.field_def_3], self.project_1.project_id,
+ self.services).get(self.field_def_3)
+ self.assertEqual(
+ result.field_values[1],
+ issue_objects_pb2.FieldValue(
+ field=expected_name,
+ value=self.template_1_label1_value,
+ derivation=EXPLICIT_DERIVATION))
+ self.assertFalse(result.blocked_on_issue_refs)
+ self.assertFalse(result.blocking_issue_refs)
+ self.assertFalse(result.attachment_count)
+ self.assertFalse(result.star_count)
+ self.assertEqual(len(result.phases), 1)
+ self.assertEqual(result.phases[0], self.phase_1.name)
+
+ def test_FillIssueFromTemplate_NoPhase(self):
+ result = self.converter._FillIssueFromTemplate(
+ self.template_3, self.project_1.project_id)
+ self.assertEqual(len(result.field_values), 1)
+ self.assertEqual(
+ result.field_values[0].field, 'projects/{}/fieldDefs/{}'.format(
+ self.project_1.project_name, self.field_def_1))
+ self.assertEqual(result.field_values[0].value, self.fv_1_value)
+ self.assertEqual(result.field_values[0].derivation, EXPLICIT_DERIVATION)
+ self.assertEqual(len(result.phases), 0)
+
+ def test_FillIssueFromTemplate_FilterApprovalFV(self):
+ template = self.services.template.TestAddIssueTemplateDef(
+ 11114,
+ self.project_1.project_id,
+ 'template3',
+ field_values=[self.fv_1, self.fv_6],
+ approval_values=[self.av_2],
+ )
+ result = self.converter._FillIssueFromTemplate(
+ template, self.project_1.project_id)
+ self.assertEqual(len(result.field_values), 1)
+ self.assertEqual(
+ result.field_values[0].field, 'projects/{}/fieldDefs/{}'.format(
+ self.project_1.project_name, self.field_def_1))
+ self.assertEqual(result.field_values[0].value, self.fv_1_value)
+ self.assertEqual(result.field_values[0].derivation, EXPLICIT_DERIVATION)
+
+ def testConvertIssueTemplates(self):
+ result = self.converter.ConvertIssueTemplates(
+ self.project_1.project_id, [self.template_1])
+ self.assertEqual(len(result), 1)
+ actual = result[0]
+ self.assertEqual(
+ actual.name, 'projects/{}/templates/{}'.format(
+ self.project_1.project_name, self.template_1.template_id))
+ self.assertEqual(actual.display_name, self.template_1.name)
+ self.assertEqual(actual.summary_must_be_edited, False)
+ self.assertEqual(
+ actual.template_privacy,
+ project_objects_pb2.IssueTemplate.TemplatePrivacy.Value('PUBLIC'))
+ self.assertEqual(
+ actual.default_owner,
+ project_objects_pb2.IssueTemplate.DefaultOwner.Value(
+ 'DEFAULT_OWNER_UNSPECIFIED'))
+ self.assertEqual(actual.component_required, False)
+ self.assertEqual(actual.admins, ['users/{}'.format(self.user_2.user_id)])
+ self.assertEqual(
+ actual.issue,
+ self.converter._FillIssueFromTemplate(
+ self.template_1, self.project_1.project_id))
+ self.assertListEqual(
+ [av for av in actual.approval_values],
+ self.converter.ConvertApprovalValues(
+ self.template_1.approval_values, self.template_1.field_values,
+ self.template_1.phases, project_id=self.project_1.project_id))
+
+ def testConvertIssueTemplates_IgnoresNonExistentTemplate(self):
+ result = self.converter.ConvertIssueTemplates(
+ self.project_1.project_id, [self.dne_template])
+ self.assertEqual(len(result), 0)
+
+ def testConvertLabels_OmitsFieldDefs(self):
+ """It omits field def labels"""
+ input_labels = ['pri-1', '{}-2'.format(self.field_def_3_name)]
+ result = self.converter.ConvertLabels(
+ input_labels, [], self.project_1.project_id)
+ self.assertEqual(len(result), 1)
+ expected = issue_objects_pb2.Issue.LabelValue(
+ label=input_labels[0], derivation=EXPLICIT_DERIVATION)
+ self.assertEqual(result[0], expected)
+
+ def testConvertLabels_DerivedLabels(self):
+ """It handles derived labels"""
+ input_labels = ['pri-1']
+ result = self.converter.ConvertLabels(
+ [], input_labels, self.project_1.project_id)
+ self.assertEqual(len(result), 1)
+ expected = issue_objects_pb2.Issue.LabelValue(
+ label=input_labels[0], derivation=RULE_DERIVATION)
+ self.assertEqual(result[0], expected)
+
+ def testConvertLabels(self):
+ """It includes both non-derived and derived labels"""
+ input_labels = ['pri-1', '{}-2'.format(self.field_def_3_name)]
+ input_der_labels = ['{}-3'.format(self.field_def_3_name), 'job-secret']
+ result = self.converter.ConvertLabels(
+ input_labels, input_der_labels, self.project_1.project_id)
+ self.assertEqual(len(result), 2)
+ expected_0 = issue_objects_pb2.Issue.LabelValue(
+ label=input_labels[0], derivation=EXPLICIT_DERIVATION)
+ self.assertEqual(result[0], expected_0)
+ expected_1 = issue_objects_pb2.Issue.LabelValue(
+ label=input_der_labels[1], derivation=RULE_DERIVATION)
+ self.assertEqual(result[1], expected_1)
+
+ def testConvertLabels_Empty(self):
+ result = self.converter.ConvertLabels([], [], self.project_1.project_id)
+ self.assertEqual(result, [])
+
+ def testConvertEnumFieldValues_OnlyFieldDefs(self):
+ """It only returns enum field values"""
+ expected_value = '2'
+ input_labels = [
+ 'pri-1', '{}-{}'.format(self.field_def_3_name, expected_value)
+ ]
+ result = self.converter.ConvertEnumFieldValues(
+ input_labels, [], self.project_1.project_id)
+ self.assertEqual(len(result), 1)
+ expected_name = rnc.ConvertFieldDefNames(
+ self.cnxn, [self.field_def_3], self.project_1.project_id,
+ self.services).get(self.field_def_3)
+ expected = issue_objects_pb2.FieldValue(
+ field=expected_name,
+ value=expected_value,
+ derivation=EXPLICIT_DERIVATION)
+ self.assertEqual(result[0], expected)
+
+ def testConvertEnumFieldValues_DerivedLabels(self):
+ """It handles derived enum field values"""
+ expected_value = '2'
+ input_der_labels = [
+ 'pri-1', '{}-{}'.format(self.field_def_3_name, expected_value)
+ ]
+ result = self.converter.ConvertEnumFieldValues(
+ [], input_der_labels, self.project_1.project_id)
+ self.assertEqual(len(result), 1)
+ expected_name = rnc.ConvertFieldDefNames(
+ self.cnxn, [self.field_def_3], self.project_1.project_id,
+ self.services).get(self.field_def_3)
+ expected = issue_objects_pb2.FieldValue(
+ field=expected_name, value=expected_value, derivation=RULE_DERIVATION)
+ self.assertEqual(result[0], expected)
+
+ def testConvertEnumFieldValues_Empty(self):
+ result = self.converter.ConvertEnumFieldValues(
+ [], [], self.project_1.project_id)
+ self.assertEqual(result, [])
+
+ def testConvertEnumFieldValues_ProjectSpecific(self):
+ """It only considers field defs from specified project"""
+ expected_value = '2'
+ input_labels = [
+ '{}-{}'.format(self.field_def_3_name, expected_value),
+ '{}-ipsum'.format(self.field_def_project2_name)
+ ]
+ result = self.converter.ConvertEnumFieldValues(
+ input_labels, [], self.project_1.project_id)
+ self.assertEqual(len(result), 1)
+ expected_name = rnc.ConvertFieldDefNames(
+ self.cnxn, [self.field_def_3], self.project_1.project_id,
+ self.services).get(self.field_def_3)
+ expected = issue_objects_pb2.FieldValue(
+ field=expected_name,
+ value=expected_value,
+ derivation=EXPLICIT_DERIVATION)
+ self.assertEqual(result[0], expected)
+
+ def testConvertEnumFieldValues(self):
+ """It handles derived enum field values"""
+ expected_value_0 = '2'
+ expected_value_1 = 'macOS'
+ input_labels = [
+ 'pri-1', '{}-{}'.format(self.field_def_3_name, expected_value_0),
+ '{}-ipsum'.format(self.field_def_project2_name)
+ ]
+ input_der_labels = [
+ '{}-{}'.format(self.field_def_4_name, expected_value_1), 'foo-bar'
+ ]
+ result = self.converter.ConvertEnumFieldValues(
+ input_labels, input_der_labels, self.project_1.project_id)
+ self.assertEqual(len(result), 2)
+ expected_0_name = rnc.ConvertFieldDefNames(
+ self.cnxn, [self.field_def_3], self.project_1.project_id,
+ self.services).get(self.field_def_3)
+ expected_0 = issue_objects_pb2.FieldValue(
+ field=expected_0_name,
+ value=expected_value_0,
+ derivation=EXPLICIT_DERIVATION)
+ self.assertEqual(result[0], expected_0)
+ expected_1_name = rnc.ConvertFieldDefNames(
+ self.cnxn, [self.field_def_4], self.project_1.project_id,
+ self.services).get(self.field_def_4)
+ expected_1 = issue_objects_pb2.FieldValue(
+ field=expected_1_name,
+ value=expected_value_1,
+ derivation=RULE_DERIVATION)
+ self.assertEqual(result[1], expected_1)
+
+ @mock.patch('project.project_helpers.GetThumbnailUrl')
+ def testConvertProject(self, mock_GetThumbnailUrl):
+ """We can convert a Project."""
+ mock_GetThumbnailUrl.return_value = 'xyz'
+ expected_api_project = project_objects_pb2.Project(
+ name='projects/{}'.format(self.project_1.project_name),
+ display_name=self.project_1.project_name,
+ summary=self.project_1.summary,
+ thumbnail_url='xyz')
+ self.assertEqual(
+ expected_api_project, self.converter.ConvertProject(self.project_1))
+
+ @mock.patch('project.project_helpers.GetThumbnailUrl')
+ def testConvertProjects(self, mock_GetThumbnailUrl):
+ """We can convert a Sequence of Projects."""
+ mock_GetThumbnailUrl.return_value = 'xyz'
+ expected_api_projects = [
+ project_objects_pb2.Project(
+ name='projects/{}'.format(self.project_1.project_name),
+ display_name=self.project_1.project_name,
+ summary=self.project_1.summary,
+ thumbnail_url='xyz'),
+ project_objects_pb2.Project(
+ name='projects/{}'.format(self.project_2.project_name),
+ display_name=self.project_2.project_name,
+ summary=self.project_2.summary,
+ thumbnail_url='xyz')
+ ]
+ self.assertEqual(
+ expected_api_projects,
+ self.converter.ConvertProjects([self.project_1, self.project_2]))
+
+ def testConvertProjectConfig(self):
+ """We can convert a project_config"""
+ project_config = self.services.config.GetProjectConfig(
+ self.cnxn, self.project_1.project_id)
+ expected_grid_config = project_objects_pb2.ProjectConfig.GridViewConfig(
+ default_x_attr=project_config.default_x_attr,
+ default_y_attr=project_config.default_y_attr)
+ template_names = rnc.ConvertTemplateNames(
+ self.cnxn, project_config.project_id, [
+ project_config.default_template_for_developers,
+ project_config.default_template_for_users
+ ], self.services)
+ expected_api_config = project_objects_pb2.ProjectConfig(
+ name=rnc.ConvertProjectConfigName(
+ self.cnxn, self.project_1.project_id, self.services),
+ exclusive_label_prefixes=project_config.exclusive_label_prefixes,
+ member_default_query=project_config.member_default_query,
+ default_sort=project_config.default_sort_spec,
+ default_columns=[
+ issue_objects_pb2.IssuesListColumn(column=col)
+ for col in project_config.default_col_spec.split()
+ ],
+ project_grid_config=expected_grid_config,
+ member_default_template=template_names.get(
+ project_config.default_template_for_developers),
+ non_members_default_template=template_names.get(
+ project_config.default_template_for_users),
+ revision_url_format=self.project_1.revision_url_format,
+ custom_issue_entry_url=project_config.custom_issue_entry_url)
+ self.converter.user_auth = authdata.AuthData.FromUser(
+ self.cnxn, self.user_1, self.services)
+ self.assertEqual(
+ expected_api_config,
+ self.converter.ConvertProjectConfig(project_config))
+
+ def testConvertProjectConfig_NonMembers(self):
+ """We can convert a project_config for non project members"""
+ self.converter.user_auth = authdata.AuthData.FromUser(
+ self.cnxn, self.user_2, self.services)
+ project_config = self.services.config.GetProjectConfig(
+ self.cnxn, self.project_1.project_id)
+ api_config = self.converter.ConvertProjectConfig(project_config)
+
+ expected_default_query = project_config.member_default_query
+ self.assertEqual(expected_default_query, api_config.member_default_query)
+
+ expected_member_default_template = rnc.ConvertTemplateNames(
+ self.cnxn, project_config.project_id,
+ [project_config.default_template_for_developers], self.services).get(
+ project_config.default_template_for_developers)
+ self.assertEqual(
+ expected_member_default_template, api_config.member_default_template)
+
+ def testCreateProjectMember(self):
+ """We can create a ProjectMember."""
+ expected_project_member = project_objects_pb2.ProjectMember(
+ name='projects/proj/members/111',
+ role=project_objects_pb2.ProjectMember.ProjectRole.Value('OWNER'))
+ self.assertEqual(
+ expected_project_member,
+ self.converter.CreateProjectMember(self.cnxn, 789, 111, 'OWNER'))
+
+ def test_ConvertDateAction(self):
+ """We can convert from protorpc to protoc FieldDef.DateAction"""
+ date_type_settings = project_objects_pb2.FieldDef.DateTypeSettings
+
+ input_type = tracker_pb2.DateAction.NO_ACTION
+ actual = self.converter._ConvertDateAction(input_type)
+ expected = date_type_settings.DateAction.Value('NO_ACTION')
+ self.assertEqual(expected, actual)
+
+ input_type = tracker_pb2.DateAction.PING_OWNER_ONLY
+ actual = self.converter._ConvertDateAction(input_type)
+ expected = date_type_settings.DateAction.Value('NOTIFY_OWNER')
+ self.assertEqual(expected, actual)
+
+ input_type = tracker_pb2.DateAction.PING_PARTICIPANTS
+ actual = self.converter._ConvertDateAction(input_type)
+ expected = date_type_settings.DateAction.Value('NOTIFY_PARTICIPANTS')
+ self.assertEqual(expected, actual)
+
+ def test_ConvertRoleRequirements(self):
+ """We can convert from protorpc to protoc FieldDef.RoleRequirements"""
+ user_type_settings = project_objects_pb2.FieldDef.UserTypeSettings
+
+ actual = self.converter._ConvertRoleRequirements(False)
+ expected = user_type_settings.RoleRequirements.Value('NO_ROLE_REQUIREMENT')
+ self.assertEqual(expected, actual)
+
+ actual = self.converter._ConvertRoleRequirements(True)
+ expected = user_type_settings.RoleRequirements.Value('PROJECT_MEMBER')
+ self.assertEqual(expected, actual)
+
+ def test_ConvertNotifyTriggers(self):
+ """We can convert from protorpc to protoc FieldDef.NotifyTriggers"""
+ user_type_settings = project_objects_pb2.FieldDef.UserTypeSettings
+
+ input_type = tracker_pb2.NotifyTriggers.NEVER
+ actual = self.converter._ConvertNotifyTriggers(input_type)
+ expected = user_type_settings.NotifyTriggers.Value('NEVER')
+ self.assertEqual(expected, actual)
+
+ input_type = tracker_pb2.NotifyTriggers.ANY_COMMENT
+ actual = self.converter._ConvertNotifyTriggers(input_type)
+ expected = user_type_settings.NotifyTriggers.Value('ANY_COMMENT')
+ self.assertEqual(expected, actual)
+
+ def test_ConvertFieldDefType(self):
+ """We can convert from protorpc FieldType to protoc FieldDef.Type"""
+ input_type = tracker_pb2.FieldTypes.ENUM_TYPE
+ actual = self.converter._ConvertFieldDefType(input_type)
+ expected = project_objects_pb2.FieldDef.Type.Value('ENUM')
+ self.assertEqual(expected, actual)
+
+ input_type = tracker_pb2.FieldTypes.INT_TYPE
+ actual = self.converter._ConvertFieldDefType(input_type)
+ expected = project_objects_pb2.FieldDef.Type.Value('INT')
+ self.assertEqual(expected, actual)
+
+ input_type = tracker_pb2.FieldTypes.STR_TYPE
+ actual = self.converter._ConvertFieldDefType(input_type)
+ expected = project_objects_pb2.FieldDef.Type.Value('STR')
+ self.assertEqual(expected, actual)
+
+ input_type = tracker_pb2.FieldTypes.USER_TYPE
+ actual = self.converter._ConvertFieldDefType(input_type)
+ expected = project_objects_pb2.FieldDef.Type.Value('USER')
+ self.assertEqual(expected, actual)
+
+ input_type = tracker_pb2.FieldTypes.DATE_TYPE
+ actual = self.converter._ConvertFieldDefType(input_type)
+ expected = project_objects_pb2.FieldDef.Type.Value('DATE')
+ self.assertEqual(expected, actual)
+
+ input_type = tracker_pb2.FieldTypes.URL_TYPE
+ actual = self.converter._ConvertFieldDefType(input_type)
+ expected = project_objects_pb2.FieldDef.Type.Value('URL')
+ self.assertEqual(expected, actual)
+
+ def test_ConvertFieldDefType_BOOL(self):
+ """We raise exception for unsupported input type BOOL"""
+ input_type = tracker_pb2.FieldTypes.BOOL_TYPE
+ with self.assertRaises(ValueError) as cm:
+ self.converter._ConvertFieldDefType(input_type)
+ self.assertEqual(
+ 'Unsupported tracker_pb2.FieldType enum. Boolean types '
+ 'are unsupported and approval types are found in ApprovalDefs',
+ str(cm.exception))
+
+ def test_ConvertFieldDefType_APPROVAL(self):
+ """We raise exception for input type APPROVAL"""
+ input_type = tracker_pb2.FieldTypes.APPROVAL_TYPE
+ with self.assertRaises(ValueError) as cm:
+ self.converter._ConvertFieldDefType(input_type)
+ self.assertEqual(
+ 'Unsupported tracker_pb2.FieldType enum. Boolean types '
+ 'are unsupported and approval types are found in ApprovalDefs',
+ str(cm.exception))
+
+ def testConvertFieldDefs(self):
+ """We can convert field defs"""
+ project_config = self.services.config.GetProjectConfig(
+ self.cnxn, self.project_1.project_id)
+ input_fds = project_config.field_defs
+ output = self.converter.ConvertFieldDefs(
+ input_fds, self.project_1.project_id)
+ fd1_rn = rnc.ConvertFieldDefNames(
+ self.cnxn, [self.field_def_1], self.project_1.project_id,
+ self.services).get(self.field_def_1)
+ self.assertEqual(fd1_rn, output[0].name)
+ self.assertEqual(self.field_def_1_name, output[0].display_name)
+ self.assertEqual('', output[0].docstring)
+ self.assertEqual(
+ project_objects_pb2.FieldDef.Type.Value('STR'), output[0].type)
+ self.assertEqual(
+ project_objects_pb2.FieldDef.Type.Value('INT'), output[1].type)
+ self.assertEqual('', output[1].applicable_issue_type)
+ fd1_admin_editor = [rnc.ConvertUserName(self.user_1.user_id)]
+ self.assertEqual(fd1_admin_editor, output[0].admins)
+ self.assertEqual(fd1_admin_editor, output[5].editors)
+
+ def testConvertFieldDefs_Traits(self):
+ """We can convert FieldDefs with traits"""
+ input_fd = self._GetFieldDefById(
+ self.project_1.project_id, self.field_def_1)
+ output = self.converter.ConvertFieldDefs(
+ [input_fd], self.project_1.project_id)
+ self.assertEqual(1, len(output))
+ expected_traits = [
+ project_objects_pb2.FieldDef.Traits.Value('REQUIRED'),
+ project_objects_pb2.FieldDef.Traits.Value('MULTIVALUED'),
+ project_objects_pb2.FieldDef.Traits.Value('PHASE')
+ ]
+ self.assertEqual(expected_traits, output[0].traits)
+
+ input_fd = self._GetFieldDefById(
+ self.project_1.project_id, self.field_def_2)
+ output = self.converter.ConvertFieldDefs(
+ [input_fd], self.project_1.project_id)
+ self.assertEqual(1, len(output))
+ expected_traits = [
+ project_objects_pb2.FieldDef.Traits.Value('DEFAULT_HIDDEN')
+ ]
+ self.assertEqual(expected_traits, output[0].traits)
+
+ def testConvertFieldDefs_ApprovalParent(self):
+ """We can convert FieldDef with approval parents"""
+ input_fd = self._GetFieldDefById(
+ self.project_1.project_id, self.field_def_6)
+ output = self.converter.ConvertFieldDefs(
+ [input_fd], self.project_1.project_id)
+ self.assertEqual(1, len(output))
+
+ approval_names_dict = rnc.ConvertApprovalDefNames(
+ self.cnxn, [self.approval_def_1_id], self.project_1.project_id,
+ self.services)
+ expected_approval_parent = approval_names_dict.get(input_fd.approval_id)
+ self.assertEqual(expected_approval_parent, output[0].approval_parent)
+
+ def testConvertFieldDefs_EnumTypeSettings(self):
+ """We can convert enum FieldDef and its settings"""
+ input_fd = self._GetFieldDefById(
+ self.project_1.project_id, self.field_def_5)
+ output = self.converter.ConvertFieldDefs(
+ [input_fd], self.project_1.project_id)
+ self.assertEqual(1, len(output))
+
+ expected_settings = project_objects_pb2.FieldDef.EnumTypeSettings(
+ choices=[
+ Choice(
+ value='submarine', docstring=self.labeldef_2.label_docstring),
+ Choice(value='basket', docstring=self.labeldef_3.label_docstring)
+ ])
+ self.assertEqual(expected_settings, output[0].enum_settings)
+
+ def testConvertFieldDefs_IntTypeSettings(self):
+ """We can convert int FieldDef and its settings"""
+ input_fd = self._GetFieldDefById(
+ self.project_1.project_id, self.field_def_2)
+ output = self.converter.ConvertFieldDefs(
+ [input_fd], self.project_1.project_id)
+ self.assertEqual(1, len(output))
+
+ expected_settings = project_objects_pb2.FieldDef.IntTypeSettings(
+ max_value=37)
+ self.assertEqual(expected_settings, output[0].int_settings)
+
+ def testConvertFieldDefs_StrTypeSettings(self):
+ """We can convert str FieldDef and its settings"""
+ input_fd = self._GetFieldDefById(
+ self.project_1.project_id, self.field_def_1)
+ output = self.converter.ConvertFieldDefs(
+ [input_fd], self.project_1.project_id)
+ self.assertEqual(1, len(output))
+
+ expected_settings = project_objects_pb2.FieldDef.StrTypeSettings(
+ regex='abc')
+ self.assertEqual(expected_settings, output[0].str_settings)
+
+ def testConvertFieldDefs_UserTypeSettings(self):
+ """We can convert user FieldDef and its settings"""
+ input_fd = self._GetFieldDefById(
+ self.project_1.project_id, self.field_def_8)
+ output = self.converter.ConvertFieldDefs(
+ [input_fd], self.project_1.project_id)
+ self.assertEqual(1, len(output))
+
+ user_settings = project_objects_pb2.FieldDef.UserTypeSettings
+ expected_settings = project_objects_pb2.FieldDef.UserTypeSettings(
+ role_requirements=user_settings.RoleRequirements.Value(
+ 'PROJECT_MEMBER'),
+ needs_perm='EDIT_PROJECT',
+ notify_triggers=user_settings.NotifyTriggers.Value('ANY_COMMENT'))
+ self.assertEqual(expected_settings, output[0].user_settings)
+
+ def testConvertFieldDefs_DateTypeSettings(self):
+ """We can convert user FieldDef and its settings"""
+ input_fd = self._GetFieldDefById(
+ self.project_1.project_id, self.field_def_9)
+ output = self.converter.ConvertFieldDefs(
+ [input_fd], self.project_1.project_id)
+ self.assertEqual(1, len(output))
+
+ date_settings = project_objects_pb2.FieldDef.DateTypeSettings
+ expected_settings = project_objects_pb2.FieldDef.DateTypeSettings(
+ date_action=date_settings.DateAction.Value('NOTIFY_OWNER'))
+ self.assertEqual(expected_settings, output[0].date_settings)
+
+ def testConvertFieldDefs_SkipsApprovals(self):
+ """We skip over approval defs"""
+ project_config = self.services.config.GetProjectConfig(
+ self.cnxn, self.project_1.project_id)
+ input_fds = project_config.field_defs
+ # project_1 is set up to have 10 non-approval fields and 2 approval fields.
+ self.assertEqual(12, len(input_fds))
+ output = self.converter.ConvertFieldDefs(
+ input_fds, self.project_1.project_id)
+ # assert we skip approval fields
+ self.assertEqual(10, len(output))
+
+ def testConvertFieldDefs_NonexistentID(self):
+ """We skip over any field defs whose ID does not exist."""
+ input_fd = tracker_pb2.FieldDef(
+ field_id=self.dne_field_def_id,
+ project_id=self.project_1.project_id,
+ field_name='foobar',
+ field_type=tracker_pb2.FieldTypes('STR_TYPE'))
+
+ output = self.converter.ConvertFieldDefs(
+ [input_fd], self.project_1.project_id)
+ self.assertEqual(0, len(output))
+
+ def testConvertFieldDefs_Empty(self):
+ """We can handle empty list input"""
+ self.assertEqual(
+ [], self.converter.ConvertFieldDefs([], self.project_1.project_id))
+
+ def test_ComputeFieldDefTraits(self):
+ """We can get Sequence of Traits for a FieldDef"""
+ input_fd = self._GetFieldDefById(
+ self.project_1.project_id, self.field_def_1)
+ actual = self.converter._ComputeFieldDefTraits(input_fd)
+ expected = [
+ project_objects_pb2.FieldDef.Traits.Value('REQUIRED'),
+ project_objects_pb2.FieldDef.Traits.Value('MULTIVALUED'),
+ project_objects_pb2.FieldDef.Traits.Value('PHASE')
+ ]
+ self.assertEqual(expected, actual)
+
+ input_fd = self._GetFieldDefById(
+ self.project_1.project_id, self.field_def_2)
+ actual = self.converter._ComputeFieldDefTraits(input_fd)
+ expected = [project_objects_pb2.FieldDef.Traits.Value('DEFAULT_HIDDEN')]
+ self.assertEqual(expected, actual)
+
+ input_fd = self._GetFieldDefById(
+ self.project_1.project_id, self.field_def_7)
+ actual = self.converter._ComputeFieldDefTraits(input_fd)
+ expected = [project_objects_pb2.FieldDef.Traits.Value('RESTRICTED')]
+ self.assertEqual(expected, actual)
+
+ def test_ComputeFieldDefTraits_Empty(self):
+ """We return an empty Sequence of Traits for plain FieldDef"""
+ input_fd = self._GetFieldDefById(
+ self.project_1.project_id, self.field_def_3)
+ actual = self.converter._ComputeFieldDefTraits(input_fd)
+ self.assertEqual([], actual)
+
+ def test_GetEnumFieldChoices(self):
+ """We can get all choices for an enum field"""
+ input_fd = self._GetFieldDefById(
+ self.project_1.project_id, self.field_def_5)
+ actual = self.converter._GetEnumFieldChoices(input_fd)
+ expected = [
+ Choice(
+ value=self.labeldef_2.label.split('-')[1],
+ docstring=self.labeldef_2.label_docstring),
+ Choice(
+ value=self.labeldef_3.label.split('-')[1],
+ docstring=self.labeldef_3.label_docstring),
+ ]
+ self.assertEqual(expected, actual)
+
+ def test_GetEnumFieldChoices_NotEnumField(self):
+ """We raise exception for non-enum-field"""
+ input_fd = self._GetFieldDefById(
+ self.project_1.project_id, self.field_def_1)
+ with self.assertRaises(ValueError) as cm:
+ self.converter._GetEnumFieldChoices(input_fd)
+ self.assertEqual(
+ 'Cannot get value from label for non-enum-type field', str(
+ cm.exception))
+
+ def testConvertApprovalDefs(self):
+ """We can convert ApprovalDefs"""
+ input_ad = self._GetApprovalDefById(
+ self.project_1.project_id, self.approval_def_1_id)
+ actual = self.converter.ConvertApprovalDefs(
+ [input_ad], self.project_1.project_id)
+
+ resource_names_dict = rnc.ConvertApprovalDefNames(
+ self.cnxn, [self.approval_def_1_id], self.project_1.project_id,
+ self.services)
+ expected_name = resource_names_dict.get(self.approval_def_1_id)
+ self.assertEqual(actual[0].name, expected_name)
+ self.assertEqual(actual[0].display_name, self.approval_def_1_name)
+ matching_fd = self._GetFieldDefById(
+ self.project_1.project_id, self.approval_def_1_id)
+ expected_docstring = matching_fd.docstring
+ self.assertEqual(actual[0].docstring, expected_docstring)
+ self.assertEqual(actual[0].survey, self.approval_def_1.survey)
+ expected_approvers = [rnc.ConvertUserName(self.user_2.user_id)]
+ self.assertEqual(actual[0].approvers, expected_approvers)
+ expected_admins = [rnc.ConvertUserName(self.user_1.user_id)]
+ self.assertEqual(actual[0].admins, expected_admins)
+
+ def testConvertApprovalDefs_Empty(self):
+ """We can handle empty case"""
+ actual = self.converter.ConvertApprovalDefs([], self.project_1.project_id)
+ self.assertEqual(actual, [])
+
+ def testConvertApprovalDefs_SkipsNonApprovalDefs(self):
+ """We skip if no matching field def exists"""
+ input_ad = tracker_pb2.ApprovalDef(
+ approval_id=self.dne_field_def_id,
+ approver_ids=[self.user_2.user_id],
+ survey='anything goes')
+ actual = self.converter.ConvertApprovalDefs(
+ [input_ad], self.project_1.project_id)
+ self.assertEqual(actual, [])
+
+ def testConvertLabelDefs(self):
+ """We can convert LabelDefs"""
+ actual = self.converter.ConvertLabelDefs(
+ [self.labeldef_1, self.labeldef_5], self.project_1.project_id)
+ resource_names_dict = rnc.ConvertLabelDefNames(
+ self.cnxn, [self.labeldef_1.label, self.labeldef_5.label],
+ self.project_1.project_id, self.services)
+ expected_0_name = resource_names_dict.get(self.labeldef_1.label)
+ expected_0 = project_objects_pb2.LabelDef(
+ name=expected_0_name,
+ value=self.labeldef_1.label,
+ docstring=self.labeldef_1.label_docstring,
+ state=project_objects_pb2.LabelDef.LabelDefState.Value('ACTIVE'))
+ self.assertEqual(expected_0, actual[0])
+ expected_1_name = resource_names_dict.get(self.labeldef_5.label)
+ expected_1 = project_objects_pb2.LabelDef(
+ name=expected_1_name,
+ value=self.labeldef_5.label,
+ docstring=self.labeldef_5.label_docstring,
+ state=project_objects_pb2.LabelDef.LabelDefState.Value('DEPRECATED'))
+ self.assertEqual(expected_1, actual[1])
+
+ def testConvertLabelDefs_Empty(self):
+ """We can handle empty input case"""
+ actual = self.converter.ConvertLabelDefs([], self.project_1.project_id)
+ self.assertEqual([], actual)
+
+ def testConvertStatusDefs(self):
+ """We can convert StatusDefs"""
+ actual = self.converter.ConvertStatusDefs(
+ self.predefined_statuses, self.project_1.project_id)
+ self.assertEqual(len(actual), 4)
+
+ input_names = [sd.status for sd in self.predefined_statuses]
+ names = rnc.ConvertStatusDefNames(
+ self.cnxn, input_names, self.project_1.project_id, self.services)
+ self.assertEqual(names[self.status_1.status], actual[0].name)
+ self.assertEqual(names[self.status_2.status], actual[1].name)
+ self.assertEqual(names[self.status_3.status], actual[2].name)
+ self.assertEqual(names[self.status_4.status], actual[3].name)
+
+ self.assertEqual(self.status_1.status, actual[0].value)
+ self.assertEqual(
+ project_objects_pb2.StatusDef.StatusDefType.Value('OPEN'),
+ actual[0].type)
+ self.assertEqual(0, actual[0].rank)
+ self.assertEqual(self.status_1.status_docstring, actual[0].docstring)
+ self.assertEqual(
+ project_objects_pb2.StatusDef.StatusDefState.Value('ACTIVE'),
+ actual[0].state)
+
+ def testConvertStatusDefs_Empty(self):
+ """Can handle empty input case"""
+ actual = self.converter.ConvertStatusDefs([], self.project_1.project_id)
+ self.assertEqual([], actual)
+
+ def testConvertStatusDefs_Rank(self):
+ """Rank is indepdendent of input order"""
+ input_sds = [self.status_2, self.status_4, self.status_3, self.status_1]
+ actual = self.converter.ConvertStatusDefs(
+ input_sds, self.project_1.project_id)
+ self.assertEqual(1, actual[0].rank)
+ self.assertEqual(3, actual[1].rank)
+
+ def testConvertStatusDefs_type_MERGED(self):
+ """Includes mergeable status when parsed from project config"""
+ actual = self.converter.ConvertStatusDefs(
+ [self.status_2], self.project_1.project_id)
+ self.assertEqual(
+ project_objects_pb2.StatusDef.StatusDefType.Value('MERGED'),
+ actual[0].type)
+
+ def testConvertStatusDefs_state_DEPRECATED(self):
+ """Includes deprecated status"""
+ actual = self.converter.ConvertStatusDefs(
+ [self.status_4], self.project_1.project_id)
+ self.assertEqual(
+ project_objects_pb2.StatusDef.StatusDefState.Value('DEPRECATED'),
+ actual[0].state)
+
+ def testConvertComponentDef(self):
+ now = 123
+ project = self.services.project.TestAddProject('comp-test', project_id=987)
+ config = fake.MakeTestConfig(project.project_id, [], [])
+ component_def = fake.MakeTestComponentDef(
+ project.project_id, 1, path='Chickens>Dickens')
+ component_def.created = now
+ config.component_defs = [component_def]
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ actual = self.converter.ConvertComponentDef(component_def)
+ expected = project_objects_pb2.ComponentDef(
+ name='projects/comp-test/componentDefs/1',
+ value='Chickens>Dickens',
+ state=project_objects_pb2.ComponentDef.ComponentDefState.Value(
+ 'ACTIVE'),
+ create_time=timestamp_pb2.Timestamp(seconds=now),
+ modify_time=timestamp_pb2.Timestamp())
+ self.assertEqual(actual, expected)
+
+ def testConvertComponentDefs(self):
+ """We can convert ComponentDefs"""
+ project_config = self.services.config.GetProjectConfig(
+ self.cnxn, self.project_1.project_id)
+ self.assertEqual(len(project_config.component_defs), 2)
+
+ actual = self.converter.ConvertComponentDefs(
+ project_config.component_defs, self.project_1.project_id)
+ self.assertEqual(2, len(actual))
+
+ resource_names_dict = rnc.ConvertComponentDefNames(
+ self.cnxn, [self.component_def_1_id, self.component_def_2_id],
+ self.project_1.project_id, self.services)
+ self.assertEqual(
+ resource_names_dict.get(self.component_def_1_id), actual[0].name)
+ self.assertEqual(
+ resource_names_dict.get(self.component_def_2_id), actual[1].name)
+ self.assertEqual(self.component_def_1_path, actual[0].value)
+ self.assertEqual(self.component_def_2_path, actual[1].value)
+ self.assertEqual('cd1_docstring', actual[0].docstring)
+ self.assertEqual(
+ project_objects_pb2.ComponentDef.ComponentDefState.Value('ACTIVE'),
+ actual[0].state)
+ self.assertEqual(
+ project_objects_pb2.ComponentDef.ComponentDefState.Value('DEPRECATED'),
+ actual[1].state)
+ # component_def 1 and 2 have the same admins, ccs, creator, and create_time
+ expected_admins = [rnc.ConvertUserName(self.user_1.user_id)]
+ self.assertEqual(expected_admins, actual[0].admins)
+ expected_ccs = [rnc.ConvertUserName(self.user_2.user_id)]
+ self.assertEqual(expected_ccs, actual[0].ccs)
+ expected_creator = rnc.ConvertUserName(self.user_1.user_id)
+ self.assertEqual(expected_creator, actual[0].creator)
+ expected_create_time = timestamp_pb2.Timestamp(seconds=self.PAST_TIME)
+ self.assertEqual(expected_create_time, actual[0].create_time)
+
+ expected_labels = [ld.label for ld in self.predefined_labels]
+ self.assertEqual(expected_labels, actual[0].labels)
+ self.assertEqual([], actual[1].labels)
+
+ def testConvertComponentDefs_Empty(self):
+ """Can handle empty input case"""
+ actual = self.converter.ConvertComponentDefs([], self.project_1.project_id)
+ self.assertEqual([], actual)
+
+ def testConvertProjectSavedQueries(self):
+ """We can convert ProjectSavedQueries"""
+ input_psqs = [self.psq_2]
+ actual = self.converter.ConvertProjectSavedQueries(
+ input_psqs, self.project_1.project_id)
+ self.assertEqual(1, len(actual))
+
+ resource_names_dict = rnc.ConvertProjectSavedQueryNames(
+ self.cnxn, [self.psq_2.query_id], self.project_1.project_id,
+ self.services)
+ self.assertEqual(
+ resource_names_dict.get(self.psq_2.query_id), actual[0].name)
+ self.assertEqual(self.psq_2.name, actual[0].display_name)
+ self.assertEqual(self.psq_2.query, actual[0].query)
+
+ def testConvertProjectSavedQueries_ExpandsBasedOn(self):
+ """We expand query to include base_query_id"""
+ actual = self.converter.ConvertProjectSavedQueries(
+ [self.psq_1], self.project_1.project_id)
+ expected_query = '{} {}'.format(
+ tbo.GetBuiltInQuery(self.psq_1.base_query_id), self.psq_1.query)
+ self.assertEqual(expected_query, actual[0].query)
+
+ def testConvertProjectSavedQueries_NotInProject(self):
+ """We skip over saved queries that don't belong to this project"""
+ psq_not_registered = tracker_pb2.SavedQuery(
+ query_id=4, name='psq no registered name', query='no registered')
+ actual = self.converter.ConvertProjectSavedQueries(
+ [psq_not_registered], self.project_1.project_id)
+ self.assertEqual([], actual)
+
+ def testConvertProjectSavedQueries_Empty(self):
+ """We can handle empty inputs"""
+ actual = self.converter.ConvertProjectSavedQueries(
+ [], self.project_1.project_id)
+ self.assertEqual([], actual)
diff --git a/api/v3/test/frontend_servicer_test.py b/api/v3/test/frontend_servicer_test.py
new file mode 100644
index 0000000..e58f1ab
--- /dev/null
+++ b/api/v3/test/frontend_servicer_test.py
@@ -0,0 +1,237 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+"""Tests for the hotlists servicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from google.protobuf import timestamp_pb2
+from mock import patch
+
+from api import resource_name_converters as rnc
+from api.v3 import converters
+from api.v3 import frontend_servicer
+from api.v3.api_proto import frontend_pb2
+from api.v3.api_proto import project_objects_pb2
+from framework import exceptions
+from framework import monorailcontext
+from proto import tracker_pb2
+from services import service_manager
+from testing import fake
+from tracker import tracker_constants
+
+
+class FrontendServicerTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ features=fake.FeaturesService(),
+ issue=fake.IssueService(),
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ user=fake.UserService(),
+ template=fake.TemplateService(),
+ usergroup=fake.UserGroupService())
+ self.frontend_svcr = frontend_servicer.FrontendServicer(
+ self.services, make_rate_limiter=False)
+
+ self.user_1 = self.services.user.TestAddUser('user_111@example.com', 111)
+ self.user_1_resource_name = 'users/111'
+ self.project_1_resource_name = 'projects/proj'
+ self.project_1 = self.services.project.TestAddProject(
+ 'proj', project_id=789)
+ self.template_0 = self.services.template.TestAddIssueTemplateDef(
+ 11110, self.project_1.project_id, 'template0')
+ self.PAST_TIME = 12345
+ self.component_def_1_path = 'foo'
+ self.component_def_1_id = self.services.config.CreateComponentDef(
+ self.cnxn, self.project_1.project_id, self.component_def_1_path,
+ 'cd1_docstring', False, [self.user_1.user_id], [], self.PAST_TIME,
+ self.user_1.user_id, [])
+ self.field_def_1_name = 'test_field_1'
+ self.field_def_1 = self._CreateFieldDef(
+ self.project_1.project_id,
+ self.field_def_1_name,
+ 'STR_TYPE',
+ admin_ids=[self.user_1.user_id],
+ is_required=True,
+ is_multivalued=True,
+ is_phase_field=True,
+ regex='abc')
+ self.approval_def_1_name = 'approval_field_1'
+ self.approval_def_1_id = self._CreateFieldDef(
+ self.project_1.project_id,
+ self.approval_def_1_name,
+ 'APPROVAL_TYPE',
+ docstring='ad_1_docstring',
+ admin_ids=[self.user_1.user_id])
+ self.approval_def_1 = tracker_pb2.ApprovalDef(
+ approval_id=self.approval_def_1_id,
+ approver_ids=[self.user_1.user_id],
+ survey='approval_def_1 survey')
+ self.services.config.UpdateConfig(
+ self.cnxn,
+ self.project_1,
+ # UpdateConfig accepts tuples rather than protorpc *Defs
+ approval_defs=[
+ (ad.approval_id, ad.approver_ids, ad.survey)
+ for ad in [self.approval_def_1]
+ ])
+
+ def _CreateFieldDef(
+ self,
+ project_id,
+ field_name,
+ field_type_str,
+ docstring=None,
+ min_value=None,
+ max_value=None,
+ regex=None,
+ needs_member=None,
+ needs_perm=None,
+ grants_perm=None,
+ notify_on=None,
+ date_action_str=None,
+ admin_ids=None,
+ editor_ids=None,
+ is_required=False,
+ is_niche=False,
+ is_multivalued=False,
+ is_phase_field=False,
+ approval_id=None,
+ is_restricted_field=False):
+ """Calls CreateFieldDef with reasonable defaults, returns the ID."""
+ if admin_ids is None:
+ admin_ids = []
+ if editor_ids is None:
+ editor_ids = []
+ return self.services.config.CreateFieldDef(
+ self.cnxn,
+ project_id,
+ field_name,
+ field_type_str,
+ None,
+ None,
+ is_required,
+ is_niche,
+ is_multivalued,
+ min_value,
+ max_value,
+ regex,
+ needs_member,
+ needs_perm,
+ grants_perm,
+ notify_on,
+ date_action_str,
+ docstring,
+ admin_ids,
+ editor_ids,
+ is_phase_field=is_phase_field,
+ approval_id=approval_id,
+ is_restricted_field=is_restricted_field)
+
+ def CallWrapped(self, wrapped_handler, mc, *args, **kwargs):
+ self.frontend_svcr.converter = converters.Converter(mc, self.services)
+ return wrapped_handler.wrapped(self.frontend_svcr, mc, *args, **kwargs)
+
+ @patch('project.project_helpers.GetThumbnailUrl')
+ def testGatherProjectEnvironment(self, mock_GetThumbnailUrl):
+ """We can fetch all project related parameters for web frontend."""
+ mock_GetThumbnailUrl.return_value = 'xyz'
+
+ request = frontend_pb2.GatherProjectEnvironmentRequest(
+ parent=self.project_1_resource_name)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ response = self.CallWrapped(
+ self.frontend_svcr.GatherProjectEnvironment, mc, request)
+ project_config = self.services.config.GetProjectConfig(
+ self.cnxn, self.project_1.project_id)
+
+ self.assertEqual(
+ response.project,
+ self.frontend_svcr.converter.ConvertProject(self.project_1))
+ self.assertEqual(
+ response.project_config,
+ self.frontend_svcr.converter.ConvertProjectConfig(project_config))
+
+ self.assertEqual(
+ len(response.statuses),
+ len(tracker_constants.DEFAULT_WELL_KNOWN_STATUSES))
+ self.assertEqual(
+ response.statuses[0],
+ project_objects_pb2.StatusDef(
+ name='projects/{project_name}/statusDefs/{status}'.format(
+ project_name=self.project_1.project_name,
+ status=tracker_constants.DEFAULT_WELL_KNOWN_STATUSES[0][0]),
+ value=tracker_constants.DEFAULT_WELL_KNOWN_STATUSES[0][0],
+ type=project_objects_pb2.StatusDef.StatusDefType.Value('OPEN'),
+ rank=0,
+ docstring=tracker_constants.DEFAULT_WELL_KNOWN_STATUSES[0][1],
+ state=project_objects_pb2.StatusDef.StatusDefState.Value('ACTIVE'),
+ ))
+
+ self.assertEqual(
+ len(response.well_known_labels),
+ len(tracker_constants.DEFAULT_WELL_KNOWN_LABELS))
+ self.assertEqual(
+ response.well_known_labels[0],
+ project_objects_pb2.LabelDef(
+ name='projects/{project_name}/labelDefs/{label}'.format(
+ project_name=self.project_1.project_name,
+ label=tracker_constants.DEFAULT_WELL_KNOWN_LABELS[0][0]),
+ value=tracker_constants.DEFAULT_WELL_KNOWN_LABELS[0][0],
+ docstring=tracker_constants.DEFAULT_WELL_KNOWN_LABELS[0][1],
+ state=project_objects_pb2.LabelDef.LabelDefState.Value('ACTIVE'),
+ ))
+
+ expected = self.frontend_svcr.converter.ConvertComponentDefs(
+ project_config.component_defs, self.project_1.project_id)
+ # Have to use list comprehension to break response sub field into list
+ self.assertEqual([api_cd for api_cd in response.components], expected)
+
+ expected = self.frontend_svcr.converter.ConvertFieldDefs(
+ project_config.field_defs, self.project_1.project_id)
+ self.assertEqual([api_fd for api_fd in response.fields], expected)
+
+ expected = self.frontend_svcr.converter.ConvertApprovalDefs(
+ project_config.approval_defs, self.project_1.project_id)
+ self.assertEqual([api_ad for api_ad in response.approval_fields], expected)
+
+ def testGatherProjectMembershipsForUser(self):
+ """We can list a user's project memberships."""
+ self.services.project.TestAddProject(
+ 'owner_proj', project_id=777, owner_ids=[111])
+ self.services.project.TestAddProject(
+ 'committer_proj', project_id=888, committer_ids=[111])
+ contributor_proj = self.services.project.TestAddProject(
+ 'contributor_proj', project_id=999)
+ contributor_proj.contributor_ids = [111]
+
+ request = frontend_pb2.GatherProjectMembershipsForUserRequest(
+ user=self.user_1_resource_name)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ response = self.CallWrapped(
+ self.frontend_svcr.GatherProjectMembershipsForUser, mc, request)
+
+ owner_membership = project_objects_pb2.ProjectMember(
+ name='projects/{}/members/{}'.format('owner_proj', '111'),
+ role=project_objects_pb2.ProjectMember.ProjectRole.Value('OWNER'))
+ committer_membership = project_objects_pb2.ProjectMember(
+ name='projects/{}/members/{}'.format('committer_proj', '111'),
+ role=project_objects_pb2.ProjectMember.ProjectRole.Value('COMMITTER'))
+ contributor_membership = project_objects_pb2.ProjectMember(
+ name='projects/{}/members/{}'.format('contributor_proj', '111'),
+ role=project_objects_pb2.ProjectMember.ProjectRole.Value('CONTRIBUTOR'))
+ self.assertEqual(
+ response,
+ frontend_pb2.GatherProjectMembershipsForUserResponse(
+ project_memberships=[
+ owner_membership, committer_membership, contributor_membership
+ ]))
diff --git a/api/v3/test/hotlists_servicer_test.py b/api/v3/test/hotlists_servicer_test.py
new file mode 100644
index 0000000..e9808b5
--- /dev/null
+++ b/api/v3/test/hotlists_servicer_test.py
@@ -0,0 +1,397 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for the hotlists servicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from google.protobuf import empty_pb2
+from google.protobuf import field_mask_pb2
+
+from api import resource_name_converters as rnc
+from api.v3 import hotlists_servicer
+from api.v3 import converters
+from api.v3.api_proto import hotlists_pb2
+from api.v3.api_proto import feature_objects_pb2
+from api.v3.api_proto import issue_objects_pb2
+from api.v3.api_proto import user_objects_pb2
+from framework import exceptions
+from framework import monorailcontext
+from framework import permissions
+from features import features_constants
+from testing import fake
+from services import features_svc
+from services import service_manager
+
+
+class HotlistsServicerTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ features=fake.FeaturesService(),
+ issue=fake.IssueService(),
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService())
+ self.hotlists_svcr = hotlists_servicer.HotlistsServicer(
+ self.services, make_rate_limiter=False)
+ self.converter = None
+ self.PAST_TIME = 12345
+ self.user_1 = self.services.user.TestAddUser('user_111@example.com', 111)
+ self.user_2 = self.services.user.TestAddUser('user_222@example.com', 222)
+ self.user_3 = self.services.user.TestAddUser('user_333@example.com', 333)
+
+ user_ids = [self.user_1.user_id, self.user_2.user_id, self.user_3.user_id]
+ self.user_ids_to_name = rnc.ConvertUserNames(user_ids)
+
+ self.project_1 = self.services.project.TestAddProject(
+ 'proj', project_id=789)
+
+ self.issue_1 = fake.MakeTestIssue(
+ self.project_1.project_id, 1, 'sum', 'New', 111,
+ project_name=self.project_1.project_name)
+ self.issue_2 = fake.MakeTestIssue(
+ self.project_1.project_id, 2, 'sum', 'New', 111,
+ project_name=self.project_1.project_name)
+ self.issue_3 = fake.MakeTestIssue(
+ self.project_1.project_id, 3, 'sum', 'New', 111,
+ project_name=self.project_1.project_name)
+ self.issue_4 = fake.MakeTestIssue(
+ self.project_1.project_id, 4, 'sum', 'New', 111,
+ project_name=self.project_1.project_name)
+ self.issue_5 = fake.MakeTestIssue(
+ self.project_1.project_id, 5, 'sum', 'New', 111,
+ project_name=self.project_1.project_name)
+ self.issue_6 = fake.MakeTestIssue(
+ self.project_1.project_id, 6, 'sum', 'New', 111,
+ project_name=self.project_1.project_name)
+ self.services.issue.TestAddIssue(self.issue_1)
+ self.services.issue.TestAddIssue(self.issue_2)
+ self.services.issue.TestAddIssue(self.issue_3)
+ self.services.issue.TestAddIssue(self.issue_4)
+ self.services.issue.TestAddIssue(self.issue_5)
+ self.services.issue.TestAddIssue(self.issue_6)
+ issue_ids = [
+ self.issue_1.issue_id, self.issue_2.issue_id, self.issue_3.issue_id,
+ self.issue_4.issue_id, self.issue_5.issue_id, self.issue_6.issue_id
+ ]
+ self.issue_ids_to_name = rnc.ConvertIssueNames(
+ self.cnxn, issue_ids, self.services)
+
+ hotlist_items = [
+ (
+ self.issue_4.issue_id, 31, self.user_3.user_id, self.PAST_TIME,
+ 'note5'),
+ (
+ self.issue_3.issue_id, 21, self.user_1.user_id, self.PAST_TIME,
+ 'note1'),
+ (
+ self.issue_2.issue_id, 11, self.user_2.user_id, self.PAST_TIME,
+ 'note2'),
+ (
+ self.issue_1.issue_id, 1, self.user_1.user_id, self.PAST_TIME,
+ 'note4')
+ ]
+ self.hotlist_1 = self.services.features.TestAddHotlist(
+ 'HotlistName',
+ summary='summary',
+ description='description',
+ owner_ids=[self.user_1.user_id],
+ editor_ids=[self.user_2.user_id],
+ hotlist_item_fields=hotlist_items,
+ default_col_spec='',
+ is_private=True)
+ self.hotlist_resource_name = rnc.ConvertHotlistName(
+ self.hotlist_1.hotlist_id)
+
+ def CallWrapped(self, wrapped_handler, mc, *args, **kwargs):
+ self.converter = converters.Converter(mc, self.services)
+ self.hotlists_svcr.converter = self.converter
+ return wrapped_handler.wrapped(self.hotlists_svcr, mc, *args, **kwargs)
+
+ # TODO(crbug/monorail/7104): Add page_token tests when implemented.
+ def testListHotlistItems(self):
+ """We can list a Hotlist's HotlistItems."""
+ request = hotlists_pb2.ListHotlistItemsRequest(
+ parent=self.hotlist_resource_name, page_size=2, order_by='note,stars')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+ response = self.CallWrapped(
+ self.hotlists_svcr.ListHotlistItems, mc, request)
+ expected_items = self.converter.ConvertHotlistItems(
+ self.hotlist_1.hotlist_id,
+ [self.hotlist_1.items[1], self.hotlist_1.items[2]])
+ self.assertEqual(
+ response, hotlists_pb2.ListHotlistItemsResponse(items=expected_items))
+
+ def testListHotlistItems_Empty(self):
+ """We can return a response if the Hotlist has no items"""
+ empty_hotlist = self.services.features.TestAddHotlist(
+ 'Empty',
+ owner_ids=[self.user_1.user_id],
+ editor_ids=[self.user_2.user_id],
+ hotlist_item_fields=[])
+ hotlist_resource_name = rnc.ConvertHotlistName(empty_hotlist.hotlist_id)
+ request = hotlists_pb2.ListHotlistItemsRequest(parent=hotlist_resource_name)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+ response = self.CallWrapped(
+ self.hotlists_svcr.ListHotlistItems, mc, request)
+ self.assertEqual(response, hotlists_pb2.ListHotlistItemsResponse(items=[]))
+
+ def testListHotlistItems_InvalidPageSize(self):
+ """We raise an exception if `page_size` is negative."""
+ request = hotlists_pb2.ListHotlistItemsRequest(
+ parent=self.hotlist_resource_name, page_size=-1)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.hotlists_svcr.ListHotlistItems, mc, request)
+
+ def testListHotlistItems_DefaultPageSize(self):
+ """We use our default page size when no `page_size` is given."""
+ request = hotlists_pb2.ListHotlistItemsRequest(
+ parent=self.hotlist_resource_name)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+ response = self.CallWrapped(
+ self.hotlists_svcr.ListHotlistItems, mc, request)
+ self.assertEqual(
+ len(response.items),
+ min(
+ features_constants.DEFAULT_RESULTS_PER_PAGE,
+ len(self.hotlist_1.items)))
+
+ def testRerankHotlistItems(self):
+ """We can rerank a Hotlist."""
+ item_names_dict = rnc.ConvertHotlistItemNames(
+ self.cnxn, self.hotlist_1.hotlist_id,
+ [item.issue_id for item in self.hotlist_1.items], self.services)
+ request = hotlists_pb2.RerankHotlistItemsRequest(
+ name=self.hotlist_resource_name,
+ hotlist_items=[
+ item_names_dict[self.issue_4.issue_id],
+ item_names_dict[self.issue_3.issue_id]
+ ],
+ target_position=0)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+ self.CallWrapped(self.hotlists_svcr.RerankHotlistItems, mc, request)
+ updated_hotlist = self.services.features.GetHotlist(
+ self.cnxn, self.hotlist_1.hotlist_id)
+ self.assertEqual(
+ [item.issue_id for item in updated_hotlist.items],
+ [self.issue_4.issue_id, self.issue_3.issue_id,
+ self.issue_1.issue_id, self.issue_2.issue_id])
+
+ def testRemoveHotlistItems(self):
+ """We can remove items from a Hotlist."""
+ issue_1_name = self.issue_ids_to_name[self.issue_1.issue_id]
+ issue_2_name = self.issue_ids_to_name[self.issue_2.issue_id]
+ request = hotlists_pb2.RemoveHotlistItemsRequest(
+ parent=self.hotlist_resource_name, issues=[issue_1_name, issue_2_name])
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+ self.CallWrapped(self.hotlists_svcr.RemoveHotlistItems, mc, request)
+ updated_hotlist = self.services.features.GetHotlist(
+ self.cnxn, self.hotlist_1.hotlist_id)
+ # The hotlist used to have 4 items and we've removed two.
+ self.assertEqual(len(updated_hotlist.items), 2)
+
+ def testAddHotlistItems(self):
+ """We can add items to a Hotlist."""
+ issue_5_name = self.issue_ids_to_name[self.issue_5.issue_id]
+ issue_6_name = self.issue_ids_to_name[self.issue_6.issue_id]
+ request = hotlists_pb2.AddHotlistItemsRequest(
+ parent=self.hotlist_resource_name, issues=[issue_5_name, issue_6_name])
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+ self.CallWrapped(self.hotlists_svcr.AddHotlistItems, mc, request)
+ updated_hotlist = self.services.features.GetHotlist(
+ self.cnxn, self.hotlist_1.hotlist_id)
+ # The hotlist used to have 4 items and we've added two.
+ self.assertEqual(len(updated_hotlist.items), 6)
+
+ def testRemoveHotlistEditors(self):
+ """We can remove editors from a Hotlist."""
+ user_2_name = self.user_ids_to_name[self.user_2.user_id]
+ request = hotlists_pb2.RemoveHotlistEditorsRequest(
+ name=self.hotlist_resource_name, editors=[user_2_name])
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+ self.CallWrapped(self.hotlists_svcr.RemoveHotlistEditors, mc, request)
+ updated_hotlist = self.services.features.GetHotlist(
+ self.cnxn, self.hotlist_1.hotlist_id)
+ # User 2 was the only editor in the hotlist, and we removed them.
+ self.assertEqual(len(updated_hotlist.editor_ids), 0)
+
+ def testGetHotlist(self):
+ """We can get a Hotlist."""
+ request = hotlists_pb2.GetHotlistRequest(name=self.hotlist_resource_name)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+ api_hotlist = self.CallWrapped(self.hotlists_svcr.GetHotlist, mc, request)
+ self.assertEqual(api_hotlist, self.converter.ConvertHotlist(self.hotlist_1))
+
+ def testGatherHotlistsForUser(self):
+ """We can get all visible hotlists of a user."""
+ request = hotlists_pb2.GatherHotlistsForUserRequest(
+ user=self.user_ids_to_name[self.user_2.user_id])
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+ response = self.CallWrapped(
+ self.hotlists_svcr.GatherHotlistsForUser, mc, request)
+
+ user_names_by_id = rnc.ConvertUserNames(
+ [self.user_2.user_id, self.user_1.user_id])
+ expected_api_hotlists = [
+ feature_objects_pb2.Hotlist(
+ name=self.hotlist_resource_name,
+ display_name='HotlistName',
+ summary='summary',
+ description='description',
+ hotlist_privacy=feature_objects_pb2.Hotlist.HotlistPrivacy.Value(
+ 'PRIVATE'),
+ owner=user_names_by_id[self.user_1.user_id],
+ editors=[user_names_by_id[self.user_2.user_id]])
+ ]
+ self.assertEqual(
+ response,
+ hotlists_pb2.GatherHotlistsForUserResponse(
+ hotlists=expected_api_hotlists))
+
+ def testUpdateHotlist_AllFields(self):
+ """We can update a Hotlist."""
+ request = hotlists_pb2.UpdateHotlistRequest(
+ update_mask=field_mask_pb2.FieldMask(
+ paths=[
+ 'summary',
+ 'description',
+ 'default_columns',
+ 'hotlist_privacy',
+ 'display_name',
+ 'owner',
+ 'editors',
+ ]),
+ hotlist=feature_objects_pb2.Hotlist(
+ name=self.hotlist_resource_name,
+ display_name='newName',
+ summary='new summary',
+ description='new description',
+ default_columns=[
+ issue_objects_pb2.IssuesListColumn(column='new-chicken-egg')
+ ],
+ hotlist_privacy=feature_objects_pb2.Hotlist.HotlistPrivacy.Value(
+ 'PUBLIC'),
+ owner=self.user_ids_to_name[self.user_2.user_id],
+ editors=[self.user_ids_to_name[self.user_3.user_id]]))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+ api_hotlist = self.CallWrapped(
+ self.hotlists_svcr.UpdateHotlist, mc, request)
+ user_names_by_id = rnc.ConvertUserNames(
+ [self.user_3.user_id, self.user_2.user_id, self.user_1.user_id])
+ expected_hotlist = feature_objects_pb2.Hotlist(
+ name=self.hotlist_resource_name,
+ display_name='newName',
+ summary='new summary',
+ description='new description',
+ default_columns=[
+ issue_objects_pb2.IssuesListColumn(column='new-chicken-egg')
+ ],
+ hotlist_privacy=feature_objects_pb2.Hotlist.HotlistPrivacy.Value(
+ 'PUBLIC'),
+ owner=user_names_by_id[self.user_2.user_id],
+ editors=[
+ user_names_by_id[self.user_2.user_id],
+ user_names_by_id[self.user_3.user_id]
+ ])
+ self.assertEqual(api_hotlist, expected_hotlist)
+
+ def testUpdateHotlist_OneField(self):
+ request = hotlists_pb2.UpdateHotlistRequest(
+ update_mask=field_mask_pb2.FieldMask(paths=['summary']),
+ hotlist=feature_objects_pb2.Hotlist(
+ name=self.hotlist_resource_name,
+ display_name='newName',
+ summary='new summary',
+ description='new description',
+ default_columns=[
+ issue_objects_pb2.IssuesListColumn(column='new-chicken-egg')
+ ],
+ hotlist_privacy=feature_objects_pb2.Hotlist.HotlistPrivacy.Value(
+ 'PUBLIC')))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+ api_hotlist = self.CallWrapped(
+ self.hotlists_svcr.UpdateHotlist, mc, request)
+ user_names_by_id = rnc.ConvertUserNames(
+ [self.user_2.user_id, self.user_1.user_id])
+ expected_hotlist = feature_objects_pb2.Hotlist(
+ name=self.hotlist_resource_name,
+ display_name='HotlistName',
+ summary='new summary',
+ description='description',
+ default_columns=[],
+ hotlist_privacy=feature_objects_pb2.Hotlist.HotlistPrivacy.Value(
+ 'PRIVATE'),
+ owner=user_names_by_id[self.user_1.user_id],
+ editors=[user_names_by_id[self.user_2.user_id]])
+ self.assertEqual(api_hotlist, expected_hotlist)
+
+ def testUpdateHotlist_EmptyFieldMask(self):
+ request = hotlists_pb2.UpdateHotlistRequest(
+ hotlist=feature_objects_pb2.Hotlist(summary='new'))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.hotlists_svcr.UpdateHotlist, mc, request)
+
+ def testUpdateHotlist_EmptyHotlist(self):
+ request = hotlists_pb2.UpdateHotlistRequest(
+ update_mask=field_mask_pb2.FieldMask(paths=['summary']))
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.hotlists_svcr.UpdateHotlist, mc, request)
+
+ def testDeleteHotlist(self):
+ """We can delete a Hotlist."""
+ request = hotlists_pb2.GetHotlistRequest(name=self.hotlist_resource_name)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+ api_response = self.CallWrapped(
+ self.hotlists_svcr.DeleteHotlist, mc, request)
+ self.assertEqual(api_response, empty_pb2.Empty())
+
+ with self.assertRaises(features_svc.NoSuchHotlistException):
+ self.services.features.GetHotlist(
+ self.cnxn, self.hotlist_1.hotlist_id)
diff --git a/api/v3/test/issues_servicer_test.py b/api/v3/test/issues_servicer_test.py
new file mode 100644
index 0000000..7cfee41
--- /dev/null
+++ b/api/v3/test/issues_servicer_test.py
@@ -0,0 +1,890 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for the issues servicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import copy
+import unittest
+import mock
+
+from api.v3 import converters
+from api.v3 import issues_servicer
+from api.v3.api_proto import issues_pb2
+from api.v3.api_proto import issue_objects_pb2
+from framework import exceptions
+from framework import framework_helpers
+from framework import monorailcontext
+from framework import permissions
+from proto import tracker_pb2
+from testing import fake
+from services import service_manager
+
+from google.appengine.ext import testbed
+from google.protobuf import timestamp_pb2
+from google.protobuf import field_mask_pb2
+
+
+def _Issue(project_id, local_id):
+ issue = tracker_pb2.Issue(owner_id=0)
+ issue.project_name = 'proj-%d' % project_id
+ issue.project_id = project_id
+ issue.local_id = local_id
+ issue.issue_id = project_id * 100 + local_id
+ return issue
+
+
+CURRENT_TIME = 12346.78
+
+
+class IssuesServicerTest(unittest.TestCase):
+
+ def setUp(self):
+ # memcache and datastore needed for generating page tokens.
+ self.testbed = testbed.Testbed()
+ self.testbed.activate()
+ self.testbed.init_memcache_stub()
+ self.testbed.init_datastore_v3_stub()
+
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ config=fake.ConfigService(),
+ issue=fake.IssueService(),
+ issue_star=fake.IssueStarService(),
+ project=fake.ProjectService(),
+ features=fake.FeaturesService(),
+ spam=fake.SpamService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService())
+ self.issues_svcr = issues_servicer.IssuesServicer(
+ self.services, make_rate_limiter=False)
+ self.PAST_TIME = int(CURRENT_TIME - 1)
+
+ self.owner = self.services.user.TestAddUser('owner@example.com', 111)
+ self.user_2 = self.services.user.TestAddUser('user_2@example.com', 222)
+
+ self.project_1 = self.services.project.TestAddProject(
+ 'chicken', project_id=789)
+ self.issue_1_resource_name = 'projects/chicken/issues/1234'
+ self.issue_1 = fake.MakeTestIssue(
+ self.project_1.project_id,
+ 1234,
+ 'sum',
+ 'New',
+ self.owner.user_id,
+ labels=['find-me', 'pri-3'],
+ project_name=self.project_1.project_name)
+ self.services.issue.TestAddIssue(self.issue_1)
+
+ self.project_2 = self.services.project.TestAddProject('cow', project_id=788)
+ self.issue_2_resource_name = 'projects/cow/issues/1235'
+ self.issue_2 = fake.MakeTestIssue(
+ self.project_2.project_id,
+ 1235,
+ 'sum',
+ 'New',
+ self.user_2.user_id,
+ project_name=self.project_2.project_name)
+ self.services.issue.TestAddIssue(self.issue_2)
+ self.issue_3 = fake.MakeTestIssue(
+ self.project_2.project_id,
+ 1236,
+ 'sum',
+ 'New',
+ self.user_2.user_id,
+ labels=['find-me', 'pri-1'],
+ project_name=self.project_2.project_name)
+ self.services.issue.TestAddIssue(self.issue_3)
+
+ def CallWrapped(self, wrapped_handler, mc, *args, **kwargs):
+ self.issues_svcr.converter = converters.Converter(mc, self.services)
+ return wrapped_handler.wrapped(self.issues_svcr, mc, *args, **kwargs)
+
+ def testGetIssue(self):
+ """We can get an issue."""
+ request = issues_pb2.GetIssueRequest(name=self.issue_1_resource_name)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ actual_response = self.CallWrapped(self.issues_svcr.GetIssue, mc, request)
+ self.assertEqual(
+ actual_response, self.issues_svcr.converter.ConvertIssue(self.issue_1))
+
+ def testBatchGetIssues(self):
+ """We can batch get issues."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.BatchGetIssuesRequest(
+ names=['projects/cow/issues/1235', 'projects/cow/issues/1236'])
+ actual_response = self.CallWrapped(
+ self.issues_svcr.BatchGetIssues, mc, request)
+ self.assertEqual(
+ [issue.name for issue in actual_response.issues],
+ ['projects/cow/issues/1235', 'projects/cow/issues/1236'])
+
+ def testBatchGetIssues_Empty(self):
+ """We can return a response if the request has no names."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.BatchGetIssuesRequest(names=[])
+ actual_response = self.CallWrapped(
+ self.issues_svcr.BatchGetIssues, mc, request)
+ self.assertEqual(
+ actual_response, issues_pb2.BatchGetIssuesResponse(issues=[]))
+
+ def testBatchGetIssues_WithParent(self):
+ """We can batch get issues with a given parent."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.BatchGetIssuesRequest(
+ parent='projects/cow',
+ names=['projects/cow/issues/1235', 'projects/cow/issues/1236'])
+ actual_response = self.CallWrapped(
+ self.issues_svcr.BatchGetIssues, mc, request)
+ self.assertEqual(
+ [issue.name for issue in actual_response.issues],
+ ['projects/cow/issues/1235', 'projects/cow/issues/1236'])
+
+ def testBatchGetIssues_FromMultipleProjects(self):
+ """We can batch get issues from multiple projects."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.BatchGetIssuesRequest(
+ names=[
+ 'projects/chicken/issues/1234', 'projects/cow/issues/1235',
+ 'projects/cow/issues/1236'
+ ])
+ actual_response = self.CallWrapped(
+ self.issues_svcr.BatchGetIssues, mc, request)
+ self.assertEqual(
+ [issue.name for issue in actual_response.issues], [
+ 'projects/chicken/issues/1234', 'projects/cow/issues/1235',
+ 'projects/cow/issues/1236'
+ ])
+
+ def testBatchGetIssues_WithBadInput(self):
+ """We raise an exception with bad input to batch get issues."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.BatchGetIssuesRequest(
+ parent='projects/cow',
+ names=['projects/cow/issues/1235', 'projects/chicken/issues/1234'])
+ with self.assertRaisesRegexp(
+ exceptions.InputException,
+ 'projects/chicken/issues/1234 is not a child issue of projects/cow.'):
+ self.CallWrapped(self.issues_svcr.BatchGetIssues, mc, request)
+
+ request = issues_pb2.BatchGetIssuesRequest(
+ parent='projects/sheep',
+ names=['projects/cow/issues/1235', 'projects/chicken/issues/1234'])
+ with self.assertRaisesRegexp(
+ exceptions.InputException,
+ 'projects/cow/issues/1235 is not a child issue of projects/sheep.\n' +
+ 'projects/chicken/issues/1234 is not a child issue of projects/sheep.'):
+ self.CallWrapped(self.issues_svcr.BatchGetIssues, mc, request)
+
+ request = issues_pb2.BatchGetIssuesRequest(
+ parent='projects/cow',
+ names=['projects/cow/badformat/1235', 'projects/chicken/issues/1234'])
+ with self.assertRaisesRegexp(
+ exceptions.InputException,
+ 'Invalid resource name: projects/cow/badformat/1235.'):
+ self.CallWrapped(self.issues_svcr.BatchGetIssues, mc, request)
+
+ def testBatchGetIssues_NonExistentIssues(self):
+ """We raise an exception with bad input to batch get issues."""
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.BatchGetIssuesRequest(
+ parent='projects/chicken',
+ names=['projects/chicken/issues/1', 'projects/chicken/issues/2'])
+ with self.assertRaisesRegexp(
+ exceptions.NoSuchIssueException,
+ "\['projects/chicken/issues/1', 'projects/chicken/issues/2'\] not found"
+ ):
+ self.CallWrapped(self.issues_svcr.BatchGetIssues, mc, request)
+
+ @mock.patch('api.v3.api_constants.MAX_BATCH_ISSUES', 2)
+ def testBatchGetIssues(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.BatchGetIssuesRequest(
+ parent='projects/cow',
+ names=[
+ 'projects/cow/issues/1235', 'projects/chicken/issues/1234',
+ 'projects/cow/issues/1233'
+ ])
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.BatchGetIssues, mc, request)
+
+ @mock.patch('search.frontendsearchpipeline.FrontendSearchPipeline')
+ @mock.patch('api.v3.api_constants.MAX_ISSUES_PER_PAGE', 2)
+ def testSearchIssues(self, mock_pipeline):
+ """We can search for issues in some projects."""
+ request = issues_pb2.SearchIssuesRequest(
+ projects=['projects/chicken', 'projects/cow'],
+ query='label:find-me',
+ order_by='-pri',
+ page_size=3)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_2.email)
+
+ instance = mock.Mock(
+ spec=True, total_count=3, visible_results=[self.issue_1, self.issue_3])
+ mock_pipeline.return_value = instance
+ instance.SearchForIIDs = mock.Mock()
+ instance.MergeAndSortIssues = mock.Mock()
+ instance.Paginate = mock.Mock()
+
+ actual_response = self.CallWrapped(
+ self.issues_svcr.SearchIssues, mc, request)
+ # start index is 0.
+ # number of items is coerced from 3 -> 2
+ mock_pipeline.assert_called_once_with(
+ self.cnxn,
+ self.services,
+ mc.auth, [222],
+ 'label:find-me', ['chicken', 'cow'],
+ 2,
+ 0,
+ 1,
+ '',
+ '-pri',
+ mc.warnings,
+ mc.errors,
+ True,
+ mc.profiler,
+ project=None)
+ self.assertEqual(
+ [issue.name for issue in actual_response.issues],
+ ['projects/chicken/issues/1234', 'projects/cow/issues/1236'])
+
+ # Check the `next_page_token` can be used to get the next page of results.
+ request.page_token = actual_response.next_page_token
+ self.CallWrapped(self.issues_svcr.SearchIssues, mc, request)
+ # start index is now 2.
+ mock_pipeline.assert_called_with(
+ self.cnxn,
+ self.services,
+ mc.auth, [222],
+ 'label:find-me', ['chicken', 'cow'],
+ 2,
+ 2,
+ 1,
+ '',
+ '-pri',
+ mc.warnings,
+ mc.errors,
+ True,
+ mc.profiler,
+ project=None)
+
+ @mock.patch('search.frontendsearchpipeline.FrontendSearchPipeline')
+ @mock.patch('api.v3.api_constants.MAX_ISSUES_PER_PAGE', 2)
+ def testSearchIssues_PaginationErrorOrderByChanged(self, mock_pipeline):
+ """Error when changing the order_by and using the same page_otoken."""
+ request = issues_pb2.SearchIssuesRequest(
+ projects=['projects/chicken', 'projects/cow'],
+ query='label:find-me',
+ order_by='-pri',
+ page_size=3)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_2.email)
+
+ instance = mock.Mock(
+ spec=True, total_count=3, visible_results=[self.issue_1, self.issue_3])
+ mock_pipeline.return_value = instance
+ instance.SearchForIIDs = mock.Mock()
+ instance.MergeAndSortIssues = mock.Mock()
+ instance.Paginate = mock.Mock()
+
+ actual_response = self.CallWrapped(
+ self.issues_svcr.SearchIssues, mc, request)
+
+ # The request should fail if we use `next_page_token` and change parameters.
+ request.page_token = actual_response.next_page_token
+ request.order_by = 'owner'
+ with self.assertRaises(exceptions.PageTokenException):
+ self.CallWrapped(self.issues_svcr.SearchIssues, mc, request)
+
+ # Note the 'empty' case doesn't make sense for ListComments, as one is created
+ # for every issue.
+ def testListComments(self):
+ comment_2 = tracker_pb2.IssueComment(
+ id=123,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='comment 2')
+ self.services.issue.TestAddComment(comment_2, self.issue_1.local_id)
+ request = issues_pb2.ListCommentsRequest(
+ parent=self.issue_1_resource_name, page_size=1)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ actual_response = self.CallWrapped(
+ self.issues_svcr.ListComments, mc, request)
+ self.assertEqual(len(actual_response.comments), 1)
+
+ # Check the `next_page_token` can be used to get the next page of results
+ request.page_token = actual_response.next_page_token
+ next_actual_response = self.CallWrapped(
+ self.issues_svcr.ListComments, mc, request)
+ self.assertEqual(len(next_actual_response.comments), 1)
+ self.assertEqual(next_actual_response.comments[0].content, 'comment 2')
+
+ def testListComments_UnsupportedFilter(self):
+ """If anything other than approval is provided, it's an error."""
+ filter_str = 'content = "x"'
+ request = issues_pb2.ListCommentsRequest(
+ parent=self.issue_1_resource_name, page_size=1, filter=filter_str)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.ListComments, mc, request)
+
+ def testListComments_TwoApprovalsErrors(self):
+ """If anything other than a single approval is provided, it's an error."""
+ filter_str = (
+ 'approval = "projects/chicken/approvalDefs/404" OR '
+ 'approval = "projects/chicken/approvalDefs/405')
+ request = issues_pb2.ListCommentsRequest(
+ parent=self.issue_1_resource_name, page_size=1, filter=filter_str)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.ListComments, mc, request)
+
+ def testListComments_FilterTypoError(self):
+ """Even an extra space is an error."""
+ filter_str = 'approval = "projects/chicken/approvalDefs/404" '
+ request = issues_pb2.ListCommentsRequest(
+ parent=self.issue_1_resource_name, page_size=1, filter=filter_str)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.ListComments, mc, request)
+
+ def testListComments_UnknownApprovalInFilter(self):
+ """Filter with unknown approval returns no error and no comments."""
+ approval_comment = tracker_pb2.IssueComment(
+ id=123,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='comment 2 - approval 1',
+ approval_id=1)
+ self.services.issue.TestAddComment(approval_comment, self.issue_1.local_id)
+ request = issues_pb2.ListCommentsRequest(
+ parent=self.issue_1_resource_name, page_size=1,
+ filter='approval = "projects/chicken/approvalDefs/404"')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(self.issues_svcr.ListComments, mc, request)
+ self.assertEqual(len(response.comments), 0)
+
+ def testListComments_ApprovalInFilter(self):
+ approval_comment = tracker_pb2.IssueComment(
+ id=123,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='comment 2 - approval 1',
+ approval_id=1)
+ self.services.issue.TestAddComment(approval_comment, self.issue_1.local_id)
+ request = issues_pb2.ListCommentsRequest(
+ parent=self.issue_1_resource_name, page_size=1,
+ filter='approval = "projects/chicken/approvalDefs/1"')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(self.issues_svcr.ListComments, mc, request)
+ self.assertEqual(len(response.comments), 1)
+ self.assertEqual(response.comments[0].content, approval_comment.content)
+
+ def testListApprovalValues(self):
+ config = fake.MakeTestConfig(self.project_2.project_id, [], [])
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ # Make regular field def and value
+ fd_1 = fake.MakeTestFieldDef(
+ 1, self.project_2.project_id, tracker_pb2.FieldTypes.STR_TYPE,
+ field_name='field1')
+ self.services.config.TestAddFieldDef(fd_1)
+ fv_1 = fake.MakeFieldValue(
+ field_id=fd_1.field_id, str_value='value1', derived=False)
+
+ # Make testing approval def and its associated field def
+ approval_gate = fake.MakeTestFieldDef(
+ 2, self.project_2.project_id, tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ field_name='approval-gate-1')
+ self.services.config.TestAddFieldDef(approval_gate)
+ ad = fake.MakeTestApprovalDef(2, approver_ids=[self.user_2.user_id])
+ self.services.config.TestAddApprovalDef(ad, self.project_2.project_id)
+
+ # Make approval value
+ av = fake.MakeApprovalValue(2, set_on=self.PAST_TIME,
+ approver_ids=[self.user_2.user_id], setter_id=self.user_2.user_id)
+
+ # Make field def that belongs to above approval_def
+ fd_2 = fake.MakeTestFieldDef(
+ 3, self.project_2.project_id, tracker_pb2.FieldTypes.STR_TYPE,
+ field_name='field2', approval_id=2)
+ self.services.config.TestAddFieldDef(fd_2)
+ fv_2 = fake.MakeFieldValue(
+ field_id=fd_2.field_id, str_value='value2', derived=False)
+
+ issue_resource_name = 'projects/cow/issues/1237'
+ issue = fake.MakeTestIssue(
+ self.project_2.project_id,
+ 1237,
+ 'sum',
+ 'New',
+ self.user_2.user_id,
+ project_name=self.project_2.project_name,
+ field_values=[fv_1, fv_2],
+ approval_values=[av])
+ self.services.issue.TestAddIssue(issue)
+
+ request = issues_pb2.ListApprovalValuesRequest(parent=issue_resource_name)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ actual_response = self.CallWrapped(
+ self.issues_svcr.ListApprovalValues, mc, request)
+
+ self.assertEqual(len(actual_response.approval_values), 1)
+ expected_fv = issue_objects_pb2.FieldValue(
+ field='projects/cow/fieldDefs/3',
+ value='value2',
+ derivation=issue_objects_pb2.Derivation.Value('EXPLICIT'))
+ expected = issue_objects_pb2.ApprovalValue(
+ name='projects/cow/issues/1237/approvalValues/2',
+ status=issue_objects_pb2.ApprovalValue.ApprovalStatus.Value('NOT_SET'),
+ approvers=['users/222'],
+ approval_def='projects/cow/approvalDefs/2',
+ set_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
+ setter='users/222',
+ field_values=[expected_fv])
+ self.assertEqual(actual_response.approval_values[0], expected)
+
+ def testListApprovalValues_Empty(self):
+ request = issues_pb2.ListApprovalValuesRequest(
+ parent=self.issue_1_resource_name)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ actual_response = self.CallWrapped(
+ self.issues_svcr.ListApprovalValues, mc, request)
+ self.assertEqual(len(actual_response.approval_values), 0)
+
+ @mock.patch(
+ 'features.send_notifications.PrepareAndSendIssueChangeNotification')
+ def testMakeIssue(self, _fake_pasicn):
+ request_issue = issue_objects_pb2.Issue(
+ summary='sum',
+ status=issue_objects_pb2.Issue.StatusValue(status='New'),
+ cc_users=[issue_objects_pb2.Issue.UserValue(user='users/222')],
+ labels=[issue_objects_pb2.Issue.LabelValue(label='foo-bar')]
+ )
+ request = issues_pb2.MakeIssueRequest(
+ parent='projects/chicken',
+ issue=request_issue,
+ description='description'
+ )
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(
+ self.issues_svcr.MakeIssue, mc, request)
+ self.assertEqual(response.summary, 'sum')
+ self.assertEqual(response.status.status, 'New')
+ self.assertEqual(response.cc_users[0].user, 'users/222')
+ self.assertEqual(response.labels[0].label, 'foo-bar')
+ self.assertEqual(response.star_count, 1)
+
+ @mock.patch(
+ 'features.send_notifications.PrepareAndSendIssueChangeNotification')
+ @mock.patch('time.time')
+ def testModifyIssues(self, fake_time, fake_notify):
+ fake_time.return_value = 12345
+
+ issue = _Issue(780, 1)
+ self.services.project.TestAddProject(
+ issue.project_name, project_id=issue.project_id,
+ owner_ids=[self.owner.user_id])
+
+ issue.labels = ['keep-me', 'remove-me']
+ self.services.issue.TestAddIssue(issue)
+ exp_issue = copy.deepcopy(issue)
+
+ self.services.issue.CreateIssueComment = mock.Mock()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+
+ request = issues_pb2.ModifyIssuesRequest(
+ deltas=[
+ issues_pb2.IssueDelta(
+ issue=issue_objects_pb2.Issue(
+ name='projects/proj-780/issues/1',
+ labels=[issue_objects_pb2.Issue.LabelValue(
+ label='add-me')]),
+ update_mask=field_mask_pb2.FieldMask(paths=['labels']),
+ labels_remove=['remove-me'])],
+ uploads=[issues_pb2.AttachmentUpload(
+ filename='mowgli.gif', content='cute dog')],
+ comment_content='Release the chicken.',
+ notify_type=issues_pb2.NotifyType.Value('NO_NOTIFICATION'))
+
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyIssues, mc, request)
+ exp_issue.labels = ['keep-me', 'add-me']
+ exp_issue.modified_timestamp = 12345
+ exp_api_issue = self.issues_svcr.converter.ConvertIssue(exp_issue)
+ self.assertEqual([iss for iss in response.issues], [exp_api_issue])
+
+ # All updated issues should have been fetched from DB, skipping cache.
+ # So we expect assume_stale=False was applied to all issues during the
+ # the fetch.
+ exp_issue.assume_stale = False
+ # These derived values get set to the following when an issue goes through
+ # the ApplyFilterRules path. (see filter_helpers._ComputeDerivedFields)
+ exp_issue.derived_owner_id = 0
+ exp_issue.derived_status = ''
+ exp_attachments = [framework_helpers.AttachmentUpload(
+ 'mowgli.gif', 'cute dog', 'image/gif')]
+ exp_amendments = [tracker_pb2.Amendment(
+ field=tracker_pb2.FieldID.LABELS, newvalue='-remove-me add-me')]
+ self.services.issue.CreateIssueComment.assert_called_once_with(
+ self.cnxn, exp_issue, mc.auth.user_id, 'Release the chicken.',
+ attachments=exp_attachments, amendments=exp_amendments, commit=False)
+ fake_notify.assert_called_once_with(
+ issue.issue_id, 'testing-app.appspot.com', self.owner.user_id,
+ comment_id=mock.ANY, old_owner_id=None, send_email=False)
+
+ def testModifyIssues_Empty(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.ModifyIssuesRequest()
+ response = self.CallWrapped(self.issues_svcr.ModifyIssues, mc, request)
+ self.assertEqual(response, issues_pb2.ModifyIssuesResponse())
+
+ @mock.patch('api.v3.api_constants.MAX_MODIFY_ISSUES', 2)
+ @mock.patch('api.v3.api_constants.MAX_MODIFY_IMPACTED_ISSUES', 4)
+ def testModifyIssues_TooMany(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.ModifyIssuesRequest(
+ deltas=[
+ issues_pb2.IssueDelta(),
+ issues_pb2.IssueDelta(),
+ issues_pb2.IssueDelta()
+ ])
+ with self.assertRaisesRegexp(
+ exceptions.InputException,
+ 'Requesting 3 updates when the allowed maximum is 2 updates.'):
+ self.CallWrapped(self.issues_svcr.ModifyIssues, mc, request)
+
+ issue_ref_list = [issue_objects_pb2.IssueRef()]
+ request = issues_pb2.ModifyIssuesRequest(
+ deltas=[
+ issues_pb2.IssueDelta(
+ issue=issue_objects_pb2.Issue(
+ blocked_on_issue_refs=issue_ref_list),
+ blocked_on_issues_remove=issue_ref_list,
+ update_mask=field_mask_pb2.FieldMask(
+ paths=['merged_into_issue_ref'])),
+ issues_pb2.IssueDelta(
+ issue=issue_objects_pb2.Issue(
+ blocking_issue_refs=issue_ref_list),
+ blocking_issues_remove=issue_ref_list)
+ ])
+ with self.assertRaisesRegexp(
+ exceptions.InputException,
+ 'Updates include 5 impacted issues when the allowed maximum is 4.'):
+ self.CallWrapped(self.issues_svcr.ModifyIssues, mc, request)
+
+ @mock.patch('time.time', mock.MagicMock(return_value=CURRENT_TIME))
+ @mock.patch(
+ 'features.send_notifications.PrepareAndSendApprovalChangeNotification')
+ def testModifyIssueApprovalValues(self, fake_notify):
+ self.services.issue.DeltaUpdateIssueApproval = mock.Mock()
+ config = fake.MakeTestConfig(self.project_1.project_id, [], [])
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ # Make testing approval def and its associated field def
+ field_id = 2
+ approval_field_def = fake.MakeTestFieldDef(
+ field_id,
+ self.project_1.project_id,
+ tracker_pb2.FieldTypes.APPROVAL_TYPE,
+ field_name='approval-gate-1')
+ self.services.config.TestAddFieldDef(approval_field_def)
+ ad = fake.MakeTestApprovalDef(field_id, approver_ids=[self.owner.user_id])
+ self.services.config.TestAddApprovalDef(ad, self.project_1.project_id)
+
+ # Make approval value
+ av = fake.MakeApprovalValue(
+ field_id,
+ status=tracker_pb2.ApprovalStatus.NEEDS_REVIEW,
+ set_on=self.PAST_TIME,
+ approver_ids=[self.owner.user_id],
+ setter_id=self.user_2.user_id)
+
+ issue = fake.MakeTestIssue(
+ self.project_1.project_id,
+ 1237,
+ 'sum',
+ 'New',
+ self.owner.user_id,
+ project_name=self.project_1.project_name,
+ approval_values=[av])
+ self.services.issue.TestAddIssue(issue)
+
+ av_name = 'projects/%s/issues/%d/approvalValues/%d' % (
+ self.project_1.project_name, issue.local_id, ad.approval_id)
+ delta = issues_pb2.ApprovalDelta(
+ approval_value=issue_objects_pb2.ApprovalValue(
+ name=av_name,
+ status=issue_objects_pb2.ApprovalValue.ApprovalStatus.Value('NA')),
+ update_mask=field_mask_pb2.FieldMask(paths=['status']))
+
+ request = issues_pb2.ModifyIssueApprovalValuesRequest(deltas=[delta],)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyIssueApprovalValues, mc, request)
+ expected_ingested_delta = tracker_pb2.ApprovalDelta(
+ status=tracker_pb2.ApprovalStatus.NA,
+ set_on=int(CURRENT_TIME),
+ setter_id=self.owner.user_id,
+ )
+ # NOTE: Because we mock out DeltaUpdateIssueApproval, the ApprovalValues
+ # returned haven't been changed in this test. We can't test that it was
+ # changed correctly, but we can make sure it's for the right ApprovalValue.
+ self.assertEqual(len(response.approval_values), 1)
+ self.assertEqual(response.approval_values[0].name, av_name)
+ self.services.issue.DeltaUpdateIssueApproval.assert_called_once_with(
+ mc.cnxn,
+ self.owner.user_id,
+ config,
+ issue,
+ av,
+ expected_ingested_delta,
+ comment_content=u'',
+ is_description=False,
+ attachments=None,
+ kept_attachments=None)
+ fake_notify.assert_called_once_with(
+ issue.issue_id,
+ ad.approval_id,
+ 'testing-app.appspot.com',
+ mock.ANY,
+ send_email=True)
+
+ @mock.patch('api.v3.api_constants.MAX_MODIFY_APPROVAL_VALUES', 2)
+ def testModifyIssueApprovalValues_TooMany(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ request = issues_pb2.ModifyIssueApprovalValuesRequest(
+ deltas=[
+ issues_pb2.ApprovalDelta(),
+ issues_pb2.ApprovalDelta(),
+ issues_pb2.ApprovalDelta()
+ ])
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.issues_svcr.ModifyIssueApprovalValues, mc, request)
+
+ def testModifyIssueApprovalValues_Empty(self):
+ request = issues_pb2.ModifyIssueApprovalValuesRequest()
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyIssueApprovalValues, mc, request)
+ self.assertEqual(len(response.approval_values), 0)
+
+ @mock.patch(
+ 'businesslogic.work_env.WorkEnv.GetIssue',
+ return_value=tracker_pb2.Issue(
+ owner_id=0,
+ project_name='chicken',
+ project_id=789,
+ local_id=1234,
+ issue_id=80134))
+ def testModifyCommentState(self, mocked_get_issue):
+ name = self.issue_1_resource_name + '/comments/1'
+ state = issue_objects_pb2.IssueContentState.Value('DELETED')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ with self.assertRaises(exceptions.NoSuchCommentException):
+ self.CallWrapped(self.issues_svcr.ModifyCommentState, mc, request)
+ mocked_get_issue.assert_any_call(self.issue_1.issue_id, use_cache=False)
+
+ def testModifyCommentState_Delete(self):
+ comment_1 = tracker_pb2.IssueComment(
+ id=124,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='first actual comment')
+ self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
+
+ name = self.issue_1_resource_name + '/comments/1'
+ state = issue_objects_pb2.IssueContentState.Value('DELETED')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyCommentState, mc, request)
+ self.assertEqual(response.comment.state, state)
+ self.assertEqual(response.comment.content, 'first actual comment')
+
+ # Test noop
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyCommentState, mc, request)
+ self.assertEqual(response.comment.state, state)
+
+ # Test undelete
+ state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyCommentState, mc, request)
+ self.assertEqual(response.comment.state, state)
+
+ @mock.patch(
+ 'framework.permissions.UpdateIssuePermissions',
+ return_value=permissions.ADMIN_PERMISSIONSET)
+ def testModifyCommentState_Spam(self, _mocked):
+ comment_1 = tracker_pb2.IssueComment(
+ id=124,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='first actual comment')
+ self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
+
+ name = self.issue_1_resource_name + '/comments/1'
+ state = issue_objects_pb2.IssueContentState.Value('SPAM')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyCommentState, mc, request)
+ self.assertEqual(response.comment.state, state)
+
+ # Test noop
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyCommentState, mc, request)
+ self.assertEqual(response.comment.state, state)
+
+ # Test unflag as spam
+ state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyCommentState, mc, request)
+ self.assertEqual(response.comment.state, state)
+
+ def testModifyCommentState_Active(self):
+ comment_1 = tracker_pb2.IssueComment(
+ id=124,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='first actual comment')
+ self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
+
+ name = self.issue_1_resource_name + '/comments/1'
+ state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ response = self.CallWrapped(
+ self.issues_svcr.ModifyCommentState, mc, request)
+ self.assertEqual(response.comment.state, state)
+
+ def testModifyCommentState_Spam_ActionNotSupported(self):
+ # Cannot transition from deleted to spam
+ comment_1 = tracker_pb2.IssueComment(
+ id=124,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='first actual comment',
+ deleted_by=self.owner.user_id)
+ self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
+
+ name = self.issue_1_resource_name + '/comments/1'
+ state = issue_objects_pb2.IssueContentState.Value('SPAM')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ with self.assertRaises(exceptions.ActionNotSupported):
+ self.CallWrapped(self.issues_svcr.ModifyCommentState, mc, request)
+
+ def testModifyCommentState_Delete_ActionNotSupported(self):
+ # Cannot transition from spam to deleted
+ comment_1 = tracker_pb2.IssueComment(
+ id=124,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='first actual comment',
+ is_spam=True)
+ self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
+
+ name = self.issue_1_resource_name + '/comments/1'
+ state = issue_objects_pb2.IssueContentState.Value('DELETED')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ with self.assertRaises(exceptions.ActionNotSupported):
+ self.CallWrapped(self.issues_svcr.ModifyCommentState, mc, request)
+
+ def testModifyCommentState_NoSuchComment(self):
+ name = self.issue_1_resource_name + '/comments/1'
+ state = issue_objects_pb2.IssueContentState.Value('DELETED')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.owner.email)
+ with self.assertRaises(exceptions.NoSuchCommentException):
+ self.CallWrapped(self.issues_svcr.ModifyCommentState, mc, request)
+
+ def testModifyCommentState_Delete_PermissionException(self):
+ comment_1 = tracker_pb2.IssueComment(
+ id=124,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='first actual comment')
+ self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
+
+ name = self.issue_1_resource_name + '/comments/1'
+ state = issue_objects_pb2.IssueContentState.Value('DELETED')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_2.email)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.ModifyCommentState, mc, request)
+
+ @mock.patch(
+ 'framework.permissions.UpdateIssuePermissions',
+ return_value=permissions.READ_ONLY_PERMISSIONSET)
+ def testModifyCommentState_Spam_PermissionException(self, _mocked):
+ comment_1 = tracker_pb2.IssueComment(
+ id=124,
+ issue_id=self.issue_1.issue_id,
+ project_id=self.issue_1.project_id,
+ user_id=self.owner.user_id,
+ content='first actual comment')
+ self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
+
+ name = self.issue_1_resource_name + '/comments/1'
+ state = issue_objects_pb2.IssueContentState.Value('SPAM')
+ request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_2.email)
+ with self.assertRaises(permissions.PermissionException):
+ self.CallWrapped(self.issues_svcr.ModifyCommentState, mc, request)
diff --git a/api/v3/test/monorail_servicer_test.py b/api/v3/test/monorail_servicer_test.py
new file mode 100644
index 0000000..3569879
--- /dev/null
+++ b/api/v3/test/monorail_servicer_test.py
@@ -0,0 +1,534 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for MonorailServicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import time
+import unittest
+import mock
+import mox
+
+from components.prpc import server
+from components.prpc import codes
+from components.prpc import context
+from google.appengine.ext import testbed
+from google.protobuf import json_format
+
+import settings
+from api.v3 import monorail_servicer
+from framework import authdata
+from framework import exceptions
+from framework import framework_constants
+from framework import monorailcontext
+from framework import permissions
+from framework import ratelimiter
+from framework import xsrf
+from services import cachemanager_svc
+from services import config_svc
+from services import service_manager
+from services import features_svc
+from testing import fake
+from testing import testing_helpers
+
+
+class MonorailServicerFunctionsTest(unittest.TestCase):
+
+ def testConvertPRPCStatusToHTTPStatus(self):
+ """We can convert pRPC status codes to http codes for monitoring."""
+ prpc_context = context.ServicerContext()
+
+ prpc_context.set_code(codes.StatusCode.OK)
+ self.assertEqual(
+ 200, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
+
+ prpc_context.set_code(codes.StatusCode.INVALID_ARGUMENT)
+ self.assertEqual(
+ 400, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
+
+ prpc_context.set_code(codes.StatusCode.PERMISSION_DENIED)
+ self.assertEqual(
+ 403, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
+
+ prpc_context.set_code(codes.StatusCode.NOT_FOUND)
+ self.assertEqual(
+ 404, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
+
+ prpc_context.set_code(codes.StatusCode.INTERNAL)
+ self.assertEqual(
+ 500, monorail_servicer.ConvertPRPCStatusToHTTPStatus(prpc_context))
+
+
+class UpdateSomethingRequest(testing_helpers.Blank):
+ """A fake request that would do a write."""
+ pass
+
+
+class ListSomethingRequest(testing_helpers.Blank):
+ """A fake request that would do a read."""
+ pass
+
+
+class TestableServicer(monorail_servicer.MonorailServicer):
+ """Fake servicer class."""
+
+ def __init__(self, services):
+ super(TestableServicer, self).__init__(services)
+ self.was_called = False
+ self.seen_mc = None
+ self.seen_request = None
+
+ @monorail_servicer.PRPCMethod
+ def CalcSomething(self, mc, request):
+ """Raise the test exception, or return what we got for verification."""
+ self.was_called = True
+ self.seen_mc = mc
+ self.seen_request = request
+ assert mc
+ assert request
+ if request.exc_class:
+ raise request.exc_class()
+ else:
+ return 'fake response proto'
+
+
+class MonorailServicerTest(unittest.TestCase):
+
+ def setUp(self):
+ self.mox = mox.Mox()
+ self.testbed = testbed.Testbed()
+ self.testbed.activate()
+ self.testbed.init_memcache_stub()
+ self.testbed.init_datastore_v3_stub()
+ self.testbed.init_user_stub()
+
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService(),
+ project=fake.ProjectService(),
+ cache_manager=fake.CacheManager())
+ self.project = self.services.project.TestAddProject(
+ 'proj', project_id=789, owner_ids=[111])
+ # Allowlisted in testing/api_clients.cfg
+ self.allowlisted_client_id = '98723764876'
+ self.non_member = self.services.user.TestAddUser(
+ 'nonmember@example.com', 222)
+ self.test_user = self.services.user.TestAddUser('test@example.com', 420)
+ self.svcr = TestableServicer(self.services)
+ self.nonmember_token = xsrf.GenerateToken(222, xsrf.XHR_SERVLET_PATH)
+ self.request = UpdateSomethingRequest(exc_class=None)
+ self.prpc_context = context.ServicerContext()
+ self.prpc_context.set_code(codes.StatusCode.OK)
+ self.prpc_context._invocation_metadata = [
+ (monorail_servicer.XSRF_TOKEN_HEADER, self.nonmember_token)]
+ # This string is returned by app_identity.get_application_id() when
+ # called in the test env.
+ self.app_id = 'testing-app'
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+ self.testbed.deactivate()
+
+ def SetUpRecordMonitoringStats(self):
+ self.mox.StubOutWithMock(json_format, 'MessageToJson')
+ json_format.MessageToJson(self.request).AndReturn('json of request')
+ json_format.MessageToJson('fake response proto').AndReturn(
+ 'json of response')
+ self.mox.ReplayAll()
+
+ def testRun_SiteWide_Normal(self):
+ """Calling the handler through the decorator."""
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ self.SetUpRecordMonitoringStats()
+ # pylint: disable=unexpected-keyword-arg
+ response = self.svcr.CalcSomething(
+ self.request, self.prpc_context, cnxn=self.cnxn)
+ self.assertIsNone(self.svcr.seen_mc.cnxn) # Because of CleanUp().
+ self.assertEqual(self.svcr.seen_mc.auth.email, self.non_member.email)
+ self.assertIn(permissions.CREATE_HOTLIST.lower(),
+ self.svcr.seen_mc.perms.perm_names)
+ self.assertNotIn(permissions.ADMINISTER_SITE.lower(),
+ self.svcr.seen_mc.perms.perm_names)
+ self.assertEqual(self.request, self.svcr.seen_request)
+ self.assertEqual('fake response proto', response)
+ self.assertEqual(codes.StatusCode.OK, self.prpc_context._code)
+
+ def testRun_RequesterBanned(self):
+ """If we reject the request, give PERMISSION_DENIED."""
+ self.non_member.banned = 'Spammer'
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ self.SetUpRecordMonitoringStats()
+ # pylint: disable=unexpected-keyword-arg
+ self.svcr.CalcSomething(
+ self.request, self.prpc_context, cnxn=self.cnxn)
+ self.assertFalse(self.svcr.was_called)
+ self.assertEqual(
+ codes.StatusCode.PERMISSION_DENIED, self.prpc_context._code)
+
+ def testRun_AnonymousRequester(self):
+ """Test we properly process anonymous users with valid tokens."""
+ self.prpc_context._invocation_metadata = [
+ (monorail_servicer.XSRF_TOKEN_HEADER,
+ xsrf.GenerateToken(0, xsrf.XHR_SERVLET_PATH))]
+ self.SetUpRecordMonitoringStats()
+ # pylint: disable=unexpected-keyword-arg
+ response = self.svcr.CalcSomething(
+ self.request, self.prpc_context, cnxn=self.cnxn)
+ self.assertIsNone(self.svcr.seen_mc.cnxn) # Because of CleanUp().
+ self.assertIsNone(self.svcr.seen_mc.auth.email)
+ self.assertNotIn(permissions.CREATE_HOTLIST.lower(),
+ self.svcr.seen_mc.perms.perm_names)
+ self.assertNotIn(permissions.ADMINISTER_SITE.lower(),
+ self.svcr.seen_mc.perms.perm_names)
+ self.assertEqual(self.request, self.svcr.seen_request)
+ self.assertEqual('fake response proto', response)
+ self.assertEqual(codes.StatusCode.OK, self.prpc_context._code)
+
+ def testRun_DistributedInvalidation(self):
+ """The Run method must call DoDistributedInvalidation()."""
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ self.SetUpRecordMonitoringStats()
+ # pylint: disable=unexpected-keyword-arg
+ self.svcr.CalcSomething(
+ self.request, self.prpc_context, cnxn=self.cnxn)
+ self.assertIsNotNone(self.services.cache_manager.last_call)
+
+ def testRun_HandlerErrorResponse(self):
+ """An expected exception in the method causes an error status."""
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ self.SetUpRecordMonitoringStats()
+ # pylint: disable=attribute-defined-outside-init
+ self.request.exc_class = exceptions.NoSuchUserException
+ # pylint: disable=unexpected-keyword-arg
+ response = self.svcr.CalcSomething(
+ self.request, self.prpc_context, cnxn=self.cnxn)
+ self.assertTrue(self.svcr.was_called)
+ self.assertIsNone(self.svcr.seen_mc.cnxn) # Because of CleanUp().
+ self.assertEqual(self.svcr.seen_mc.auth.email, self.non_member.email)
+ self.assertEqual(self.request, self.svcr.seen_request)
+ self.assertIsNone(response)
+ self.assertEqual(codes.StatusCode.NOT_FOUND, self.prpc_context._code)
+
+ def testRun_HandlerProgrammingError(self):
+ """An unexception in the handler method is re-raised."""
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ self.SetUpRecordMonitoringStats()
+ # pylint: disable=attribute-defined-outside-init
+ self.request.exc_class = NotImplementedError
+ self.assertRaises(
+ NotImplementedError,
+ self.svcr.CalcSomething,
+ self.request, self.prpc_context, cnxn=self.cnxn)
+ self.assertTrue(self.svcr.was_called)
+ self.assertIsNone(self.svcr.seen_mc.cnxn) # Because of CleanUp().
+
+ def testGetAndAssertRequesterAuth_Cookie_Anon(self):
+ """We get and allow requests from anon user using cookie auth."""
+ metadata = {
+ monorail_servicer.XSRF_TOKEN_HEADER: xsrf.GenerateToken(
+ 0, xsrf.XHR_SERVLET_PATH)}
+ # Signed out.
+ client_id, user_auth = self.svcr.GetAndAssertRequesterAuth(
+ self.cnxn, metadata, self.services)
+ self.assertIsNone(user_auth.email)
+ self.assertEqual(client_id, 'https://%s.appspot.com' % self.app_id)
+
+ def testGetAndAssertRequesterAuth_Cookie_SignedIn(self):
+ """We get and allow requests from signed in users using cookie auth."""
+ metadata = dict(self.prpc_context.invocation_metadata())
+ # Signed in with cookie auth.
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ client_id, user_auth = self.svcr.GetAndAssertRequesterAuth(
+ self.cnxn, metadata, self.services)
+ self.assertEqual(self.non_member.email, user_auth.email)
+ self.assertEqual(client_id, 'https://%s.appspot.com' % self.app_id)
+
+ def testGetAndAssertRequester_Anon_BadToken(self):
+ """We get the email address of the signed in user using oauth."""
+ metadata = {}
+ # Anonymous user has invalid token.
+ with self.assertRaises(permissions.PermissionException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ @mock.patch('google.oauth2.id_token.verify_oauth2_token')
+ def testGetAndAssertRequesterAuth_IDToken_CaseInsensitiveBearer(
+ self, mock_verifier):
+ """We are case-insensitive when looking for the 'bearer' string."""
+ metadata = {'authorization': 'beaReR allowlisted-user-id-token'}
+ some_other_site_user = self.services.user.TestAddUser(
+ 'some-human-user@human.test', 888)
+
+ # Signed in with oauth.
+ mock_verifier.return_value = {
+ 'aud': self.allowlisted_client_id,
+ 'email': some_other_site_user.email,
+ }
+
+ client_id, user_auth = self.svcr.GetAndAssertRequesterAuth(
+ self.cnxn, metadata, self.services)
+ self.assertEqual(client_id, self.allowlisted_client_id)
+ self.assertEqual(user_auth.email, some_other_site_user.email)
+ mock_verifier.assert_called_once_with('allowlisted-user-id-token', mock.ANY)
+
+ @mock.patch('google.oauth2.id_token.verify_oauth2_token')
+ def testGetAndAssertRequesterAuth_IDToken_AutoCreateUser(self, mock_verifier):
+ """We can auto-create Monorail users for the requester."""
+ metadata = {'authorization': 'beaReR allowlisted-user-id-token'}
+ # Signed in with oauth.
+ mock_verifier.return_value = {
+ 'aud': self.allowlisted_client_id,
+ 'email': 'new-user@email.com',
+ }
+
+ client_id, user_auth = self.svcr.GetAndAssertRequesterAuth(
+ self.cnxn, metadata, self.services)
+ self.assertEqual(client_id, self.allowlisted_client_id)
+ self.assertEqual(user_auth.email, 'new-user@email.com')
+ mock_verifier.assert_called_once_with('allowlisted-user-id-token', mock.ANY)
+
+ def testGetAndAssertRequesterAuth_IDToken_InvalidAuthToken(self):
+ """We raise an exception if 'bearer' is missing from headers."""
+ metadata = {'authorization': 'allowlisted-user-id-token'}
+
+ with self.assertRaises(permissions.PermissionException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ @mock.patch('google.oauth2.id_token.verify_oauth2_token')
+ def testGetAndAssertRequesterAuth_IDToken_ServiceAccountAllowed(
+ self, mock_verifier):
+ """We allow requests from allowlisted service accounts with correct aud."""
+ metadata = {'authorization': 'Bearer allowlisted-user-id-token'}
+ # Allowlisted in testing/api_clients.cfg
+ allowlisted_service_account_email = self.services.user.TestAddUser(
+ '123456789@developer.gserviceaccount.com', 889)
+
+ aud = 'https://%s.appspot.com' % self.app_id
+ # Signed in with oauth.
+ mock_verifier.return_value = {
+ 'aud': aud,
+ 'email': allowlisted_service_account_email.email,
+ }
+
+ client_id, user_auth = self.svcr.GetAndAssertRequesterAuth(
+ self.cnxn, metadata, self.services)
+ self.assertEqual(client_id, aud)
+ self.assertEqual(user_auth.email, allowlisted_service_account_email.email)
+ mock_verifier.assert_called_once_with('allowlisted-user-id-token', mock.ANY)
+
+ @mock.patch('google.oauth2.id_token.verify_oauth2_token')
+ def testGetAndAssertRequesterAuth_IDToken_ServiceAccountNotAllowed(
+ self, mock_verifier):
+ """We raise an exception if the service account is not allowlisted"""
+ metadata = {'authorization': 'Bearer non-allowlisted-user-id-token'}
+
+ # Signed in with oauth.
+ mock_verifier.return_value = {
+ 'aud': 'https://%s.appspot.com' % self.app_id,
+ # A random service account, not allow-listed.
+ 'email': 'bigbadwolf@gserviceaccount.com',
+ }
+
+ with self.assertRaisesRegexp(
+ permissions.PermissionException, r'Account .+ is not allowlisted'):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ @mock.patch('google.oauth2.id_token.verify_oauth2_token')
+ def testGetAndAssertRequesterAuth_IDToken_ServiceAccountBadAud(
+ self, mock_verifier):
+ """We raise an exception when a service account token['aud'] is invalid."""
+ metadata = {'authorization': 'Bearer non-allowlisted-user-id-token'}
+ # Allowlisted in testing/api_clients.cfg
+ allowlisted_service_account_email = self.services.user.TestAddUser(
+ '123456789@developer.gserviceaccount.com', 889)
+
+ # Signed in with oauth.
+ mock_verifier.return_value = {
+ 'aud': 'id-token-inteded-for-some-other-site',
+ 'email': allowlisted_service_account_email.email,
+ }
+
+ with self.assertRaisesRegexp(
+ permissions.PermissionException, r'Invalid token audience: .+'):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ @mock.patch('google.oauth2.id_token.verify_oauth2_token')
+ def testGetAndAssertRequesterAuth_IDToken_ClientNotAllowed(
+ self, mock_verifier):
+ """We raise an exception if the client ID is not allowlisted."""
+ metadata = {'authorization': 'Bearer non-allowlisted-client-id-token'}
+
+ # Signed in with oauth.
+ mock_verifier.return_value = {
+ # A client ID not allow-listed.
+ 'aud': 'some-other-site-client-id',
+ # Some human user that the client is impersonating for the request.
+ 'email': 'some-other-site-user@test.com',
+ }
+
+ with self.assertRaisesRegexp(
+ permissions.PermissionException, r'Client .+ is not allowlisted'):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ # Assert some-other-site-user was not auto-created.
+ with self.assertRaises(exceptions.NoSuchUserException):
+ self.services.user.LookupUserID(
+ self.cnxn, 'some-other-site-user@test.com')
+
+ @mock.patch('google.oauth2.id_token.verify_oauth2_token')
+ def testGetAndAssertRequesterAuth_IDToken_NoEmail(self, mock_verifier):
+ """We raise an exception if ID token has no email information."""
+ metadata = {'authorization': 'Bearer allowlisted-user-id-token'}
+
+ # Signed in with oauth.
+ mock_verifier.return_value = {'aud': self.allowlisted_client_id}
+
+ with self.assertRaises(permissions.PermissionException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ @mock.patch('google.oauth2.id_token.verify_oauth2_token')
+ def testGetAndAssertRequesterAuth_IDToken_InvalidIDToken(self, mock_verifier):
+ """We raise an exception if the ID token is invalid."""
+ metadata = {'authorization': 'Bearer bad-token'}
+
+ mock_verifier.side_effect = ValueError()
+
+ with self.assertRaises(permissions.PermissionException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ def testGetAndAssertRequesterAuth_Banned(self):
+ self.non_member.banned = 'Spammer'
+ metadata = dict(self.prpc_context.invocation_metadata())
+ # Signed in with cookie auth.
+ self.testbed.setup_env(user_email=self.non_member.email, overwrite=True)
+ with self.assertRaises(permissions.BannedUserException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ def testGetRequester_TestAccountOnAppspot(self):
+ """Specifying test_account is ignored on deployed server."""
+ # pylint: disable=attribute-defined-outside-init
+ metadata = {'x-test-account': 'test@example.com'}
+ with self.assertRaises(exceptions.InputException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+
+ def testGetRequester_TestAccountOnDev(self):
+ """For integration testing, we can set test_account on dev_server."""
+ try:
+ orig_local_mode = settings.local_mode
+ settings.local_mode = True
+
+ # pylint: disable=attribute-defined-outside-init
+ metadata = {'x-test-account': 'test@example.com'}
+ client_id, test_auth = self.svcr.GetAndAssertRequesterAuth(
+ self.cnxn, metadata, self.services)
+ self.assertEqual('test@example.com', test_auth.email)
+ self.assertEqual('https://%s.appspot.com' % self.app_id, client_id)
+
+ # pylint: disable=attribute-defined-outside-init
+ metadata = {'x-test-account': 'test@anythingelse.com'}
+ with self.assertRaises(exceptions.InputException):
+ self.svcr.GetAndAssertRequesterAuth(self.cnxn, metadata, self.services)
+ finally:
+ settings.local_mode = orig_local_mode
+
+ def testAssertBaseChecks_SiteIsReadOnly_Write(self):
+ """We reject writes and allow reads when site is read-only."""
+ orig_read_only = settings.read_only
+ try:
+ settings.read_only = True
+ metadata = {}
+ self.assertRaises(
+ permissions.PermissionException,
+ self.svcr.AssertBaseChecks, self.request, metadata)
+ finally:
+ settings.read_only = orig_read_only
+
+ def testAssertBaseChecks_SiteIsReadOnly_Read(self):
+ """We reject writes and allow reads when site is read-only."""
+ orig_read_only = settings.read_only
+ try:
+ settings.read_only = True
+ metadata = {monorail_servicer.XSRF_TOKEN_HEADER: self.nonmember_token}
+
+ # Our default request is an update.
+ with self.assertRaises(permissions.PermissionException):
+ self.svcr.AssertBaseChecks(self.request, metadata)
+
+ # A method name starting with "List" or "Get" will run OK.
+ self.request = ListSomethingRequest(exc_class=None)
+ self.svcr.AssertBaseChecks(self.request, metadata)
+ finally:
+ settings.read_only = orig_read_only
+
+ def CheckExceptionStatus(self, e, expected_code, details=None):
+ mc = monorailcontext.MonorailContext(self.services)
+ self.prpc_context.set_code(codes.StatusCode.OK)
+ processed = self.svcr.ProcessException(e, self.prpc_context, mc)
+ if expected_code:
+ self.assertTrue(processed)
+ self.assertEqual(expected_code, self.prpc_context._code)
+ else:
+ self.assertFalse(processed)
+ # Uncaught exceptions should indicate an error.
+ self.assertEqual(codes.StatusCode.INTERNAL, self.prpc_context._code)
+ if details is not None:
+ self.assertEqual(details, self.prpc_context._details)
+
+ def testProcessException(self):
+ """Expected exceptions are converted to pRPC codes, expected not."""
+ self.CheckExceptionStatus(
+ exceptions.NoSuchUserException(), codes.StatusCode.NOT_FOUND)
+ self.CheckExceptionStatus(
+ exceptions.NoSuchProjectException(), codes.StatusCode.NOT_FOUND)
+ self.CheckExceptionStatus(
+ exceptions.NoSuchIssueException(), codes.StatusCode.NOT_FOUND)
+ self.CheckExceptionStatus(
+ exceptions.NoSuchComponentException(), codes.StatusCode.NOT_FOUND)
+ self.CheckExceptionStatus(
+ permissions.BannedUserException(), codes.StatusCode.PERMISSION_DENIED)
+ self.CheckExceptionStatus(
+ permissions.PermissionException(), codes.StatusCode.PERMISSION_DENIED)
+ self.CheckExceptionStatus(
+ exceptions.GroupExistsException(), codes.StatusCode.ALREADY_EXISTS)
+ self.CheckExceptionStatus(
+ exceptions.InvalidComponentNameException(),
+ codes.StatusCode.INVALID_ARGUMENT)
+ self.CheckExceptionStatus(
+ exceptions.FilterRuleException(),
+ codes.StatusCode.INVALID_ARGUMENT,
+ details='Violates filter rule that should error.')
+ self.CheckExceptionStatus(
+ exceptions.InputException('echoed values'),
+ codes.StatusCode.INVALID_ARGUMENT,
+ details='Invalid arguments: echoed values')
+ self.CheckExceptionStatus(
+ exceptions.OverAttachmentQuota(), codes.StatusCode.RESOURCE_EXHAUSTED)
+ self.CheckExceptionStatus(
+ ratelimiter.ApiRateLimitExceeded('client_id', 'email'),
+ codes.StatusCode.PERMISSION_DENIED)
+ self.CheckExceptionStatus(
+ features_svc.HotlistAlreadyExists(), codes.StatusCode.ALREADY_EXISTS)
+ self.CheckExceptionStatus(NotImplementedError(), None)
+
+ def testProcessException_ErrorMessageEscaped(self):
+ """If we ever echo user input in error messages, it is escaped.."""
+ self.CheckExceptionStatus(
+ exceptions.InputException('echoed <script>"code"</script>'),
+ codes.StatusCode.INVALID_ARGUMENT,
+ details=('Invalid arguments: echoed '
+ '<script>"code"</script>'))
+
+ def testRecordMonitoringStats_RequestClassDoesNotEndInRequest(self):
+ """We cope with request proto class names that do not end in 'Request'."""
+ self.request = 'this is a string'
+ self.SetUpRecordMonitoringStats()
+ start_time = 1522559788.939511
+ now = 1522569311.892738
+ self.svcr.RecordMonitoringStats(
+ start_time, self.request, 'fake response proto', self.prpc_context,
+ now=now)
diff --git a/api/v3/test/paginator_test.py b/api/v3/test/paginator_test.py
new file mode 100644
index 0000000..ca0b713
--- /dev/null
+++ b/api/v3/test/paginator_test.py
@@ -0,0 +1,78 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+"""Tests for the Paginator class."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from google.appengine.ext import testbed
+
+from api.v3 import paginator
+from api.v3.api_proto import hotlists_pb2
+from framework import exceptions
+from framework import paginate
+
+class PaginatorTest(unittest.TestCase):
+
+ def setUp(self):
+ self.testbed = testbed.Testbed()
+ self.testbed.activate()
+ self.testbed.init_memcache_stub()
+ self.testbed.init_datastore_v3_stub()
+
+ self.paginator = paginator.Paginator(
+ parent='animal/goose/sound/honks', query='chaos')
+
+ def testGetStart(self):
+ """We can get the start index from a page_token."""
+ start = 5
+ page_token = paginate.GeneratePageToken(
+ self.paginator.request_contents, start)
+ self.assertEqual(self.paginator.GetStart(page_token), start)
+
+ def testGetStart_EmptyPageToken(self):
+ """We return the default start for an empty page_token."""
+ request = hotlists_pb2.ListHotlistItemsRequest()
+ self.assertEqual(0, self.paginator.GetStart(request.page_token))
+
+ def testGenerateNextPageToken(self):
+ """We return the next page token."""
+ next_start = 10
+ expected_page_token = paginate.GeneratePageToken(
+ self.paginator.request_contents, next_start)
+ self.assertEqual(
+ self.paginator.GenerateNextPageToken(next_start), expected_page_token)
+
+ def testGenerateNextPageToken_NoStart(self):
+ """We return None if start is not provided."""
+ next_start = None
+ self.assertEqual(self.paginator.GenerateNextPageToken(next_start), None)
+
+ def testCoercePageSize(self):
+ """A valid page_size is used when provided."""
+ self.assertEqual(1, paginator.CoercePageSize(1, 5))
+
+ def testCoercePageSize_Negative(self):
+ """An exception is raised for a negative page_size."""
+ with self.assertRaises(exceptions.InputException):
+ paginator.CoercePageSize(-1, 5)
+
+ def testCoercePageSize_TooBig(self):
+ """A page_size above the max is coerced to the max."""
+ self.assertEqual(5, paginator.CoercePageSize(6, 5, 2))
+
+ def testCoercePageSize_Default(self):
+ """A default page_size different from max_size is used when provided."""
+ self.assertEqual(2, paginator.CoercePageSize(None, 5, 2))
+
+ def testCoercePageSize_NotProvided(self):
+ """max_size is used if no page_size or default_size provided."""
+ self.assertEqual(5, paginator.CoercePageSize(None, 5))
+
+ def testCoercePageSize_Zero(self):
+ """Handles zero equivalently to None."""
+ self.assertEqual(5, paginator.CoercePageSize(0, 5))
\ No newline at end of file
diff --git a/api/v3/test/permissions_converter_test.py b/api/v3/test/permissions_converter_test.py
new file mode 100644
index 0000000..e679eb6
--- /dev/null
+++ b/api/v3/test/permissions_converter_test.py
@@ -0,0 +1,44 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+"""Tests for converting permission strings to API permissions enums."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from api.v3 import permission_converters as pc
+from api.v3.api_proto import permission_objects_pb2
+from framework import exceptions
+from framework import permissions
+
+
+class ConverterFunctionsTest(unittest.TestCase):
+
+ def testConvertHotlistPermissions(self):
+ api_perms = pc.ConvertHotlistPermissions(
+ [permissions.ADMINISTER_HOTLIST, permissions.EDIT_HOTLIST])
+ expected_perms = [
+ permission_objects_pb2.Permission.Value('HOTLIST_ADMINISTER'),
+ permission_objects_pb2.Permission.Value('HOTLIST_EDIT')
+ ]
+ self.assertEqual(api_perms, expected_perms)
+
+ def testConvertHotlistPermissions_InvalidPermission(self):
+ with self.assertRaises(exceptions.InputException):
+ pc.ConvertHotlistPermissions(['EatHotlist'])
+
+ def testConvertFieldDefPermissions(self):
+ api_perms = pc.ConvertFieldDefPermissions(
+ [permissions.EDIT_FIELD_DEF_VALUE, permissions.EDIT_FIELD_DEF])
+ expected_perms = [
+ permission_objects_pb2.Permission.Value('FIELD_DEF_VALUE_EDIT'),
+ permission_objects_pb2.Permission.Value('FIELD_DEF_EDIT')
+ ]
+ self.assertEqual(api_perms, expected_perms)
+
+ def testConvertFieldDefPermissions_InvalidPermission(self):
+ with self.assertRaises(exceptions.InputException):
+ pc.ConvertFieldDefPermissions(['EatFieldDef'])
diff --git a/api/v3/test/permissions_servicer_test.py b/api/v3/test/permissions_servicer_test.py
new file mode 100644
index 0000000..076bd40
--- /dev/null
+++ b/api/v3/test/permissions_servicer_test.py
@@ -0,0 +1,105 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+"""Tests for the permissions servicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from api.v3 import permission_converters as pc
+from api.v3 import permissions_servicer
+from api.v3.api_proto import permissions_pb2
+from api.v3.api_proto import permission_objects_pb2
+from framework import exceptions
+from framework import monorailcontext
+from framework import permissions
+from testing import fake
+from services import features_svc
+from services import service_manager
+
+
+class PermissionsServicerTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ features=fake.FeaturesService(),
+ issue=fake.IssueService(),
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService())
+ self.project = self.services.project.TestAddProject(
+ 'proj', project_id=789, committer_ids=[111])
+ self.permissions_svcr = permissions_servicer.PermissionsServicer(
+ self.services, make_rate_limiter=False)
+ self.user_1 = self.services.user.TestAddUser('goose_1@example.com', 111)
+ self.hotlist_1 = self.services.features.TestAddHotlist(
+ 'ThingsToBreak', owner_ids=[self.user_1.user_id])
+ self.services.config.CreateFieldDef(
+ self.cnxn, self.project.project_id, 'Field_1', 'STR_TYPE', None, None,
+ None, None, None, None, None, None, None, None, None, None, None, None,
+ [], [])
+ self.config = self.services.config.GetProjectConfig(
+ self.cnxn, self.project.project_id)
+
+ def CallWrapped(self, wrapped_handler, *args, **kwargs):
+ return wrapped_handler.wrapped(self.permissions_svcr, *args, **kwargs)
+
+ def testBatchGetPermissionSets_Hotlist(self):
+ """We can batch get PermissionSets for hotlists."""
+ hotlist_1_name = 'hotlists/%s' % self.hotlist_1.hotlist_id
+ request = permissions_pb2.BatchGetPermissionSetsRequest(
+ names=[hotlist_1_name])
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+ response = self.CallWrapped(
+ self.permissions_svcr.BatchGetPermissionSets, mc, request)
+
+ expected_permission_sets = [
+ permission_objects_pb2.PermissionSet(
+ resource=hotlist_1_name,
+ permissions=[
+ permission_objects_pb2.Permission.Value('HOTLIST_ADMINISTER'),
+ permission_objects_pb2.Permission.Value('HOTLIST_EDIT'),
+ ])
+ ]
+ self.assertEqual(
+ response,
+ permissions_pb2.BatchGetPermissionSetsResponse(
+ permission_sets=expected_permission_sets))
+
+ def testBatchGetPermissionSets_FieldDef(self):
+ """We can batch get PermissionSets for fields."""
+ field = self.config.field_defs[0]
+ field_1_name = 'projects/%s/fieldDefs/%s' % (
+ self.project.project_name, field.field_id)
+ request = permissions_pb2.BatchGetPermissionSetsRequest(
+ names=[field_1_name])
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(self.project)
+ response = self.CallWrapped(
+ self.permissions_svcr.BatchGetPermissionSets, mc, request)
+
+ expected_permission_sets = [
+ permission_objects_pb2.PermissionSet(
+ resource=field_1_name,
+ permissions=[
+ permission_objects_pb2.Permission.Value('FIELD_DEF_VALUE_EDIT'),
+ ])
+ ]
+ self.assertEqual(
+ response,
+ permissions_pb2.BatchGetPermissionSetsResponse(
+ permission_sets=expected_permission_sets))
+
+ # Each case of recognized resource name is tested in testBatchGetPermissions.
+ def testGetPermissionSet_InvalidName(self):
+ """We raise exception when the resource name is unrecognized."""
+ we = None
+ with self.assertRaises(exceptions.InputException):
+ self.permissions_svcr._GetPermissionSet(self.cnxn, we, 'goose/honk')
diff --git a/api/v3/test/projects_servicer_test.py b/api/v3/test/projects_servicer_test.py
new file mode 100644
index 0000000..83aa8ab
--- /dev/null
+++ b/api/v3/test/projects_servicer_test.py
@@ -0,0 +1,245 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+"""Tests for the hotlists servicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+import mock
+import logging
+
+from google.protobuf import timestamp_pb2
+from google.protobuf import empty_pb2
+
+from api import resource_name_converters as rnc
+from api.v3 import projects_servicer
+from api.v3 import converters
+from api.v3.api_proto import projects_pb2
+from api.v3.api_proto import project_objects_pb2
+from api.v3.api_proto import issue_objects_pb2
+from framework import exceptions
+from framework import monorailcontext
+from framework import permissions
+from testing import fake
+from services import service_manager
+
+from google.appengine.ext import testbed
+
+class ProjectsServicerTest(unittest.TestCase):
+
+ def setUp(self):
+ # memcache and datastore needed for generating page tokens.
+ self.testbed = testbed.Testbed()
+ self.testbed.activate()
+ self.testbed.init_memcache_stub()
+ self.testbed.init_datastore_v3_stub()
+
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ features=fake.FeaturesService(),
+ issue=fake.IssueService(),
+ project=fake.ProjectService(),
+ config=fake.ConfigService(),
+ user=fake.UserService(),
+ template=fake.TemplateService(),
+ usergroup=fake.UserGroupService())
+ self.projects_svcr = projects_servicer.ProjectsServicer(
+ self.services, make_rate_limiter=False)
+
+ self.user_1 = self.services.user.TestAddUser('user_111@example.com', 111)
+
+ self.project_1 = self.services.project.TestAddProject(
+ 'proj', project_id=789)
+ self.template_1 = self.services.template.TestAddIssueTemplateDef(
+ 123, 789, 'template_1_name', content='foo bar', summary='foo')
+ self.project_1_resource_name = 'projects/proj'
+ self.converter = None
+
+ def CallWrapped(self, wrapped_handler, mc, *args, **kwargs):
+ self.converter = converters.Converter(mc, self.services)
+ self.projects_svcr.converter = self.converter
+ return wrapped_handler.wrapped(self.projects_svcr, mc, *args, **kwargs)
+
+ def testListIssueTemplates(self):
+ request = projects_pb2.ListIssueTemplatesRequest(
+ parent=self.project_1_resource_name)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ response = self.CallWrapped(
+ self.projects_svcr.ListIssueTemplates, mc, request)
+
+ expected_issue = issue_objects_pb2.Issue(
+ summary=self.template_1.summary,
+ state=issue_objects_pb2.IssueContentState.Value('ACTIVE'),
+ status=issue_objects_pb2.Issue.StatusValue(
+ status=self.template_1.status,
+ derivation=issue_objects_pb2.Derivation.Value('EXPLICIT')))
+ expected_template = project_objects_pb2.IssueTemplate(
+ name='projects/{}/templates/{}'.format(
+ self.project_1.project_name, self.template_1.template_id),
+ display_name=self.template_1.name,
+ issue=expected_issue,
+ summary_must_be_edited=False,
+ template_privacy=project_objects_pb2.IssueTemplate.TemplatePrivacy
+ .Value('PUBLIC'),
+ default_owner=project_objects_pb2.IssueTemplate.DefaultOwner.Value(
+ 'DEFAULT_OWNER_UNSPECIFIED'),
+ component_required=False)
+
+ self.assertEqual(
+ response,
+ projects_pb2.ListIssueTemplatesResponse(templates=[expected_template]))
+
+ @mock.patch('api.v3.api_constants.MAX_COMPONENTS_PER_PAGE', 3)
+ def testListComponentDefs(self):
+ project = self.services.project.TestAddProject(
+ 'greece', project_id=987, owner_ids=[self.user_1.user_id])
+ config = fake.MakeTestConfig(project.project_id, [], [])
+ cd_1 = fake.MakeTestComponentDef(project.project_id, 1, path='Circe')
+ cd_2 = fake.MakeTestComponentDef(project.project_id, 2, path='Achilles')
+ cd_3 = fake.MakeTestComponentDef(project.project_id, 3, path='Patroclus')
+ cd_4 = fake.MakeTestComponentDef(project.project_id, 3, path='Galatea')
+ config.component_defs = [cd_1, cd_2, cd_3, cd_4]
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+
+ request = projects_pb2.ListComponentDefsRequest(parent='projects/greece')
+ response_1 = self.CallWrapped(
+ self.projects_svcr.ListComponentDefs, mc, request)
+ expected_cds_1 = self.converter.ConvertComponentDefs(
+ [cd_1, cd_2, cd_3], project.project_id)
+ self.assertEqual(list(response_1.component_defs), expected_cds_1)
+
+ request = projects_pb2.ListComponentDefsRequest(
+ parent='projects/greece', page_token=response_1.next_page_token)
+ response_2 = self.CallWrapped(
+ self.projects_svcr.ListComponentDefs, mc, request)
+ expected_cds_2 = self.converter.ConvertComponentDefs(
+ [cd_4], project.project_id)
+ self.assertEqual(list(response_2.component_defs), expected_cds_2)
+
+ @mock.patch('api.v3.api_constants.MAX_COMPONENTS_PER_PAGE', 2)
+ def testListComponentDefs_PaginateAndMaxSizeCap(self):
+ project = self.services.project.TestAddProject(
+ 'greece', project_id=987, owner_ids=[self.user_1.user_id])
+ config = fake.MakeTestConfig(project.project_id, [], [])
+ cd_1 = fake.MakeTestComponentDef(project.project_id, 1, path='Circe')
+ cd_2 = fake.MakeTestComponentDef(project.project_id, 2, path='Achilles')
+ cd_3 = fake.MakeTestComponentDef(project.project_id, 3, path='Patroclus')
+ cd_4 = fake.MakeTestComponentDef(project.project_id, 4, path='Galatea')
+ cd_5 = fake.MakeTestComponentDef(project.project_id, 5, path='Briseis')
+ config.component_defs = [cd_1, cd_2, cd_3, cd_4, cd_5]
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+
+ request = projects_pb2.ListComponentDefsRequest(
+ parent='projects/greece', page_size=3)
+ response_1 = self.CallWrapped(
+ self.projects_svcr.ListComponentDefs, mc, request)
+ expected_cds_1 = self.converter.ConvertComponentDefs(
+ [cd_1, cd_2], project.project_id)
+ self.assertEqual(list(response_1.component_defs), expected_cds_1)
+
+ request = projects_pb2.ListComponentDefsRequest(
+ parent='projects/greece', page_size=3,
+ page_token=response_1.next_page_token)
+ response_2 = self.CallWrapped(
+ self.projects_svcr.ListComponentDefs, mc, request)
+ expected_cds_2 = self.converter.ConvertComponentDefs(
+ [cd_3, cd_4], project.project_id)
+ self.assertEqual(list(response_2.component_defs), expected_cds_2)
+
+ request = projects_pb2.ListComponentDefsRequest(
+ parent='projects/greece', page_size=3,
+ page_token=response_2.next_page_token)
+ response_3 = self.CallWrapped(
+ self.projects_svcr.ListComponentDefs, mc, request)
+ expected_cds_3 = self.converter.ConvertComponentDefs(
+ [cd_5], project.project_id)
+ self.assertEqual(response_3, projects_pb2.ListComponentDefsResponse(
+ component_defs=expected_cds_3))
+
+ @mock.patch('time.time')
+ def testCreateComponentDef(self, mockTime):
+ now = 123
+ mockTime.return_value = now
+
+ user_1 = self.services.user.TestAddUser('achilles@test.com', 981)
+ self.services.user.TestAddUser('patroclus@test.com', 982)
+ self.services.user.TestAddUser('circe@test.com', 983)
+
+ project = self.services.project.TestAddProject(
+ 'chicken', project_id=987, owner_ids=[user_1.user_id])
+ config = fake.MakeTestConfig(project.project_id, [], [])
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ expected = project_objects_pb2.ComponentDef(
+ value='circe',
+ docstring='You threw me to the crows',
+ admins=['users/983'],
+ ccs=['users/981', 'users/982'],
+ labels=['more-soup', 'beach-day'],
+ )
+ request = projects_pb2.CreateComponentDefRequest(
+ parent='projects/chicken', component_def=expected)
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=user_1.email)
+ response = self.CallWrapped(
+ self.projects_svcr.CreateComponentDef, mc, request)
+
+ self.assertEqual(1, len(config.component_defs))
+ expected.name = 'projects/chicken/componentDefs/%d' % config.component_defs[
+ 0].component_id
+ expected.state = project_objects_pb2.ComponentDef.ComponentDefState.Value(
+ 'ACTIVE')
+ expected.creator = 'users/981'
+ expected.create_time.FromSeconds(now)
+ expected.modify_time.FromSeconds(0)
+ self.assertEqual(response, expected)
+
+ def testDeleteComponentDef(self):
+ user_1 = self.services.user.TestAddUser('achilles@test.com', 981)
+ project = self.services.project.TestAddProject(
+ 'chicken', project_id=987, owner_ids=[user_1.user_id])
+ config = fake.MakeTestConfig(project.project_id, [], [])
+ component_def = fake.MakeTestComponentDef(
+ project.project_id, 1, path='Chickens>Dickens')
+ config.component_defs = [component_def]
+ self.services.config.StoreConfig(self.cnxn, config)
+
+ request = projects_pb2.DeleteComponentDefRequest(
+ name='projects/chicken/componentDefs/1')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=user_1.email)
+ actual = self.CallWrapped(
+ self.projects_svcr.DeleteComponentDef, mc, request)
+ self.assertEqual(actual, empty_pb2.Empty())
+
+ self.assertEqual(config.component_defs, [])
+
+ @mock.patch('project.project_helpers.GetThumbnailUrl')
+ def testListProjects(self, mock_GetThumbnailUrl):
+ mock_GetThumbnailUrl.return_value = 'xyz'
+
+ request = projects_pb2.ListProjectsRequest()
+
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ response = self.CallWrapped(self.projects_svcr.ListProjects, mc, request)
+
+ expected_project = project_objects_pb2.Project(
+ name=self.project_1_resource_name,
+ display_name=self.project_1.project_name,
+ summary=self.project_1.summary,
+ thumbnail_url='xyz')
+
+ self.assertEqual(
+ response,
+ projects_pb2.ListProjectsResponse(projects=[expected_project]))
diff --git a/api/v3/test/users_servicer_test.py b/api/v3/test/users_servicer_test.py
new file mode 100644
index 0000000..8982ec9
--- /dev/null
+++ b/api/v3/test/users_servicer_test.py
@@ -0,0 +1,136 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+"""Tests for the users servicer."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+import mock
+
+from google.protobuf import empty_pb2
+
+from api import resource_name_converters as rnc
+from api.v3 import users_servicer
+from api.v3 import converters
+from api.v3.api_proto import users_pb2
+from api.v3.api_proto import user_objects_pb2
+from framework import exceptions
+from framework import monorailcontext
+from framework import permissions
+from testing import fake
+from testing import testing_helpers
+from services import features_svc
+from services import user_svc
+from services import service_manager
+
+
+class UsersServicerTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cnxn = fake.MonorailConnection()
+ self.services = service_manager.Services(
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService(),
+ project=fake.ProjectService(),
+ project_star=fake.ProjectStarService())
+ self.users_svcr = users_servicer.UsersServicer(
+ self.services, make_rate_limiter=False)
+
+ self.user_1 = self.services.user.TestAddUser('user_111@example.com', 111)
+ self.user_2 = self.services.user.TestAddUser('user_222@example.com', 222)
+ self.user_3 = self.services.user.TestAddUser('user_333@example.com', 333)
+
+ self.project_1 = self.services.project.TestAddProject(
+ 'proj', project_id=789)
+
+ self.converter = None
+
+ def CallWrapped(self, wrapped_handler, mc, *args, **kwargs):
+ self.converter = converters.Converter(mc, self.services)
+ self.users_svcr.converter = self.converter
+ return wrapped_handler.wrapped(self.users_svcr, mc, *args, **kwargs)
+
+ def testGetUser(self):
+ request = users_pb2.GetUserRequest(name='users/222')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+ response = self.CallWrapped(self.users_svcr.GetUser, mc, request)
+ expected_response = user_objects_pb2.User(
+ name='users/222',
+ display_name=testing_helpers.ObscuredEmail(self.user_2.email),
+ email=testing_helpers.ObscuredEmail(self.user_2.email),
+ availability_message='User never visited')
+ self.assertEqual(response, expected_response)
+
+ def testBatchGetUsers(self):
+ request = users_pb2.BatchGetUsersRequest(
+ names=['users/222', 'users/333'])
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+ response = self.CallWrapped(self.users_svcr.BatchGetUsers, mc, request)
+ expected_users = [
+ user_objects_pb2.User(
+ name='users/222',
+ display_name=testing_helpers.ObscuredEmail(self.user_2.email),
+ email=testing_helpers.ObscuredEmail(self.user_2.email),
+ availability_message='User never visited'),
+ user_objects_pb2.User(
+ name='users/333',
+ display_name=testing_helpers.ObscuredEmail(self.user_3.email),
+ email=testing_helpers.ObscuredEmail(self.user_3.email),
+ availability_message='User never visited')
+ ]
+ self.assertEqual(
+ response, users_pb2.BatchGetUsersResponse(users=expected_users))
+
+ @mock.patch('api.v3.api_constants.MAX_BATCH_USERS', 2)
+ def testBatchGetUsers_TooMany(self):
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ request = users_pb2.BatchGetUsersRequest(
+ names=['users/222', 'users/333', 'users/444'])
+ with self.assertRaises(exceptions.InputException):
+ self.CallWrapped(self.users_svcr.BatchGetUsers, mc, request)
+
+ def testStarProject(self):
+ request = users_pb2.StarProjectRequest(project='projects/proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+ response = self.CallWrapped(self.users_svcr.StarProject, mc, request)
+ expected_name = 'users/111/projectStars/proj'
+
+ self.assertEqual(response, user_objects_pb2.ProjectStar(name=expected_name))
+
+ def testUnStarProject(self):
+ request = users_pb2.UnStarProjectRequest(project='projects/proj')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+ response = self.CallWrapped(self.users_svcr.UnStarProject, mc, request)
+
+ self.assertEqual(response, empty_pb2.Empty())
+
+ is_starred = self.services.project_star.IsItemStarredBy(self.cnxn, 789, 111)
+ self.assertFalse(is_starred)
+
+ def testListProjectStars(self):
+ request = users_pb2.ListProjectStarsRequest(parent='users/111')
+ mc = monorailcontext.MonorailContext(
+ self.services, cnxn=self.cnxn, requester=self.user_1.email)
+ mc.LookupLoggedInUserPerms(None)
+
+ self.services.project_star.SetStar(
+ self.cnxn, self.project_1.project_id, self.user_1.user_id, True)
+
+ response = self.CallWrapped(self.users_svcr.ListProjectStars, mc, request)
+
+ expected_response = users_pb2.ListProjectStarsResponse(
+ project_stars=[
+ user_objects_pb2.ProjectStar(name='users/111/projectStars/proj')
+ ])
+ self.assertEqual(response, expected_response)
diff --git a/api/v3/users_servicer.py b/api/v3/users_servicer.py
new file mode 100644
index 0000000..cbf70c5
--- /dev/null
+++ b/api/v3/users_servicer.py
@@ -0,0 +1,127 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from google.protobuf import empty_pb2
+
+from api import resource_name_converters as rnc
+from api.v3 import monorail_servicer
+from api.v3 import api_constants
+from api.v3.api_proto import users_pb2
+from api.v3.api_proto import user_objects_pb2
+from api.v3.api_proto import users_prpc_pb2
+from businesslogic import work_env
+from framework import exceptions
+
+
+class UsersServicer(monorail_servicer.MonorailServicer):
+ """Handle API requests related to User objects.
+ Each API request is implemented with a method as defined in the
+ .proto file. Each method does any request-specific validation, uses work_env
+ to safely operate on business objects, and returns a response proto.
+ """
+
+ DESCRIPTION = users_prpc_pb2.UsersServiceDescription
+
+ @monorail_servicer.PRPCMethod
+ def GetUser(self, mc, request):
+ # type: (MonorailContext, GetUserRequest) ->
+ # GetUserResponse
+ """pRPC API method that implements GetUser.
+
+ Raises:
+ InputException if a name in request.name is invalid.
+ NoSuchUserException if a User is not found.
+ """
+ user_id = rnc.IngestUserName(mc.cnxn, request.name, self.services)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ user = we.GetUser(user_id)
+
+ return self.converter.ConvertUser(user)
+
+ @monorail_servicer.PRPCMethod
+ def BatchGetUsers(self, mc, request):
+ # type: (MonorailContext, BatchGetUsersRequest) ->
+ # BatchGetUsersResponse
+ """pRPC API method that implements BatchGetUsers.
+
+ Raises:
+ InputException if a name in request.names is invalid.
+ NoSuchUserException if a User is not found.
+ """
+ if len(request.names) > api_constants.MAX_BATCH_USERS:
+ raise exceptions.InputException(
+ 'Requesting %d users when the allowed maximum is %d users.' %
+ (len(request.names), api_constants.MAX_BATCH_USERS))
+ user_ids = rnc.IngestUserNames(mc.cnxn, request.names, self.services)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ users = we.BatchGetUsers(user_ids)
+
+ api_users_by_id = self.converter.ConvertUsers(
+ [user.user_id for user in users])
+ api_users = [api_users_by_id[user_id] for user_id in user_ids]
+
+ return users_pb2.BatchGetUsersResponse(users=api_users)
+
+ @monorail_servicer.PRPCMethod
+ def StarProject(self, mc, request):
+ # type: (MonorailContext, StarProjectRequest) ->
+ # ProjectStar
+ """pRPC API method that implements StarProject.
+
+ Raises:
+ InputException if the project name in request.project is invalid.
+ NoSuchProjectException if no project exists with the given name.
+ """
+ project_id = rnc.IngestProjectName(mc.cnxn, request.project, self.services)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.StarProject(project_id, True)
+
+ user_id = mc.auth.user_id
+ star_name = rnc.ConvertProjectStarName(
+ mc.cnxn, user_id, project_id, self.services)
+
+ return user_objects_pb2.ProjectStar(name=star_name)
+
+ @monorail_servicer.PRPCMethod
+ def UnStarProject(self, mc, request):
+ # type: (MonorailContext, UnStarProjectRequest) ->
+ # Empty
+ """pRPC API method that implements UnStarProject.
+
+ Raises:
+ InputException if the project name in request.project is invalid.
+ NoSuchProjectException if no project exists with the given name.
+ """
+ project_id = rnc.IngestProjectName(mc.cnxn, request.project, self.services)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ we.StarProject(project_id, False)
+
+ return empty_pb2.Empty()
+
+ @monorail_servicer.PRPCMethod
+ def ListProjectStars(self, mc, request):
+ # type: (MonorailContext, ListProjectStarsRequest) ->
+ # ListProjectStarsResponse
+ """pRPC API method that implements ListProjectStars.
+
+ Raises:
+ InputException: if the `page_token` or `parent` is invalid.
+ NoSuchUserException: if the User is not found.
+ """
+ user_id = rnc.IngestUserName(mc.cnxn, request.parent, self.services)
+
+ with work_env.WorkEnv(mc, self.services) as we:
+ projects = we.ListStarredProjects(user_id)
+
+ # TODO(crbug.com/monorail/7175): Add pagination logic.
+ return users_pb2.ListProjectStarsResponse(
+ project_stars=self.converter.ConvertProjectStars(user_id, projects))
diff --git a/api/v3_test_call.py b/api/v3_test_call.py
new file mode 100644
index 0000000..6d5b01a
--- /dev/null
+++ b/api/v3_test_call.py
@@ -0,0 +1,107 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+#!/usr/bin/env python
+"""
+This script requires `google-auth` 1.15.0 or higher.
+To update this for monorail's third_party run the following from
+monorail/third_party/google:
+bash ./update.sh 1.15.0
+
+This is an example of how a script might make calls to monorail's v3 pRPC API.
+
+Usage example:
+```
+python v3_test_call.py \
+monorail.v3.Issues GetIssue '{"name": "projects/monorail/issues/404"}'
+```
+
+The email of your service account should be allow-listed with Monorail.
+"""
+
+import argparse
+import json
+import logging
+import os
+import sys
+import requests
+
+monorail_dir = os.path.dirname(os.path.abspath(__file__ + '/..'))
+third_party_path = os.path.join(monorail_dir, 'third_party')
+if third_party_path not in sys.path:
+ sys.path.insert(0, third_party_path)
+
+# Older versions of https://github.com/googleapis/google-auth-library-python
+# do not have the fetch_id_token() method called below.
+# v1.15.0 or later should be fine.
+from google.oauth2 import id_token
+from google.auth.transport import requests as google_requests
+
+# Download and save your service account credentials file in
+# api/service-account-key.json.
+# id_token.fetch_id_token looks inside GOOGLE_APPLICATION_CREDENTIALS to fetch
+# service account credentials.
+os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = 'service-account-key.json'
+
+# BASE_URL can point to any monorail-dev api service version.
+# However, you MAY get ssl cert errors when BASE_URL is not the
+# default below. If this happens you will have to test your version
+# by using the existing BASE_URL and migrating all traffic to your api
+# version via pantheon.
+BASE_URL = 'https://api-dot-monorail-dev.appspot.com/prpc'
+
+# TARGET_AUDIENCE should not change as long as BASE_URL is pointing to
+# some monorail-dev version. If BASE_URL is updated to point to
+# monorail-{staging|prod}, update TARGET_AUDIENCE accordingly.
+TARGET_AUDIENCE = 'https://monorail-dev.appspot.com'
+
+# XSSI_PREFIX found at the beginning of every prpc response.
+XSSI_PREFIX = ")]}'\n"
+
+import httplib2
+from oauth2client.client import GoogleCredentials
+
+
+def make_call(service, method, json_body):
+ # Fetch ID token
+ request = google_requests.Request()
+ token = id_token.fetch_id_token(request, TARGET_AUDIENCE)
+ # Note: ID tokens for service accounts can also be fetched with with the
+ # Cloud IAM API projects.serviceAccounts.generateIdToken
+ # generateIdToken only needs the service account email or ID and the
+ # target_audience.
+
+ # Call monorail's API.
+ headers = {
+ 'Authorization': 'Bearer %s' % token,
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ }
+
+ url = "%s/%s/%s" % (BASE_URL, service, method)
+
+ body = json.loads(json_body)
+ resp = requests.post(url, data=json.dumps(body), headers=headers)
+ logging.info(resp)
+ logging.info(resp.text)
+ logging.info(resp.content)
+ logging.info(json.dumps(json.loads(resp.content[len(XSSI_PREFIX):])))
+
+ # Verify and decode ID token to take a look at what's inside.
+ # API users should not have to do this. This is just for learning about
+ # how ID tokens work.
+ request = google_requests.Request()
+ id_info = id_token.verify_oauth2_token(token, request)
+ logging.info('id_info %s' % id_info)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description='Process some integers.')
+ parser.add_argument('service', help='pRPC service name.')
+ parser.add_argument('method', help='pRPC method name.')
+ parser.add_argument('json_body', help='pRPC HTTP body in valid JSON.')
+ args = parser.parse_args()
+ log_level = logging.INFO
+ logging.basicConfig(format='%(levelname)s: %(message)s', level=log_level)
+ make_call(args.service, args.method, args.json_body)