From e5a536a523c1ef9f6f187586a40e0dd96fb5389e Mon Sep 17 00:00:00 2001 From: jeremydw Date: Mon, 8 Jun 2015 15:29:04 -0700 Subject: [PATCH 01/46] Start buildbot integration. --- jetway/projects/messages.py | 4 ++++ jetway/projects/projects.py | 9 +++++++++ jetway/strings/__init__.py | 0 jetway/strings/memories.py | 5 +++++ jetway/strings/strings.py | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 54 insertions(+) create mode 100644 jetway/strings/__init__.py create mode 100644 jetway/strings/memories.py create mode 100644 jetway/strings/strings.py diff --git a/jetway/projects/messages.py b/jetway/projects/messages.py index 4e6c1a5..c173df4 100644 --- a/jetway/projects/messages.py +++ b/jetway/projects/messages.py @@ -30,6 +30,10 @@ class ProjectMessage(messages.Message): cover = messages.MessageField(CoverMessage, 8) +class RepoMessage(messages.Message): + url = messages.StringField(1) + + ### diff --git a/jetway/projects/projects.py b/jetway/projects/projects.py index 648001c..1b4a27f 100644 --- a/jetway/projects/projects.py +++ b/jetway/projects/projects.py @@ -45,6 +45,8 @@ class Project(ndb.Model): cover = ndb.StructuredProperty(Cover) visibility = msgprop.EnumProperty(messages.Visibility, default=messages.Visibility.PRIVATE) + repo = ndb.MessageProperty(messages.RepoMessage) + known_by_buildbot = ndb.BooleanField(default=False) def __repr__(self): return '{}/{}'.format(self.owner.nickname, self.nickname) @@ -53,6 +55,13 @@ def __repr__(self): def ident(self): return str(self.key.id()) + @classmethod + def search_unknown_by_buildbot(cls): + query = cls.query() + query = query.filter(cls.known_by_buildbot == False) + results = query.fetch() + return results + @classmethod def create(cls, owner, nickname, created_by, description=None): try: diff --git a/jetway/strings/__init__.py b/jetway/strings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/jetway/strings/memories.py b/jetway/strings/memories.py new file mode 100644 index 0000000..847f96a --- /dev/null +++ b/jetway/strings/memories.py @@ -0,0 +1,5 @@ +from google.appengine.ext import ndb + + +class Memory(ndb.Model): + name = ndb.StringProperty() diff --git a/jetway/strings/strings.py b/jetway/strings/strings.py new file mode 100644 index 0000000..d2e9f54 --- /dev/null +++ b/jetway/strings/strings.py @@ -0,0 +1,36 @@ +from google.appengine.ext import ndb + + +class Locale(ndb.Model): + lang = ndb.StringProperty() + region = ndb.StringProperty() + + +class Flags(ndb.Model): + fuzzy = ndb.BooleanProperty() + + +class (ndb.Expando): + en = ndb.StringProperty() + lang = ndb.StringProperty() + translator_comments = ndb.TextProperty() + extracted_comments = ndb.TextProperty() + reference = ndb.TextProperty() + flags = ndb.StructuredProperty(Flags) + previous_untranslated_string = ndb.StringProperty() + + +class String(ndb.Model): + locale = ndb.StructuredProperty(Locale) + source = ndb.StructuredProperty(Translation) + created = ndb.DateTimeProperty(auto_now_add=True) + modified = ndb.DateTimeProperty(auto_now=True) + memory_key = ndb.KeyProperty() + + def search(cls, query_message): + query = cls.query() + query.fetch() + query = query.filter(cls.form.agree_to_share_with_fulfiller == True) + per_page = query_message.per_page if query_message else 50 + results, next_cursor, has_more = query.fetch_page(per_page, start_cursor=cursor) + return (results, next_cursor, has_more) From b7189be1bfb4601bd3787ccd101b0a4e3e09e2da Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Wed, 26 Aug 2015 09:59:37 -0700 Subject: [PATCH 02/46] Initial work on domain visibility. --- appengine_config.py | 3 +++ jetway/main.py | 5 +++++ jetway/projects/messages.py | 1 + jetway/projects/projects.py | 14 ++++++++++---- jetway/server/handlers.py | 9 ++++++++- requirements.txt | 2 +- 6 files changed, 28 insertions(+), 6 deletions(-) diff --git a/appengine_config.py b/appengine_config.py index f0c40a9..27ea31a 100644 --- a/appengine_config.py +++ b/appengine_config.py @@ -7,6 +7,9 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib')) mimetypes.add_type('image/svg+xml', '.svg') +mimetypes.add_type('font/opentype', '.otf') +mimetypes.add_type('font/ttf', '.ttf') +mimetypes.add_type('font/woff', '.woff') if 'JETWAY_CONFIG' in os.environ: _config_path = os.getenv('JETWAY_CONFIG') diff --git a/jetway/main.py b/jetway/main.py index e6ef797..eb74873 100644 --- a/jetway/main.py +++ b/jetway/main.py @@ -78,6 +78,11 @@ def middleware(environ, start_response): # But, require users to be signed in. Use App Engine sign in to avoid # building an SSO login system for each preview domain. user = users.get_current_user() + # TODO: Remove dirty hack to get around issues with Chrome Beta and + # Firefox. Note that this exposes all ttf and woff files. + # http://stackoverflow.com/questions/31140826 + if environ['PATH_INFO'].endswith(('.ttf', '.woff')): + return app(environ, start_response) if utils.is_preview_server(environ['SERVER_NAME']): if user is None: url = users.create_login_url(environ['PATH_INFO']) diff --git a/jetway/projects/messages.py b/jetway/projects/messages.py index c60ea0b..c828201 100644 --- a/jetway/projects/messages.py +++ b/jetway/projects/messages.py @@ -14,6 +14,7 @@ class Visibility(messages.Enum): ORGANIZATION = 2 PRIVATE = 3 COVER = 4 + DOMAIN = 5 class CoverMessage(messages.Message): diff --git a/jetway/projects/projects.py b/jetway/projects/projects.py index 7369acc..05e614b 100644 --- a/jetway/projects/projects.py +++ b/jetway/projects/projects.py @@ -46,8 +46,7 @@ class Project(ndb.Model): created_by_key = ndb.KeyProperty() description = ndb.StringProperty() cover = ndb.StructuredProperty(Cover) - visibility = msgprop.EnumProperty(messages.Visibility, - default=messages.Visibility.PRIVATE) + visibility = msgprop.EnumProperty(messages.Visibility) built = ndb.DateTimeProperty() @property @@ -232,13 +231,19 @@ def can(self, user, permission=messages.Permission.READ): if (appengine_config.DEFAULT_USER_DOMAINS and domain in appengine_config.DEFAULT_USER_DOMAINS): return True + # Permit the owner. + if self.owner == user: + return True + # Permit domain users. + if (appengine_config.DEFUALT_USER_DOMAINS + and self.visibility == messages.Visibility.DOMAIN + and user.email.split('@')[-1] in appengine_config.DEFAULT_USER_DOMAINS): + return if self.visibility in [messages.Visibility.PUBLIC, messages.Visibility.COVER]: if appengine_config.DEFAULT_USER_DOMAINS: return domain in appengine_config.DEFAULT_USER_DOMAINS else: return True - if self.owner == user: - return True query = teams.Team.query(ndb.OR(# Project teams. ndb.AND(teams.Team.project_keys == self.key, teams.Team.user_keys == user.key), @@ -247,6 +252,7 @@ def can(self, user, permission=messages.Permission.READ): teams.Team.owner_key == self.owner.key, teams.Team.user_keys == user.key))) found_teams = query.fetch() + # Permit users part of the project's organization. if self.visibility == messages.Visibility.ORGANIZATION: return bool(found_teams) if self.visibility == messages.Visibility.PRIVATE: diff --git a/jetway/server/handlers.py b/jetway/server/handlers.py index 518f66a..b36fc89 100644 --- a/jetway/server/handlers.py +++ b/jetway/server/handlers.py @@ -15,6 +15,12 @@ class RequestHandler(auth_handlers.SessionHandler): + def _is_open_path(self, path): + # TODO: Remove dirty hack to get around issues with Chrome Beta and + # Firefox. Note that this exposes all ttf and woff files. + # http://stackoverflow.com/questions/31140826 + return path.endswith(('.woff', '.ttf')) + def error(self, status, title, message): template = _env.get_template('error.html') html = template.render({ @@ -45,7 +51,8 @@ def get(self): if fileset_name is None: raise filesets.FilesetDoesNotExistError fileset = filesets.Fileset.get_by_name_or_ident(fileset_name) - if not fileset.project.can(self.me, projects.Permission.READ): + if (not self._is_open_path(self.request.path) + and not fileset.project.can(self.me, projects.Permission.READ)): if self.me: text = '{} does not have access to this page.'.format(self.me) self.error(403, 'Forbidden', text) diff --git a/requirements.txt b/requirements.txt index 5884e8e..31ba9fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ protorpc-standalone==0.9.1 pycrypto==2.6 pydenticon==0.1.1 -Pillow +PIL # premailer. premailer==2.8.1 From 13bbb1a0fe58bc66d64f3cda08337c8d722fa747 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Mon, 31 Aug 2015 11:59:32 -0700 Subject: [PATCH 03/46] Domains fix. --- index.yaml | 7 +++++++ jetway/projects/projects.py | 2 +- scripts/deploy | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/index.yaml b/index.yaml index e13a19f..cf8d264 100644 --- a/index.yaml +++ b/index.yaml @@ -40,3 +40,10 @@ indexes: - name: owner_key - name: nickname direction: desc + +- kind: Fileset + properties: + - name: name + - name: project_key + - name: modified + direction: desc diff --git a/jetway/projects/projects.py b/jetway/projects/projects.py index 05e614b..8a3bea7 100644 --- a/jetway/projects/projects.py +++ b/jetway/projects/projects.py @@ -235,7 +235,7 @@ def can(self, user, permission=messages.Permission.READ): if self.owner == user: return True # Permit domain users. - if (appengine_config.DEFUALT_USER_DOMAINS + if (appengine_config.DEFAULT_USER_DOMAINS and self.visibility == messages.Visibility.DOMAIN and user.email.split('@')[-1] in appengine_config.DEFAULT_USER_DOMAINS): return diff --git a/scripts/deploy b/scripts/deploy index c4ea7eb..e956e54 100755 --- a/scripts/deploy +++ b/scripts/deploy @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/bin/bash ./scripts/test From cf0f7a1712bc634d8e8c52b3638a45c8dd577082 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Mon, 31 Aug 2015 19:47:23 -0700 Subject: [PATCH 04/46] Visibility fix. --- README.md | 1 + app.yaml | 17 +++++++++++++---- appengine_config.py | 2 +- index.yaml | 14 +++++++------- jetway/main.py | 11 ++++++++--- jetway/main_test.py | 5 +++-- jetway/projects/messages.py | 1 - jetway/projects/projects.py | 33 ++++++++++++++++----------------- jetway/projects/services.py | 18 +++++++++--------- jetway/server/utils.py | 4 ++++ 10 files changed, 62 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index c844281..3614a93 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Run the below command and follow the on-screen instructions to install dependenc ./scripts/setup +1. The Google App Engine SDK is required. Currently, the version packaged with the gcloud SDK is not compatible with the test runner, so you'll have to download the [standalone GoogleAppEngineLauncher](https://cloud.google.com/appengine/downloads). 1. Run `./scripts/test` to verify tests pass. 1. Run `./scripts/run` to start a development server. diff --git a/app.yaml b/app.yaml index 0366637..74bb095 100644 --- a/app.yaml +++ b/app.yaml @@ -2,7 +2,12 @@ api_version: 1 runtime: python27 threadsafe: true +env_variables: + JETWAY_CONFIG: config/jetway.grow-prod.yaml + libraries: +- name: lxml + version: latest - name: endpoints version: latest - name: ssl @@ -28,16 +33,20 @@ handlers: - url: /_ah/spi/.* script: jetway.main.endpoints_app -- url: /_jetway/[^/]*/static/css +- url: /_app/[^/]*/config/(.*\.(svg|png|gif|jpg))$ + static_files: config/\1 + upload: config/.*\.(svg|png|gif|jpg)$ + +- url: /_app/[^/]*/static/css static_dir: dist/css -- url: /_jetway/[^/]*/static/js +- url: /_app/[^/]*/static/js static_dir: dist/js -- url: /_jetway/[^/]*/static/html +- url: /_app/[^/]*/static/html static_dir: jetway/frontend/static/html -- url: /_jetway/[^/]*/static/images +- url: /_app/[^/]*/static/images static_dir: jetway/frontend/static/images - url: /.* diff --git a/appengine_config.py b/appengine_config.py index 27ea31a..46cae41 100644 --- a/appengine_config.py +++ b/appengine_config.py @@ -25,7 +25,7 @@ else: DOMAIN_ACCESS_USERS = None -if os.environ.get('TESTING'): +if os.environ.get('CI'): service_account_key = json.load(open('testing/service_account_key.json')) client_secrets_path = os.path.abspath('testing/client_secrets.json') client_secrets = json.load(open(client_secrets_path)) diff --git a/index.yaml b/index.yaml index cf8d264..fcb486f 100644 --- a/index.yaml +++ b/index.yaml @@ -23,6 +23,13 @@ indexes: - name: modified direction: desc +- kind: Fileset + properties: + - name: name + - name: project_key + - name: modified + direction: desc + - kind: Fileset properties: - name: project_key @@ -40,10 +47,3 @@ indexes: - name: owner_key - name: nickname direction: desc - -- kind: Fileset - properties: - - name: name - - name: project_key - - name: modified - direction: desc diff --git a/jetway/main.py b/jetway/main.py index eb74873..be24e5b 100644 --- a/jetway/main.py +++ b/jetway/main.py @@ -83,6 +83,8 @@ def middleware(environ, start_response): # http://stackoverflow.com/questions/31140826 if environ['PATH_INFO'].endswith(('.ttf', '.woff')): return app(environ, start_response) + if utils.is_avatar_request(environ['SERVER_NAME']): + return app(environ, start_response) if utils.is_preview_server(environ['SERVER_NAME']): if user is None: url = users.create_login_url(environ['PATH_INFO']) @@ -90,16 +92,19 @@ def middleware(environ, start_response): return [] return app(environ, start_response) allowed_user_domains = config.ALLOWED_USER_DOMAINS + # TODO: We require App Engine Users API anonymous users to sign in, + # so don't continue. # If all domains are allowed, continue. - if allowed_user_domains is None: - return app(environ, start_response) + # if allowed_user_domains is None: + # return app(environ, start_response) # Redirect anonymous users to login. if user is None: url = users.create_login_url(environ['PATH_INFO']) start_response('302', [('Location', url)]) return [] # Ban forbidden users. - if user.email().split('@')[-1] not in allowed_user_domains: + if (allowed_user_domains and + user.email().split('@')[-1] not in allowed_user_domains): start_response('403', []) url = users.create_logout_url(environ['PATH_INFO']) return ['Forbidden. Sign out.'.format(url)] diff --git a/jetway/main_test.py b/jetway/main_test.py index 717a219..4c60f23 100644 --- a/jetway/main_test.py +++ b/jetway/main_test.py @@ -40,8 +40,9 @@ def test_api_app(self): req = webapp2.Request.blank('/_api/protorpc.services', headers=headers, method='POST') resp = req.get_response(main.app) - self.assertEqual(resp.status_int, 200) - self.assertIn('services', resp.json) + self.assertEqual(resp.status_int, 302) # App Engine Users API. + # TODO(jeremydw): Return 200. + # self.assertIn('services', resp.json) if __name__ == '__main__': diff --git a/jetway/projects/messages.py b/jetway/projects/messages.py index c828201..9be8b7f 100644 --- a/jetway/projects/messages.py +++ b/jetway/projects/messages.py @@ -30,7 +30,6 @@ class ProjectMessage(messages.Message): ident = messages.StringField(2) owner = messages.MessageField(owner_messages.OwnerMessage, 3) description = messages.StringField(4) - git_url = messages.StringField(5) avatar_url = messages.StringField(6) visibility = messages.EnumField(Visibility, 7) cover = messages.MessageField(CoverMessage, 8) diff --git a/jetway/projects/projects.py b/jetway/projects/projects.py index 8a3bea7..8942169 100644 --- a/jetway/projects/projects.py +++ b/jetway/projects/projects.py @@ -60,6 +60,10 @@ def name_padded(self): def __repr__(self): return self.name + @property + def computed_visibility(self): + return self.visibility or messages.Visibility.DOMAIN + @property def ident(self): return str(self.key.id()) @@ -116,14 +120,17 @@ def search(cls, owner=None, order=None): def delete(self): from jetway.launches import launches - team_results = teams.Team.search(projects=[self], kind=teams.messages.Kind.DEFAULT) + team_results = teams.Team.search( + projects=[self], + kind=teams.messages.Kind.DEFAULT) launch_results = launches.Launch.search(project=self) fileset_results = filesets.Fileset.search(project=self) @ndb.transactional(retries=1, xg=True) def _delete_project(): try: - project_team = teams.Team.get(self.ident, teams.messages.Kind.PROJECT_OWNERS) + project_team = teams.Team.get( + self.ident, teams.messages.Kind.PROJECT_OWNERS) project_team.delete() except teams.TeamDoesNotExistError: pass @@ -159,15 +166,6 @@ def get_root(self): def owner(self): return owners.Owner.get_by_key(self.owner_key) - @property - def git_url(self): - host = os.getenv('SERVER_NAME') - if os.getenv('SERVER_SOFTWARE').startswith('Dev'): - port = os.getenv('SERVER_PORT') - host = '{}:{}'.format(host, port) - scheme = os.getenv('wsgi.url_scheme') - return '{}://{}/{}/{}.git'.format(scheme, host, self.owner.nickname, self.nickname) - @property def avatar_url(self): return avatars.Avatar.create_url(self) @@ -183,7 +181,6 @@ def to_message(self): message.ident = self.ident message.owner = self.owner.to_message() message.description = self.description - message.git_url = self.git_url message.avatar_url = self.avatar_url message.visibility = self.visibility if self.cover: @@ -236,10 +233,10 @@ def can(self, user, permission=messages.Permission.READ): return True # Permit domain users. if (appengine_config.DEFAULT_USER_DOMAINS - and self.visibility == messages.Visibility.DOMAIN + and self.computed_visibility == messages.Visibility.DOMAIN and user.email.split('@')[-1] in appengine_config.DEFAULT_USER_DOMAINS): return - if self.visibility in [messages.Visibility.PUBLIC, messages.Visibility.COVER]: + if self.computed_visibility in [messages.Visibility.PUBLIC, messages.Visibility.COVER]: if appengine_config.DEFAULT_USER_DOMAINS: return domain in appengine_config.DEFAULT_USER_DOMAINS else: @@ -253,9 +250,9 @@ def can(self, user, permission=messages.Permission.READ): teams.Team.user_keys == user.key))) found_teams = query.fetch() # Permit users part of the project's organization. - if self.visibility == messages.Visibility.ORGANIZATION: + if self.computed_visibility == messages.Visibility.ORGANIZATION: return bool(found_teams) - if self.visibility == messages.Visibility.PRIVATE: + if self.computed_visibility in [messages.Visibility.PRIVATE, messages.Visibility.DOMAIN]: # TODO: Remove this once exposing default permissions in UI. if (appengine_config.DEFAULT_USER_DOMAINS and user.email.split('@')[-1] in appengine_config.DEFAULT_USER_DOMAINS): @@ -265,7 +262,9 @@ def can(self, user, permission=messages.Permission.READ): if not membership: continue # Org owners have access to all projects. - if team.kind == teams.messages.Kind.ORG_OWNERS: + if team.kind == teams.messages.Kind.PROJECT_OWNERS: + return True + elif team.kind == teams.messages.Kind.ORG_OWNERS: return True # If the user is in a team that has this project, return True. if self.key in team.project_keys: diff --git a/jetway/projects/services.py b/jetway/projects/services.py index 6b65459..9ade186 100644 --- a/jetway/projects/services.py +++ b/jetway/projects/services.py @@ -51,7 +51,7 @@ def search(self, request): def update(self, request): project = self._get_project(request) if not project.can(self.me, projects.Permission.ADMINISTER): - raise api.ForbiddenError('Forbidden.') + raise api.ForbiddenError('Forbidden ({})'.format(self.me)) project.update(request.project) resp = service_messages.UpdateProjectResponse() resp.project = project.to_message() @@ -62,7 +62,7 @@ def update(self, request): def get(self, request): project = self._get_project(request) if not project.can(self.me, projects.Permission.READ): - raise api.ForbiddenError('Forbidden.') + raise api.ForbiddenError('Forbidden ({})'.format(self.me)) resp = service_messages.GetProjectResponse() resp.project = project.to_message() return resp @@ -72,7 +72,7 @@ def get(self, request): def delete(self, request): project = self._get_project(request) if not project.can(self.me, projects.Permission.ADMINISTER): - raise api.ForbiddenError('Forbidden.') + raise api.ForbiddenError('Forbidden ({})'.format(self.me)) project.delete() resp = service_messages.DeleteProjectResponse() return resp @@ -91,7 +91,7 @@ def can(self, request): def watch(self, request): project = self._get_project(request) if not project.can(self.me, projects.Permission.READ): - raise api.ForbiddenError('Forbidden.') + raise api.ForbiddenError('Forbidden ({})'.format(self.me)) watcher = project.create_watcher(self.me) resp = service_messages.CreateWatcherResponse() resp.watcher = watcher.to_message() @@ -102,7 +102,7 @@ def watch(self, request): def unwatch(self, request): project = self._get_project(request) if not project.can(self.me, projects.Permission.READ): - raise api.ForbiddenError('Forbidden.') + raise api.ForbiddenError('Forbidden ({})'.format(self.me)) project.delete_watcher(self.me) watchers = project.list_watchers() resp = service_messages.ListWatchersResponse() @@ -114,7 +114,7 @@ def unwatch(self, request): def list_watchers(self, request): project = self._get_project(request) if not project.can(self.me, projects.Permission.READ): - raise api.ForbiddenError('Forbidden.') + raise api.ForbiddenError('Forbidden ({})'.format(self.me)) watchers = project.list_watchers() resp = service_messages.ListWatchersResponse() resp.watching = any(self.me == watcher.user for watcher in watchers) @@ -126,7 +126,7 @@ def list_watchers(self, request): def list_named_filesets(self, request): project = self._get_project(request) if not project.can(self.me, projects.Permission.READ): - raise api.ForbiddenError('Forbidden.') + raise api.ForbiddenError('Forbidden ({})'.format(self.me)) named_filesets = project.list_named_filesets() resp = service_messages.ListNamedFilesetsRequest() resp.named_filesets = [named_fileset.to_message() @@ -138,7 +138,7 @@ def list_named_filesets(self, request): def create_named_fileset(self, request): project = self._get_project(request) if not project.can(self.me, projects.Permission.WRITE): - raise api.ForbiddenError('Forbidden.') + raise api.ForbiddenError('Forbidden ({})'.format(self.me)) named_fileset = project.create_named_fileset( request.named_fileset.name, request.named_fileset.branch) resp = service_messages.CreateNamedFilesetResponse() @@ -150,7 +150,7 @@ def create_named_fileset(self, request): def delete_named_fileset(self, request): project = self._get_project(request) if not project.can(self.me, projects.Permission.READ): - raise api.ForbiddenError('Forbidden.') + raise api.ForbiddenError('Forbidden ({})'.format(self.me)) project.delete_named_fileset(request.named_fileset.name) resp = service_messages.DeleteNamedFilesetResponse() return resp diff --git a/jetway/server/utils.py b/jetway/server/utils.py index 0cc3f96..a659ea0 100644 --- a/jetway/server/utils.py +++ b/jetway/server/utils.py @@ -5,6 +5,10 @@ _HOSTNAME_RE = re.compile('^(?:(.*)--)?(.*)--([^\.]*)\.') +def is_avatar_request(hostname): + return re.match('^avatars\d-dot-', hostname) + + def is_preview_server(hostname, path=None): return (hostname.endswith(appengine_config.PREVIEW_HOSTNAME) and hostname != appengine_config.PREVIEW_HOSTNAME From c569657fecf7f39884b37e3b7edc7a17f990d468 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Mon, 31 Aug 2015 21:52:11 -0700 Subject: [PATCH 05/46] Fix avatars. --- jetway/avatars/avatars.py | 2 +- jetway/server/utils.py | 2 +- jetway/server/utils_test.py | 8 ++++++++ jetway/users/users.py | 35 +++++++++++++++++------------------ requirements.txt | 2 +- 5 files changed, 28 insertions(+), 21 deletions(-) diff --git a/jetway/avatars/avatars.py b/jetway/avatars/avatars.py index 47613f3..9a1364b 100644 --- a/jetway/avatars/avatars.py +++ b/jetway/avatars/avatars.py @@ -82,7 +82,7 @@ def create_url(cls, owner): scheme = os.getenv('wsgi.url_scheme') hostname = os.getenv('DEFAULT_VERSION_HOSTNAME') sep = '.' if scheme == 'http' else '-dot-' - return '//avatars{}{}{}{}'.format(num, sep, hostname, path) + return '//avatars-{}{}{}{}'.format(num, sep, hostname, path) @classmethod def generate(cls, ident): diff --git a/jetway/server/utils.py b/jetway/server/utils.py index a659ea0..e7ab262 100644 --- a/jetway/server/utils.py +++ b/jetway/server/utils.py @@ -6,7 +6,7 @@ def is_avatar_request(hostname): - return re.match('^avatars\d-dot-', hostname) + return re.match('^avatars-.-dot-', hostname) def is_preview_server(hostname, path=None): diff --git a/jetway/server/utils_test.py b/jetway/server/utils_test.py index 3bde0ac..76ed1b8 100644 --- a/jetway/server/utils_test.py +++ b/jetway/server/utils_test.py @@ -42,6 +42,14 @@ def test_make_url(self): result = utils.make_url(fileset, project, owner, include_port=True) self.assertEqual(expected, result) + def test_is_avatar_request(self): + hostnames = [ + 'avatars-1-dot-foo', + 'avatars-U-dot-foo', + 'avatars-a-dot-foo', + ] + for hostname in hostnames: + self.assertTrue(utils.is_avatar_request(hostname)) if __name__ == '__main__': diff --git a/jetway/users/users.py b/jetway/users/users.py index d1de4a8..84c897a 100644 --- a/jetway/users/users.py +++ b/jetway/users/users.py @@ -33,10 +33,22 @@ def user_id(self): @classmethod def get_by_ident(cls, ident): - user = cls.get_by_id(int(ident)) - if user is None: - raise UserDoesNotExistError() - return user + key = cls._key_from_ident(ident) + ent = key.get() + if ent is None: + raise UserDoesNotExistError('{} does not exist.'.format(ident)) + if type(ent) != cls: + text = 'Retrieved model of type {}, expected {}.' + raise ModelKindError(text.format(type(ent), cls)) + return ent + + @classmethod + def _key_from_ident(cls, ident): + try: + return ndb.Key(urlsafe=ident) + # except (TypeError, ProtocolBufferDecodeError): + except: + return ndb.Key(cls, ident) @classmethod def get_by_email(cls, email): @@ -114,7 +126,7 @@ def get(cls, nickname): @property def ident(self): - return str(self.key.id()) + return self.key.urlsafe() def to_message(self): message = messages.UserMessage() @@ -207,16 +219,3 @@ def search_projects(self): results.append(project) results = sorted(results, key=lambda project: project.nickname) return results - - def regenerate_git_password(self): - git_password = security.generate_random_string(length=20) - hashed_password = security.generate_password_hash(git_password) - self.hashed_git_password = hashed_password - self.put() - return git_password - - def check_hashed_git_password(self, git_password): - matched = security.check_password_hash(git_password, self.hashed_git_password) - if matched is not True: - raise BadGitPasswordError() - return True diff --git a/requirements.txt b/requirements.txt index 31ba9fe..5884e8e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ protorpc-standalone==0.9.1 pycrypto==2.6 pydenticon==0.1.1 -PIL +Pillow # premailer. premailer==2.8.1 From 540e63b291aec9aae1b6ef126162771ce7518e2d Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Mon, 2 Nov 2015 10:52:18 -0800 Subject: [PATCH 06/46] Work on domains. --- appengine_config.py | 5 ++++- jetway/{memberships => domains}/__init__.py | 0 jetway/domains/domains.py | 6 ++++++ jetway/frontend/static/html/project.team.html | 2 +- jetway/memberships/messages.py | 5 ----- jetway/models/models.py | 17 +++++++++++++++++ jetway/{memberships => orgs}/memberships.py | 0 jetway/orgs/orgs.py | 8 ++++---- jetway/teams/teams.py | 12 +++++++----- jetway/users/users.py | 1 + 10 files changed, 40 insertions(+), 16 deletions(-) rename jetway/{memberships => domains}/__init__.py (100%) create mode 100644 jetway/domains/domains.py delete mode 100644 jetway/memberships/messages.py create mode 100644 jetway/models/models.py rename jetway/{memberships => orgs}/memberships.py (100%) diff --git a/appengine_config.py b/appengine_config.py index 46cae41..8529cfb 100644 --- a/appengine_config.py +++ b/appengine_config.py @@ -46,6 +46,8 @@ REQUIRE_HTTPS_FOR_PREVIEWS = jetway_config.get('require_https', {}).get('preview_domain', False) +HTTPS_PROXY_ENABLED_FOR_PREVIEWS = jetway_config.get('require_https', {}).get('behind_proxy', False) + REQUIRE_HTTPS_FOR_APP = jetway_config.get('require_https', {}).get('app_domain', False) GCS_SERVICE_ACCOUNT_EMAIL = service_account_key['client_email'] @@ -72,7 +74,8 @@ def get_gcs_bucket(): BUILDBOT_API_KEY = jetway_config['app'].get('webreview_buildbot_api_key') BUILDBOT_SERVICE_ACCOUNT = jetway_config['app'].get('webreview_buildbot_service_account') -BASE_URL = '{}://{}'.format(os.getenv('wsgi.url_scheme'), os.getenv('SERVER_NAME')) +APP_HOSTNAME = os.getenv('DEFAULT_VERSION_HOSTNAME', os.getenv('SERVER_NAME')) +BASE_URL = '{}://{}'.format(os.getenv('wsgi.url_scheme'), APP_HOSTNAME) if IS_DEV_SERVER: BASE_URL += ':{}'.format(os.getenv('SERVER_PORT')) diff --git a/jetway/memberships/__init__.py b/jetway/domains/__init__.py similarity index 100% rename from jetway/memberships/__init__.py rename to jetway/domains/__init__.py diff --git a/jetway/domains/domains.py b/jetway/domains/domains.py new file mode 100644 index 0000000..080b209 --- /dev/null +++ b/jetway/domains/domains.py @@ -0,0 +1,6 @@ +from jetway import models +from google.appengine.ext import ndb + + +class Domain(models.Model): + name = ndb.StringProperty() diff --git a/jetway/frontend/static/html/project.team.html b/jetway/frontend/static/html/project.team.html index fdf6bf6..6b14879 100644 --- a/jetway/frontend/static/html/project.team.html +++ b/jetway/frontend/static/html/project.team.html @@ -3,7 +3,7 @@ - {{membership.user.nickname}} + {{membership.user.email}} diff --git a/jetway/memberships/messages.py b/jetway/memberships/messages.py deleted file mode 100644 index 61c2f3b..0000000 --- a/jetway/memberships/messages.py +++ /dev/null @@ -1,5 +0,0 @@ -from protorpc import messages - - -class MembershipMessage(messages.Message): - pass diff --git a/jetway/models/models.py b/jetway/models/models.py new file mode 100644 index 0000000..04892f0 --- /dev/null +++ b/jetway/models/models.py @@ -0,0 +1,17 @@ +from google.appengine.ext import ndb + + +class Model(ndb.Model): + _message_class = None + + @classmethod + def create(cls, message): + pass + + @classmethod + def get_or_create(cls, name): + pass + + @classmethod + def get(cls, name): + pass diff --git a/jetway/memberships/memberships.py b/jetway/orgs/memberships.py similarity index 100% rename from jetway/memberships/memberships.py rename to jetway/orgs/memberships.py diff --git a/jetway/orgs/orgs.py b/jetway/orgs/orgs.py index c832d4b..8fa92b3 100644 --- a/jetway/orgs/orgs.py +++ b/jetway/orgs/orgs.py @@ -1,8 +1,8 @@ +from . import memberships +from . import messages +from ..avatars import avatars +from ..teams import teams from google.appengine.ext import ndb -from jetway.avatars import avatars -from jetway.memberships import memberships -from jetway.orgs import messages -from jetway.teams import teams import os diff --git a/jetway/teams/teams.py b/jetway/teams/teams.py index fbd8449..8bbef89 100644 --- a/jetway/teams/teams.py +++ b/jetway/teams/teams.py @@ -30,9 +30,13 @@ class CannotDeleteMembershipError(Error): class TeamMembership(ndb.Model): role = msgprop.EnumProperty(messages.Role) - user_key = ndb.KeyProperty() is_public = ndb.BooleanProperty(default=False) - review_required = ndb.BooleanProperty(default=False) + domain_key = ndb.KeyProperty() + user_key = ndb.KeyProperty() + + @property + def domain(self): + return self.domain_key.get() @property def user(self): @@ -43,7 +47,6 @@ def to_message(self): message.user = self.user.to_message() message.is_public = self.is_public message.role = self.role - message.review_required = self.review_required return message @@ -215,10 +218,9 @@ def delete_membership(self, user): return raise MembershipConflictError('Membership does not exist.') - def update_membership(self, user, role, review_required=False, is_public=False): + def update_membership(self, user, role, is_public=False): for membership in self.memberships: if membership.user_key == user.key: membership.role = role - membership.review_required = review_required membership.is_public = is_public self.put() diff --git a/jetway/users/users.py b/jetway/users/users.py index 84c897a..bdb5c76 100644 --- a/jetway/users/users.py +++ b/jetway/users/users.py @@ -132,6 +132,7 @@ def to_message(self): message = messages.UserMessage() if self.nickname: message.nickname = self.nickname + message.email = self.email message.ident = self.ident message.avatar_url = self.avatar_url message.description = self.description From 92a2db4525e2d8560ab48a496aef7621c6397623 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Tue, 3 Nov 2015 18:05:20 -0800 Subject: [PATCH 07/46] Initial buildbot integration. --- app.yaml | 1 + appengine_config.py | 3 ++ config/jetway.yaml.example | 3 ++ jetway/buildbot/__init__.py | 0 jetway/buildbot/buildbot.py | 63 +++++++++++++++++++++++++++++ jetway/buildbot/messages.py | 10 +++++ jetway/owners/services.py | 2 + jetway/projects/messages.py | 2 + jetway/projects/projects.py | 34 ++++++++++++++++ jetway/projects/service_messages.py | 9 +++++ jetway/projects/services.py | 11 +++++ requirements.txt | 1 + scripts/run | 4 +- 13 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 jetway/buildbot/__init__.py create mode 100644 jetway/buildbot/buildbot.py create mode 100644 jetway/buildbot/messages.py diff --git a/app.yaml b/app.yaml index 74bb095..fc605ed 100644 --- a/app.yaml +++ b/app.yaml @@ -4,6 +4,7 @@ threadsafe: true env_variables: JETWAY_CONFIG: config/jetway.grow-prod.yaml + GAE_USE_SOCKETS_HTTPLIB: 'anyvalue' # https://github.com/shazow/urllib3/issues/618#issuecomment-101795512 libraries: - name: lxml diff --git a/appengine_config.py b/appengine_config.py index 8529cfb..16b06d8 100644 --- a/appengine_config.py +++ b/appengine_config.py @@ -72,6 +72,9 @@ def get_gcs_bucket(): PREVIEW_HOSTNAME = jetway_config['urls']['hostname']['prod'] BUILDBOT_API_KEY = jetway_config['app'].get('webreview_buildbot_api_key') +BUILDBOT_URL = jetway_config['app'].get('webreview_buildbot_url') +BUILDBOT_PASSWORD = jetway_config['app'].get('webreview_buildbot_password') +BUILDBOT_USERNAME = jetway_config['app'].get('webreview_buildbot_username') BUILDBOT_SERVICE_ACCOUNT = jetway_config['app'].get('webreview_buildbot_service_account') APP_HOSTNAME = os.getenv('DEFAULT_VERSION_HOSTNAME', os.getenv('SERVER_NAME')) diff --git a/config/jetway.yaml.example b/config/jetway.yaml.example index 6250e77..7e004a3 100755 --- a/config/jetway.yaml.example +++ b/config/jetway.yaml.example @@ -12,6 +12,9 @@ app: # service_account_key_file: "example-123456789.json" webreview_buildbot_api_key: "xxx-xxx-xxx" webreview_buildbot_service_account: "xxx-xxx@developer.gserviceaccount.com" + webreview_buildbot_username: "admin" + webreview_buildbot_password: "admin" + webreview_buildbot_url: "http://localhost:5000" urls: # Where staging builds are served for previews. diff --git a/jetway/buildbot/__init__.py b/jetway/buildbot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/jetway/buildbot/buildbot.py b/jetway/buildbot/buildbot.py new file mode 100644 index 0000000..448fb94 --- /dev/null +++ b/jetway/buildbot/buildbot.py @@ -0,0 +1,63 @@ +from requests import auth +import appengine_config +import requests + +BASE = '{}/api'.format(appengine_config.BUILDBOT_URL) + + +class Error(Exception): + pass + + +class Buildbot(object): + + @property + def env(self): + return { + 'WEBREVIEW_API_KEY': appengine_config.BUILDBOT_API_KEY, + } + + @property + def auth(self): + return auth.HTTPBasicAuth( + appengine_config.BUILDBOT_USERNAME, + appengine_config.BUILDBOT_PASSWORD) + + def create_job(self, git_url, remote): + data = { + 'git_url': git_url, + 'remote': remote, + 'env': self.env, + } + try: + resp = requests.post(BASE + '/jobs', json=data, auth=self.auth) + except Exception as e: + raise Error(e) + return resp.json() + + def list_branches(self, job_id): + try: + resp = requests.get(BASE + '/git/repos/{}/branches'.format(job_id)) + except Exception as e: + raise Error(e) + content = resp.json() + return content + + def get_contents(self, job_id, path=None): + path = path or '/' + try: + resp = requests.get(BASE + '/git/repos/{}/contents{}'.format(job_id, path)) + except Exception as e: + raise Error(e) + return resp.json() + + def read_file(self, job_id, path): + try: + resp = requests.get(BASE + '/git/repos/{}/raw{}'.format(job_id, path)) + except Exception as e: + raise Error(e) + return resp.body + +# def write_file(self, job_id, path, contents): +# resp = requests.post(BASE + '/jobs/{}/contents/update'.format(job_id)) +# return resp.body diff --git a/jetway/buildbot/messages.py b/jetway/buildbot/messages.py new file mode 100644 index 0000000..3d735bf --- /dev/null +++ b/jetway/buildbot/messages.py @@ -0,0 +1,10 @@ +from protorpc import messages + + +class CommitMessage(messages.Message): + sha = messages.StringField(1) + + +class BranchMessage(messages.Message): + name = messages.StringField(1) + commit = messages.MessageField(CommitMessage, 2) diff --git a/jetway/owners/services.py b/jetway/owners/services.py index ec4f3f2..6a79be2 100644 --- a/jetway/owners/services.py +++ b/jetway/owners/services.py @@ -47,5 +47,7 @@ def update(self, request): def get(self, request): owner = self.get_owner(request) resp = messages.GetOwnerResponse() + if owner is None: + raise api.NotFoundError(request.owner.nickname) resp.owner = owner.to_message() return resp diff --git a/jetway/projects/messages.py b/jetway/projects/messages.py index 9be8b7f..ce5acda 100644 --- a/jetway/projects/messages.py +++ b/jetway/projects/messages.py @@ -35,3 +35,5 @@ class ProjectMessage(messages.Message): cover = messages.MessageField(CoverMessage, 8) name = messages.StringField(9) built = message_types.DateTimeField(10) + buildbot_job_id = messages.StringField(11) + git_url = messages.StringField(12) diff --git a/jetway/projects/projects.py b/jetway/projects/projects.py index 8942169..e1c54d1 100644 --- a/jetway/projects/projects.py +++ b/jetway/projects/projects.py @@ -1,4 +1,6 @@ from . import watchers +from ..buildbot import buildbot +from ..buildbot import messages as buildbot_messages from google.appengine.ext import ndb from google.appengine.ext.ndb import msgprop from jetway.avatars import avatars @@ -7,7 +9,10 @@ from jetway.owners import owners from jetway.projects import messages from jetway.teams import teams +from protorpc import protojson import appengine_config +import json +import logging import os @@ -48,11 +53,17 @@ class Project(ndb.Model): cover = ndb.StructuredProperty(Cover) visibility = msgprop.EnumProperty(messages.Visibility) built = ndb.DateTimeProperty() + buildbot_job_id = ndb.StringProperty() + git_url = ndb.StringProperty() @property def name(self): return '{}/{}'.format(self.owner.nickname, self.nickname) + @property + def permalink(self): + return '{}/{}'.format(appengine_config.BASE_URL, self.name) + @property def name_padded(self): return '{} / {}'.format(self.owner.nickname, self.nickname) @@ -82,6 +93,7 @@ def create(cls, owner, nickname, created_by, description=None): nickname=nickname, description=description) project.put() + project._create_buildbot_job() teams.Team.create(owner, None, created_by=created_by, project=project, kind=teams.messages.Kind.PROJECT_OWNERS) @@ -298,3 +310,25 @@ def delete_named_fileset(self, name): def list_named_filesets(self): return named_filesets.NamedFileset.search(project=self) + + def _create_buildbot_job(self): + bot = buildbot.Buildbot() + try: + resp = bot.create_job( + git_url=self.git_url, + remote=self.permalink) + self.buildbot_job_id = str(resp['job_id']) + self.put() + except buildbot.Error: + logging.exception('Buildbot connection error.') + + def list_branches(self): + bot = buildbot.Buildbot() + data = bot.list_branches(self.buildbot_job_id) + results = [] + for branch_data in data: + branch_data = json.dumps(branch_data) + message_class = buildbot_messages.BranchMessage + branch_message = protojson.decode_message(message_class, branch_data) + results.append(branch_message) + return results diff --git a/jetway/projects/service_messages.py b/jetway/projects/service_messages.py index 69a17f4..b937a3d 100644 --- a/jetway/projects/service_messages.py +++ b/jetway/projects/service_messages.py @@ -1,3 +1,4 @@ +from ..buildbot import messages as buildbot_messages from ..filesets.named_fileset_messages import * from .messages import * from .watcher_messages import * @@ -98,3 +99,11 @@ class DeleteNamedFilesetRequest(messages.Message): class DeleteNamedFilesetResponse(messages.Message): named_fileset = messages.MessageField(NamedFilesetMessage, 1) + + +class ListBranchesRequest(messages.Message): + project = messages.MessageField(ProjectMessage, 1) + + +class ListBranchesResponse(messages.Message): + branches = messages.MessageField(buildbot_messages.BranchMessage, 1, repeated=True) diff --git a/jetway/projects/services.py b/jetway/projects/services.py index 9ade186..2671d8a 100644 --- a/jetway/projects/services.py +++ b/jetway/projects/services.py @@ -154,3 +154,14 @@ def delete_named_fileset(self, request): project.delete_named_fileset(request.named_fileset.name) resp = service_messages.DeleteNamedFilesetResponse() return resp + + @remote.method(service_messages.ListBranchesRequest, + service_messages.ListBranchesResponse) + def list_branches(self, request): + project = self._get_project(request) + if not project.can(self.me, projects.Permission.READ): + raise api.ForbiddenError('Forbidden ({})'.format(self.me)) + branches = project.list_branches() + resp = service_messages.ListBranchesResponse() + resp.branches = branches + return resp diff --git a/requirements.txt b/requirements.txt index 5884e8e..3b504cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ GoogleAppEngineCloudStorageClient==1.9.5.0 google-api-python-client==1.3.1 httplib2 +requests protorpc-standalone==0.9.1 pycrypto==2.6 diff --git a/scripts/run b/scripts/run index 0380bb6..0e2ec82 100755 --- a/scripts/run +++ b/scripts/run @@ -1,4 +1,4 @@ #!/bin/bash -./node_modules/.bin/gulp \ - & dev_appserver.py --port=8088 --admin_port=8089 . +#./node_modules/.bin/gulp \ +dev_appserver.py --port=8088 --admin_port=8089 . From 651642bdf8b158d8f2a1bdeff139cf9bf976843d Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Wed, 4 Nov 2015 09:05:05 -0800 Subject: [PATCH 08/46] Add translations and catalogs. --- jetway/{strings => catalogs}/__init__.py | 0 jetway/catalogs/catalogs.py | 49 ++++++++++++++++++++++++ jetway/catalogs/messages.py | 5 +++ jetway/projects/projects.py | 11 ++++++ jetway/strings/memories.py | 5 --- jetway/strings/strings.py | 36 ----------------- jetway/translations/__init__.py | 0 jetway/translations/messages.py | 6 +++ jetway/translations/translations.py | 15 ++++++++ requirements.txt | 2 + 10 files changed, 88 insertions(+), 41 deletions(-) rename jetway/{strings => catalogs}/__init__.py (100%) create mode 100644 jetway/catalogs/catalogs.py create mode 100644 jetway/catalogs/messages.py delete mode 100644 jetway/strings/memories.py delete mode 100644 jetway/strings/strings.py create mode 100644 jetway/translations/__init__.py create mode 100644 jetway/translations/messages.py create mode 100644 jetway/translations/translations.py diff --git a/jetway/strings/__init__.py b/jetway/catalogs/__init__.py similarity index 100% rename from jetway/strings/__init__.py rename to jetway/catalogs/__init__.py diff --git a/jetway/catalogs/catalogs.py b/jetway/catalogs/catalogs.py new file mode 100644 index 0000000..ac3af87 --- /dev/null +++ b/jetway/catalogs/catalogs.py @@ -0,0 +1,49 @@ +from . import messages +from ..buildbot import buildbot +from ..translations import translations +from babel.messages import pofile +import cStringIO + + +class Error(Exception): + pass + + +class Catalog(object): + + def __init__(self, project, locale): + self.project = project + self.locale = locale + + @property + def path(self): + return '/translations/{}/LC_MESSAGES/messages.po'.format(self.locale) + + @property + def content(self): + bot = buildbot.Buildbot() + return bot.read_file(self.project.buildbot_job_id, path=self.path) + + @property + def babel_catalog(self): + fp = cStringIO.StringIO() + fp.write(self.content) + fp.seek(0) + return pofile.read_po(fp, self.locale) + + def list_translations(self): + translation_objs = [] + for message in self.babel_catalog: + translation = translations.Translation( + catalog=self, + msgid=message.id, + string=message.string) + translation_objs.append(translation) + return translation_objs + + def to_message(self): + message = messages.CatalogMessage() + message.locale = self.locale + message.translations = [translation.to_message() + for translation in self.list_translations()] + return message diff --git a/jetway/catalogs/messages.py b/jetway/catalogs/messages.py new file mode 100644 index 0000000..a7fe023 --- /dev/null +++ b/jetway/catalogs/messages.py @@ -0,0 +1,5 @@ +from protorpc import messages + + +class CatalogMessage(messages.Message): + locale = messages.StringField(1) diff --git a/jetway/projects/projects.py b/jetway/projects/projects.py index 88f3e2c..89329f1 100644 --- a/jetway/projects/projects.py +++ b/jetway/projects/projects.py @@ -1,6 +1,7 @@ from . import watchers from ..buildbot import buildbot from ..buildbot import messages as buildbot_messages +from ..catalogs import catalogs from google.appengine.ext import ndb from google.appengine.ext.ndb import msgprop from jetway.avatars import avatars @@ -339,3 +340,13 @@ def list_branches(self): branch_message = protojson.decode_message(message_class, branch_data) results.append(branch_message) return results + + def list_catalogs(self): + bot = buildbot.Buildbot() + items = bot.get_contents(self.buildbot_job_id, path='/translations/') + catalog_objs = [] + for item in items: + if item['type'] == 'dir': + catalog = catalogs.Catalog(project=self, locale=item['name']) + catalog_objs.append(catalog) + return catalog_objs diff --git a/jetway/strings/memories.py b/jetway/strings/memories.py deleted file mode 100644 index 847f96a..0000000 --- a/jetway/strings/memories.py +++ /dev/null @@ -1,5 +0,0 @@ -from google.appengine.ext import ndb - - -class Memory(ndb.Model): - name = ndb.StringProperty() diff --git a/jetway/strings/strings.py b/jetway/strings/strings.py deleted file mode 100644 index d2e9f54..0000000 --- a/jetway/strings/strings.py +++ /dev/null @@ -1,36 +0,0 @@ -from google.appengine.ext import ndb - - -class Locale(ndb.Model): - lang = ndb.StringProperty() - region = ndb.StringProperty() - - -class Flags(ndb.Model): - fuzzy = ndb.BooleanProperty() - - -class (ndb.Expando): - en = ndb.StringProperty() - lang = ndb.StringProperty() - translator_comments = ndb.TextProperty() - extracted_comments = ndb.TextProperty() - reference = ndb.TextProperty() - flags = ndb.StructuredProperty(Flags) - previous_untranslated_string = ndb.StringProperty() - - -class String(ndb.Model): - locale = ndb.StructuredProperty(Locale) - source = ndb.StructuredProperty(Translation) - created = ndb.DateTimeProperty(auto_now_add=True) - modified = ndb.DateTimeProperty(auto_now=True) - memory_key = ndb.KeyProperty() - - def search(cls, query_message): - query = cls.query() - query.fetch() - query = query.filter(cls.form.agree_to_share_with_fulfiller == True) - per_page = query_message.per_page if query_message else 50 - results, next_cursor, has_more = query.fetch_page(per_page, start_cursor=cursor) - return (results, next_cursor, has_more) diff --git a/jetway/translations/__init__.py b/jetway/translations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/jetway/translations/messages.py b/jetway/translations/messages.py new file mode 100644 index 0000000..e36b4ea --- /dev/null +++ b/jetway/translations/messages.py @@ -0,0 +1,6 @@ +from protorpc import messages + + +class TranslationMessage(messages.Message): + msgid = messages.StringField(1) + string = messages.StringField(2) diff --git a/jetway/translations/translations.py b/jetway/translations/translations.py new file mode 100644 index 0000000..399739a --- /dev/null +++ b/jetway/translations/translations.py @@ -0,0 +1,15 @@ +from . import messages + + +class Translation(object): + + def __init__(self, catalog, msgid, string): + self.catalog = catalog + self.msgid = msgid + self.string = string + + def to_message(self): + message = messages.TranslationMessage() + message.msgid = self.msgid + message.string = self.string + return message diff --git a/requirements.txt b/requirements.txt index 3b504cd..bf1c0d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ +Babel==1.3 + GoogleAppEngineCloudStorageClient==1.9.5.0 google-api-python-client==1.3.1 httplib2 From 2a53e3320216046dc12e9f5f699c63a39cf0e53d Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Wed, 4 Nov 2015 11:11:19 -0800 Subject: [PATCH 09/46] Add catalog services. --- jetway/buildbot/buildbot.py | 10 ++++++---- jetway/projects/projects.py | 5 ++++- jetway/projects/service_messages.py | 18 ++++++++++++++++++ jetway/projects/services.py | 22 ++++++++++++++++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/jetway/buildbot/buildbot.py b/jetway/buildbot/buildbot.py index 448fb94..df9e733 100644 --- a/jetway/buildbot/buildbot.py +++ b/jetway/buildbot/buildbot.py @@ -43,17 +43,19 @@ def list_branches(self, job_id): content = resp.json() return content - def get_contents(self, job_id, path=None): + def get_contents(self, job_id, path=None, ref=None): path = path or '/' try: - resp = requests.get(BASE + '/git/repos/{}/contents{}'.format(job_id, path)) + request_path = BASE + '/git/repos/{}/contents{}'.format(job_id, path) + resp = requests.get(request_path) except Exception as e: raise Error(e) return resp.json() - def read_file(self, job_id, path): + def read_file(self, job_id, path, ref=None): try: - resp = requests.get(BASE + '/git/repos/{}/raw{}'.format(job_id, path)) + request_path = BASE + '/git/repos/{}/raw{}'.format(job_id, path) + resp = requests.get(request_path) except Exception as e: raise Error(e) return resp.body diff --git a/jetway/projects/projects.py b/jetway/projects/projects.py index 89329f1..0c46f20 100644 --- a/jetway/projects/projects.py +++ b/jetway/projects/projects.py @@ -347,6 +347,9 @@ def list_catalogs(self): catalog_objs = [] for item in items: if item['type'] == 'dir': - catalog = catalogs.Catalog(project=self, locale=item['name']) + catalog = self.get_catalog(locale=item['name']) catalog_objs.append(catalog) return catalog_objs + + def get_catalog(self, locale): + return catalogs.Catalog(project=self, locale=locale) diff --git a/jetway/projects/service_messages.py b/jetway/projects/service_messages.py index b937a3d..a99a9ef 100644 --- a/jetway/projects/service_messages.py +++ b/jetway/projects/service_messages.py @@ -1,4 +1,5 @@ from ..buildbot import messages as buildbot_messages +from ..catalogs import messages as catalog_messages from ..filesets.named_fileset_messages import * from .messages import * from .watcher_messages import * @@ -107,3 +108,20 @@ class ListBranchesRequest(messages.Message): class ListBranchesResponse(messages.Message): branches = messages.MessageField(buildbot_messages.BranchMessage, 1, repeated=True) + + +class ProjectRequest(messages.Message): + project = messages.MessageField(ProjectMessage, 1) + + +class ListCatalogsResponse(messages.Message): + catalogs = messages.MessageField(catalog_messages.CatalogMessage, 1, repeated=True) + + +class GetCatalogRequest(messages.Message): + project = messages.MessageField(ProjectMessage, 1) + catalog = messages.MessageField(catalog_messages.CatalogMessage, 2) + + +class GetCatalogResponse(messages.Message): + catalog = messages.MessageField(catalog_messages.CatalogMessage, 1) diff --git a/jetway/projects/services.py b/jetway/projects/services.py index 2671d8a..ba6e11e 100644 --- a/jetway/projects/services.py +++ b/jetway/projects/services.py @@ -165,3 +165,25 @@ def list_branches(self, request): resp = service_messages.ListBranchesResponse() resp.branches = branches return resp + + @remote.method(service_messages.ProjectRequest, + service_messages.ListCatalogsResponse) + def list_catalogs(self, request): + project = self._get_project(request) + if not project.can(self.me, projects.Permission.READ): + raise api.ForbiddenError('Forbidden ({})'.format(self.me)) + catalogs = project.list_catalogs() + resp = service_messages.ListCatalogsResponse() + resp.catalogs = [catalog.to_message() for catalog in catalogs] + return resp + + @remote.method(service_messages.ProjectRequest, + service_messages.GetCatalogResponse) + def get_catalog(self, request): + project = self._get_project(request) + if not project.can(self.me, projects.Permission.READ): + raise api.ForbiddenError('Forbidden ({})'.format(self.me)) + catalog = project.get_catalog(request.catalog.locale) + resp = service_messages.GetCatalogResponse() + resp.catalog = catalog.to_message() + return resp From ffb2cab331c8004960bdc92194da3109fa759722 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Wed, 4 Nov 2015 17:15:48 -0800 Subject: [PATCH 10/46] Fix catalogs service. --- jetway/buildbot/buildbot.py | 6 +++--- jetway/catalogs/catalogs.py | 5 +++-- jetway/catalogs/messages.py | 2 ++ jetway/projects/services.py | 2 +- requirements.txt | 1 + 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/jetway/buildbot/buildbot.py b/jetway/buildbot/buildbot.py index df9e733..016a321 100644 --- a/jetway/buildbot/buildbot.py +++ b/jetway/buildbot/buildbot.py @@ -52,13 +52,13 @@ def get_contents(self, job_id, path=None, ref=None): raise Error(e) return resp.json() - def read_file(self, job_id, path, ref=None): + def read_file(self, job_id, path, ref): try: - request_path = BASE + '/git/repos/{}/raw{}'.format(job_id, path) + request_path = BASE + '/git/repos/{}/raw/{}{}'.format(job_id, ref, path) resp = requests.get(request_path) except Exception as e: raise Error(e) - return resp.body + return resp.content # def write_file(self, job_id, path, contents): # resp = requests.post(BASE + '/jobs/{}/contents/update'.format(job_id)) diff --git a/jetway/catalogs/catalogs.py b/jetway/catalogs/catalogs.py index ac3af87..b9192bd 100644 --- a/jetway/catalogs/catalogs.py +++ b/jetway/catalogs/catalogs.py @@ -11,9 +11,10 @@ class Error(Exception): class Catalog(object): - def __init__(self, project, locale): + def __init__(self, project, locale, ref='master'): self.project = project self.locale = locale + self.ref = ref @property def path(self): @@ -22,7 +23,7 @@ def path(self): @property def content(self): bot = buildbot.Buildbot() - return bot.read_file(self.project.buildbot_job_id, path=self.path) + return bot.read_file(self.project.buildbot_job_id, path=self.path, ref=self.ref) @property def babel_catalog(self): diff --git a/jetway/catalogs/messages.py b/jetway/catalogs/messages.py index a7fe023..fa833a9 100644 --- a/jetway/catalogs/messages.py +++ b/jetway/catalogs/messages.py @@ -1,5 +1,7 @@ from protorpc import messages +from ..translations import messages as translation_messages class CatalogMessage(messages.Message): locale = messages.StringField(1) + translations = messages.MessageField(translation_messages.TranslationMessage, 2, repeated=True) diff --git a/jetway/projects/services.py b/jetway/projects/services.py index ba6e11e..44ab5a4 100644 --- a/jetway/projects/services.py +++ b/jetway/projects/services.py @@ -177,7 +177,7 @@ def list_catalogs(self, request): resp.catalogs = [catalog.to_message() for catalog in catalogs] return resp - @remote.method(service_messages.ProjectRequest, + @remote.method(service_messages.GetCatalogRequest, service_messages.GetCatalogResponse) def get_catalog(self, request): project = self._get_project(request) diff --git a/requirements.txt b/requirements.txt index bf1c0d4..e049942 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ Babel==1.3 +pytz GoogleAppEngineCloudStorageClient==1.9.5.0 google-api-python-client==1.3.1 From 7bf24a59f0d7e0676516db719d1aa0bece261d54 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Wed, 4 Nov 2015 17:34:48 -0800 Subject: [PATCH 11/46] Update buildbot job when git url changes. --- jetway/projects/projects.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/jetway/projects/projects.py b/jetway/projects/projects.py index 0c46f20..ba67b81 100644 --- a/jetway/projects/projects.py +++ b/jetway/projects/projects.py @@ -101,7 +101,7 @@ def create(cls, owner, nickname, created_by, description=None): nickname=nickname, description=description) project.put() - project._create_buildbot_job() + project._update_buildbot_job() teams.Team.create(owner, None, created_by=created_by, project=project, kind=teams.messages.Kind.PROJECT_OWNERS) @@ -127,6 +127,21 @@ def get(cls, owner=None, nickname=None): raise ProjectDoesNotExistError(text.format(nickname)) return project + def _update_buildbot_job(self): + if not self.git_url: + self.buildbot_job_id = None + self.put() + return + bot = buildbot.Buildbot() + try: + resp = bot.create_job( + git_url=self.git_url, + remote=self.permalink) + self.buildbot_job_id = str(resp['job_id']) + self.put() + except buildbot.Error: + logging.exception('Buildbot connection error.') + @classmethod def search(cls, owner=None, order=None): query = cls.query() @@ -213,6 +228,9 @@ def update(self, message): if message.cover: self.cover = Cover.from_message(message.cover) self.visibility = message.visibility + self.git_url = message.git_url + if message.git_url != self.git_url: + self._update_buildbot_job() self.put() def search_teams(self, users=None): @@ -319,17 +337,6 @@ def delete_named_fileset(self, name): def list_named_filesets(self): return named_filesets.NamedFileset.search(project=self) - def _create_buildbot_job(self): - bot = buildbot.Buildbot() - try: - resp = bot.create_job( - git_url=self.git_url, - remote=self.permalink) - self.buildbot_job_id = str(resp['job_id']) - self.put() - except buildbot.Error: - logging.exception('Buildbot connection error.') - def list_branches(self): bot = buildbot.Buildbot() data = bot.list_branches(self.buildbot_job_id) From 61010c13734892bd0d6788beefdecf2a7a66f47c Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Mon, 9 Nov 2015 00:00:37 -0800 Subject: [PATCH 12/46] Work on buildbot integration. --- jetway/api_errors.py | 2 +- jetway/buildbot/buildbot.py | 28 ++++++++++++++++++++------ jetway/catalogs/catalogs.py | 40 +++++++++++++++++++++++++++++++------ jetway/catalogs/messages.py | 10 +++++++++- jetway/projects/projects.py | 19 +++++++++++------- jetway/projects/services.py | 24 +++++++++++++++++----- jetway/users/messages.py | 18 ++++++----------- jetway/users/services.py | 15 +++----------- jetway/users/users.py | 4 +--- 9 files changed, 107 insertions(+), 53 deletions(-) diff --git a/jetway/api_errors.py b/jetway/api_errors.py index 3c94c25..2f40b7b 100644 --- a/jetway/api_errors.py +++ b/jetway/api_errors.py @@ -2,7 +2,7 @@ import httplib -class Error(Exception): +class Error(remote.ApplicationError): pass diff --git a/jetway/buildbot/buildbot.py b/jetway/buildbot/buildbot.py index 016a321..2c6e5e7 100644 --- a/jetway/buildbot/buildbot.py +++ b/jetway/buildbot/buildbot.py @@ -9,6 +9,14 @@ class Error(Exception): pass +class ConnectionError(Error): + pass + + +class IntegrationError(Error): + pass + + class Buildbot(object): @property @@ -32,15 +40,20 @@ def create_job(self, git_url, remote): try: resp = requests.post(BASE + '/jobs', json=data, auth=self.auth) except Exception as e: - raise Error(e) - return resp.json() + raise ConnectionError(e) + content = resp.json() + if 'error' in content: + raise IntegrationError(content['error']) + return content def list_branches(self, job_id): try: resp = requests.get(BASE + '/git/repos/{}/branches'.format(job_id)) except Exception as e: - raise Error(e) + raise ConnectionError(e) content = resp.json() + if 'error' in content: + raise IntegrationError(content['error']) return content def get_contents(self, job_id, path=None, ref=None): @@ -49,15 +62,18 @@ def get_contents(self, job_id, path=None, ref=None): request_path = BASE + '/git/repos/{}/contents{}'.format(job_id, path) resp = requests.get(request_path) except Exception as e: - raise Error(e) - return resp.json() + raise ConnectionError(e) + content = resp.json() + if 'error' in content: + raise IntegrationError(content['error']) + return content def read_file(self, job_id, path, ref): try: request_path = BASE + '/git/repos/{}/raw/{}{}'.format(job_id, ref, path) resp = requests.get(request_path) except Exception as e: - raise Error(e) + raise ConnectionError(e) return resp.content # def write_file(self, job_id, path, contents): diff --git a/jetway/catalogs/catalogs.py b/jetway/catalogs/catalogs.py index b9192bd..5f228db 100644 --- a/jetway/catalogs/catalogs.py +++ b/jetway/catalogs/catalogs.py @@ -2,6 +2,7 @@ from ..buildbot import buildbot from ..translations import translations from babel.messages import pofile +import babel import cStringIO @@ -14,7 +15,13 @@ class Catalog(object): def __init__(self, project, locale, ref='master'): self.project = project self.locale = locale + self.babel_locale = babel.Locale.parse(self.locale) self.ref = ref + self._bot = buildbot.Buildbot() + self.num_translated = 0 + self.num_fuzzy = 0 + self.num_messages = 0 + self.percent_translated = 0 @property def path(self): @@ -22,8 +29,10 @@ def path(self): @property def content(self): - bot = buildbot.Buildbot() - return bot.read_file(self.project.buildbot_job_id, path=self.path, ref=self.ref) + return self._bot.read_file( + self.project.buildbot_job_id, + path=self.path, + ref=self.ref) @property def babel_catalog(self): @@ -32,9 +41,13 @@ def babel_catalog(self): fp.seek(0) return pofile.read_po(fp, self.locale) + @property + def name(self): + return self.babel_locale.get_display_name('en_US') + def list_translations(self): translation_objs = [] - for message in self.babel_catalog: + for message in list(self.babel_catalog)[1:]: translation = translations.Translation( catalog=self, msgid=message.id, @@ -42,9 +55,24 @@ def list_translations(self): translation_objs.append(translation) return translation_objs - def to_message(self): + def _generate_stats(self): + for message in list(self.babel_catalog)[1:]: + self.num_messages += 1 + if message.string: + self.num_translated += 1 + if 'fuzzy' in message.flags: + self.num_fuzzy += 1 + self.percent_translated = self.num_translated * 100 // self.num_messages + + def to_message(self, included=None): + self._generate_stats() message = messages.CatalogMessage() + message.name = self.name message.locale = self.locale - message.translations = [translation.to_message() - for translation in self.list_translations()] + if included is None or 'translations' in included: + message.translations = [translation.to_message() + for translation in self.list_translations()] + message.num_translated = self.num_translated + message.num_fuzzy = self.num_fuzzy + message.percent_translated = self.percent_translated return message diff --git a/jetway/catalogs/messages.py b/jetway/catalogs/messages.py index fa833a9..f2797ea 100644 --- a/jetway/catalogs/messages.py +++ b/jetway/catalogs/messages.py @@ -1,7 +1,15 @@ from protorpc import messages +from protorpc import message_types from ..translations import messages as translation_messages class CatalogMessage(messages.Message): locale = messages.StringField(1) - translations = messages.MessageField(translation_messages.TranslationMessage, 2, repeated=True) + translations = messages.MessageField( + translation_messages.TranslationMessage, 2, repeated=True) + name = messages.StringField(3) + percent_translated = messages.IntegerField(4) + modified = message_types.DateTimeField(5) + num_fuzzy = messages.IntegerField(6) + num_translated = messages.IntegerField(7) + num_messages = messages.IntegerField(8) diff --git a/jetway/projects/projects.py b/jetway/projects/projects.py index ba67b81..c7d3c3c 100644 --- a/jetway/projects/projects.py +++ b/jetway/projects/projects.py @@ -88,7 +88,7 @@ def search_unknown_by_buildbot(cls): return results @classmethod - def create(cls, owner, nickname, created_by, description=None): + def create(cls, owner, nickname, created_by, description=None, git_url=None): try: cls.get(owner, nickname) text = 'Project {}/{} already exists.' @@ -99,9 +99,10 @@ def create(cls, owner, nickname, created_by, description=None): owner_key=owner.key, created_by_key=created_by.key, nickname=nickname, + git_url=git_url, description=description) project.put() - project._update_buildbot_job() + project._update_buildbot_job(project.git_url) teams.Team.create(owner, None, created_by=created_by, project=project, kind=teams.messages.Kind.PROJECT_OWNERS) @@ -127,17 +128,19 @@ def get(cls, owner=None, nickname=None): raise ProjectDoesNotExistError(text.format(nickname)) return project - def _update_buildbot_job(self): - if not self.git_url: + def _update_buildbot_job(self, git_url): + if git_url is None: self.buildbot_job_id = None self.put() return + logging.info('Buildbot URL update {} -> {}'.format(self, git_url)) bot = buildbot.Buildbot() try: resp = bot.create_job( - git_url=self.git_url, + git_url=git_url, remote=self.permalink) self.buildbot_job_id = str(resp['job_id']) + logging.info('Buildbot job ID update {} -> {}'.format(self, self.buildbot_job_id)) self.put() except buildbot.Error: logging.exception('Buildbot connection error.') @@ -218,6 +221,8 @@ def to_message(self): message.description = self.description message.avatar_url = self.avatar_url message.visibility = self.visibility + message.git_url = self.git_url + message.buildbot_job_id = self.buildbot_job_id if self.cover: message.cover = self.cover.to_message() message.built = self.built @@ -228,9 +233,9 @@ def update(self, message): if message.cover: self.cover = Cover.from_message(message.cover) self.visibility = message.visibility - self.git_url = message.git_url if message.git_url != self.git_url: - self._update_buildbot_job() + self._update_buildbot_job(message.git_url) + self.git_url = message.git_url self.put() def search_teams(self, users=None): diff --git a/jetway/projects/services.py b/jetway/projects/services.py index 44ab5a4..f98079a 100644 --- a/jetway/projects/services.py +++ b/jetway/projects/services.py @@ -1,4 +1,5 @@ from . import service_messages +from ..buildbot import buildbot from protorpc import remote from jetway import api from jetway.owners import owners @@ -22,9 +23,13 @@ def _get_project(self, request): service_messages.CreateProjectResponse) def create(self, request): try: - owner = owners.Owner.get(request.project.owner.nickname) + try: + owner = owners.Owner.get(request.project.owner.nickname) + except owners.OwnerDoesNotExistError as e: + raise api.NotFoundError(str(e)) project = projects.Project.create(owner, request.project.nickname, description=request.project.description, + git_url=request.project.git_url, created_by=self.me) except projects.ProjectExistsError as e: raise api.ConflictError(str(e)) @@ -161,7 +166,10 @@ def list_branches(self, request): project = self._get_project(request) if not project.can(self.me, projects.Permission.READ): raise api.ForbiddenError('Forbidden ({})'.format(self.me)) - branches = project.list_branches() + try: + branches = project.list_branches() + except buildbot.Error as e: + raise api.Error(str(e)) resp = service_messages.ListBranchesResponse() resp.branches = branches return resp @@ -172,9 +180,12 @@ def list_catalogs(self, request): project = self._get_project(request) if not project.can(self.me, projects.Permission.READ): raise api.ForbiddenError('Forbidden ({})'.format(self.me)) - catalogs = project.list_catalogs() + try: + catalogs = project.list_catalogs() + except buildbot.Error as e: + raise api.Error(str(e)) resp = service_messages.ListCatalogsResponse() - resp.catalogs = [catalog.to_message() for catalog in catalogs] + resp.catalogs = [catalog.to_message(included=[]) for catalog in catalogs] return resp @remote.method(service_messages.GetCatalogRequest, @@ -183,7 +194,10 @@ def get_catalog(self, request): project = self._get_project(request) if not project.can(self.me, projects.Permission.READ): raise api.ForbiddenError('Forbidden ({})'.format(self.me)) - catalog = project.get_catalog(request.catalog.locale) + try: + catalog = project.get_catalog(request.catalog.locale) + except buildbot.Error as e: + raise api.Error(str(e)) resp = service_messages.GetCatalogResponse() resp.catalog = catalog.to_message() return resp diff --git a/jetway/users/messages.py b/jetway/users/messages.py index a91b3ff..f65c399 100644 --- a/jetway/users/messages.py +++ b/jetway/users/messages.py @@ -21,16 +21,8 @@ class GetMeRequest(messages.Message): class GetMeResponse(messages.Message): - me = messages.MessageField(UserMessage, 1) - - -class RegenerateGitPasswordRequest(messages.Message): - pass - - -class RegenerateGitPasswordResponse(messages.Message): - me = messages.MessageField(UserMessage, 1) - git_password = messages.StringField(2) + me = messages.MessageField(UserMessage, 1) # TODO: Deprecate "me". + user = messages.MessageField(UserMessage, 2) class SignInRequest(messages.Message): @@ -50,11 +42,13 @@ class SignOutResponse(messages.Message): class UpdateMeRequest(messages.Message): - me = messages.MessageField(UserMessage, 1) + me = messages.MessageField(UserMessage, 1) # TODO: Deprecate "me". + user = messages.MessageField(UserMessage, 2) class UpdateMeResponse(messages.Message): - me = messages.MessageField(UserMessage, 1) + me = messages.MessageField(UserMessage, 1) # TODO: Deprecate "me". + user = messages.MessageField(UserMessage, 2) class SearchOrgsRequest(messages.Message): diff --git a/jetway/users/services.py b/jetway/users/services.py index f4fffdd..71771af 100644 --- a/jetway/users/services.py +++ b/jetway/users/services.py @@ -15,6 +15,7 @@ class MeService(api.Service): def get(self, request): resp = messages.GetMeResponse() resp.me = self.me.to_me_message() + resp.user = resp.me return resp @remote.method(messages.SignInRequest, @@ -35,11 +36,12 @@ def sign_out(self, request): @api.me_required def update(self, request): try: - self.me.update(request.me) + self.me.update(request.user) except users.UserExistsError as e: raise api.ConflictError(str(e)) resp = messages.UpdateMeResponse() resp.me = self.me.to_me_message() + resp.user = resp.me return resp @remote.method(messages.SearchProjectsRequest, @@ -70,17 +72,6 @@ def search_watchers(self, request): return resp - @remote.method(messages.RegenerateGitPasswordRequest, - messages.RegenerateGitPasswordResponse) - @api.me_required - def regenerate_git_password(self, request): - git_password = self.me.regenerate_git_password() - resp = messages.RegenerateGitPasswordResponse() - resp.me = self.me.to_me_message() - resp.git_password = git_password - return resp - - class UserService(api.Service): def _get_project(self, request): diff --git a/jetway/users/users.py b/jetway/users/users.py index bdb5c76..1e2cb39 100644 --- a/jetway/users/users.py +++ b/jetway/users/users.py @@ -94,7 +94,6 @@ def create_unique_username(cls, email): class User(BaseUser): - nickname = ndb.StringProperty() description = ndb.StringProperty() location = ndb.StringProperty() @@ -132,11 +131,10 @@ def to_message(self): message = messages.UserMessage() if self.nickname: message.nickname = self.nickname - message.email = self.email message.ident = self.ident message.avatar_url = self.avatar_url message.description = self.description - message.location = self.description + message.location = self.location message.website_url = self.website_url return message From 7717a21746d604525dc80d518225726d659a5bd0 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Tue, 10 Nov 2015 15:08:12 -0800 Subject: [PATCH 13/46] Work on translations. --- index.yaml | 8 +++++++ jetway/buildbot/buildbot.py | 23 ++++++++++++++----- jetway/catalogs/catalogs.py | 35 ++++++++++++++++++++++++++--- jetway/catalogs/messages.py | 3 +++ jetway/filesets/services.py | 7 ++++-- jetway/projects/service_messages.py | 4 ++-- jetway/projects/services.py | 21 ++++++++++++++--- jetway/teams/services.py | 1 - jetway/teams/teams.py | 4 +++- jetway/translations/messages.py | 1 + jetway/translations/translations.py | 19 +++++++++++++--- jetway/users/messages.py | 1 + jetway/users/users.py | 3 +++ 13 files changed, 109 insertions(+), 21 deletions(-) diff --git a/index.yaml b/index.yaml index fcb486f..1910a5c 100644 --- a/index.yaml +++ b/index.yaml @@ -10,6 +10,14 @@ indexes: # automatically uploaded to the admin console when you next deploy # your application using appcfg.py. +- kind: Fileset + properties: + - name: commit.sha + - name: name + - name: project_key + - name: modified + direction: desc + - kind: Fileset properties: - name: commit.sha diff --git a/jetway/buildbot/buildbot.py b/jetway/buildbot/buildbot.py index 2c6e5e7..a2159bb 100644 --- a/jetway/buildbot/buildbot.py +++ b/jetway/buildbot/buildbot.py @@ -48,7 +48,7 @@ def create_job(self, git_url, remote): def list_branches(self, job_id): try: - resp = requests.get(BASE + '/git/repos/{}/branches'.format(job_id)) + resp = requests.get(BASE + '/git/repos/{}/branches'.format(job_id), auth=self.auth) except Exception as e: raise ConnectionError(e) content = resp.json() @@ -60,7 +60,7 @@ def get_contents(self, job_id, path=None, ref=None): path = path or '/' try: request_path = BASE + '/git/repos/{}/contents{}'.format(job_id, path) - resp = requests.get(request_path) + resp = requests.get(request_path, auth=self.auth) except Exception as e: raise ConnectionError(e) content = resp.json() @@ -71,11 +71,22 @@ def get_contents(self, job_id, path=None, ref=None): def read_file(self, job_id, path, ref): try: request_path = BASE + '/git/repos/{}/raw/{}{}'.format(job_id, ref, path) - resp = requests.get(request_path) + resp = requests.get(request_path, auth=self.auth) except Exception as e: raise ConnectionError(e) return resp.content -# def write_file(self, job_id, path, contents): -# resp = requests.post(BASE + '/jobs/{}/contents/update'.format(job_id)) -# return resp.body + def write_file(self, job_id, path, contents, ref, sha, committer, author): + data = { + 'branch': ref, + 'path': path, + 'content': contents, + 'sha': sha, + 'committer': committer, + 'author': author, + } + try: + resp = requests.post(BASE + '/jobs/{}/contents/update'.format(job_id), json=data) + except Exception as e: + raise ConnectionError(e) + return resp.body diff --git a/jetway/catalogs/catalogs.py b/jetway/catalogs/catalogs.py index 5f228db..87a65fd 100644 --- a/jetway/catalogs/catalogs.py +++ b/jetway/catalogs/catalogs.py @@ -3,6 +3,7 @@ from ..translations import translations from babel.messages import pofile import babel +import io import cStringIO @@ -22,21 +23,30 @@ def __init__(self, project, locale, ref='master'): self.num_fuzzy = 0 self.num_messages = 0 self.percent_translated = 0 + self.sha = None + self.content = None + self.load() @property def path(self): return '/translations/{}/LC_MESSAGES/messages.po'.format(self.locale) @property - def content(self): - return self._bot.read_file( + def ident(self): + return '{}{}'.format(self.project.name, self.path) + + def load(self): + data = self._bot.get_contents( self.project.buildbot_job_id, path=self.path, ref=self.ref) + self.content = data['content'].encode('utf-8') + self.sha = data['sha'] @property def babel_catalog(self): - fp = cStringIO.StringIO() +# fp = cStringIO.StringIO() + fp = io.BytesIO() fp.write(self.content) fp.seek(0) return pofile.read_po(fp, self.locale) @@ -68,11 +78,30 @@ def to_message(self, included=None): self._generate_stats() message = messages.CatalogMessage() message.name = self.name + message.ident = self.ident message.locale = self.locale + message.sha = self.sha + message.ref = self.ref if included is None or 'translations' in included: message.translations = [translation.to_message() for translation in self.list_translations()] + message.num_messages = self.num_messages message.num_translated = self.num_translated message.num_fuzzy = self.num_fuzzy message.percent_translated = self.percent_translated return message + + def update_translations(self, translation_messages, ref, sha, committer, author): + for message in translation_messages: + self.babel_catalog[message.msgid] = message.string + fp = io.BytesIO() + pofile.write_po(fp, self.locale) + content = fp.getvalue() + return self._bot.write_file( + self.project.buildbot_job_id, + path=self.path, + contents=content, + ref=ref, + sha=sha, + committer=committer, + author=author) diff --git a/jetway/catalogs/messages.py b/jetway/catalogs/messages.py index f2797ea..2fd8472 100644 --- a/jetway/catalogs/messages.py +++ b/jetway/catalogs/messages.py @@ -13,3 +13,6 @@ class CatalogMessage(messages.Message): num_fuzzy = messages.IntegerField(6) num_translated = messages.IntegerField(7) num_messages = messages.IntegerField(8) + ident = messages.StringField(9) + sha = messages.StringField(10) + ref = messages.StringField(11) diff --git a/jetway/filesets/services.py b/jetway/filesets/services.py index 8105bb3..5907985 100644 --- a/jetway/filesets/services.py +++ b/jetway/filesets/services.py @@ -110,8 +110,11 @@ def delete(self, request): messages.SearchFilesetResponse) def search(self, request): if request.fileset.project: - owner = owners.Owner.get(request.fileset.project.owner.nickname) - project = projects.Project.get(owner, request.fileset.project.nickname) + if request.fileset.project.ident: + project = projects.Project.get_by_ident(request.fileset.project.ident) + else: + owner = owners.Owner.get(request.fileset.project.owner.nickname) + project = projects.Project.get(owner, request.fileset.project.nickname) else: project = None if (not self._is_authorized_buildbot() diff --git a/jetway/projects/service_messages.py b/jetway/projects/service_messages.py index a99a9ef..1ee094d 100644 --- a/jetway/projects/service_messages.py +++ b/jetway/projects/service_messages.py @@ -118,10 +118,10 @@ class ListCatalogsResponse(messages.Message): catalogs = messages.MessageField(catalog_messages.CatalogMessage, 1, repeated=True) -class GetCatalogRequest(messages.Message): +class CatalogRequest(messages.Message): project = messages.MessageField(ProjectMessage, 1) catalog = messages.MessageField(catalog_messages.CatalogMessage, 2) -class GetCatalogResponse(messages.Message): +class CatalogResponse(messages.Message): catalog = messages.MessageField(catalog_messages.CatalogMessage, 1) diff --git a/jetway/projects/services.py b/jetway/projects/services.py index f98079a..b476b80 100644 --- a/jetway/projects/services.py +++ b/jetway/projects/services.py @@ -188,8 +188,8 @@ def list_catalogs(self, request): resp.catalogs = [catalog.to_message(included=[]) for catalog in catalogs] return resp - @remote.method(service_messages.GetCatalogRequest, - service_messages.GetCatalogResponse) + @remote.method(service_messages.CatalogRequest, + service_messages.CatalogResponse) def get_catalog(self, request): project = self._get_project(request) if not project.can(self.me, projects.Permission.READ): @@ -198,6 +198,21 @@ def get_catalog(self, request): catalog = project.get_catalog(request.catalog.locale) except buildbot.Error as e: raise api.Error(str(e)) - resp = service_messages.GetCatalogResponse() + resp = service_messages.CatalogResponse() + resp.catalog = catalog.to_message() + return resp + + @remote.method(service_messages.CatalogRequest, + service_messages.CatalogResponse) + def update_translations(self, request): + project = self._get_project(request) + if not project.can(self.me, projects.Permission.READ): + raise api.ForbiddenError('Forbidden ({})'.format(self.me)) + try: + catalog = project.get_catalog(request.catalog.locale) + except buildbot.Error as e: + raise api.Error(str(e)) + catalog.update_translations(request.catalog.translations) + resp = service_messages.CatalogResponse() resp.catalog = catalog.to_message() return resp diff --git a/jetway/teams/services.py b/jetway/teams/services.py index e2a02df..2c9b812 100644 --- a/jetway/teams/services.py +++ b/jetway/teams/services.py @@ -171,7 +171,6 @@ def update_membership(self, request): user = self._get_user(request) team.update_membership(user, role=request.membership.role, - review_required=request.membership.review_required, is_public=request.membership.is_public) resp = messages.UpdateMembershipResponse() resp.team = team.to_message() diff --git a/jetway/teams/teams.py b/jetway/teams/teams.py index 8bbef89..f2e5ece 100644 --- a/jetway/teams/teams.py +++ b/jetway/teams/teams.py @@ -29,7 +29,7 @@ class CannotDeleteMembershipError(Error): class TeamMembership(ndb.Model): - role = msgprop.EnumProperty(messages.Role) + role = msgprop.EnumProperty(messages.Role, default=messages.Role.READ_ONLY) is_public = ndb.BooleanProperty(default=False) domain_key = ndb.KeyProperty() user_key = ndb.KeyProperty() @@ -196,6 +196,8 @@ def get_membership(self, user): return membership def create_membership(self, user, role=None, is_public=False): + if role is None: + role = messages.Role.READ_ONLY # if role is not None and self.kind != messages.Kind.PROJECT: # raise ValueError('Role cannot be set for non-project teams.') for membership in self.memberships: diff --git a/jetway/translations/messages.py b/jetway/translations/messages.py index e36b4ea..b425090 100644 --- a/jetway/translations/messages.py +++ b/jetway/translations/messages.py @@ -4,3 +4,4 @@ class TranslationMessage(messages.Message): msgid = messages.StringField(1) string = messages.StringField(2) + ident = messages.StringField(3) diff --git a/jetway/translations/translations.py b/jetway/translations/translations.py index 399739a..ac4bc1d 100644 --- a/jetway/translations/translations.py +++ b/jetway/translations/translations.py @@ -3,13 +3,26 @@ class Translation(object): - def __init__(self, catalog, msgid, string): + def __init__(self, catalog, msgid, string, comments=None): self.catalog = catalog - self.msgid = msgid - self.string = string + self.msgid = msgid or '' + self.string = string or '' + self.comments = comments + + @property + def ident(self): + return self.catalog.ident + '/' + self.msgid + if isinstance(self.msgid, unicode): + msgid = self.msgid.encode('utf-8') + else: + msgid = self.msgid + result = '{}/{}'.format(self.catalog.ident, msgid) + result = result.decode() + return result def to_message(self): message = messages.TranslationMessage() + message.ident = self.ident message.msgid = self.msgid message.string = self.string return message diff --git a/jetway/users/messages.py b/jetway/users/messages.py index f65c399..f70abf8 100644 --- a/jetway/users/messages.py +++ b/jetway/users/messages.py @@ -11,6 +11,7 @@ class UserMessage(messages.Message): website_url = messages.StringField(5) description = messages.StringField(6) location = messages.StringField(7) + name = messages.StringField(8) ### diff --git a/jetway/users/users.py b/jetway/users/users.py index 1e2cb39..cd47eb6 100644 --- a/jetway/users/users.py +++ b/jetway/users/users.py @@ -26,6 +26,7 @@ class UserExistsError(Error): class BaseUser(models.User): email = ndb.StringProperty() + name = ndb.StringProperty() def user_id(self): # Provides compatibility with oauth2client. @@ -136,6 +137,7 @@ def to_message(self): message.description = self.description message.location = self.location message.website_url = self.website_url + message.name = self.name return message def to_me_message(self): @@ -187,6 +189,7 @@ def update(self, message): except UserDoesNotExistError: pass self.email = message.email + self.name = message.name self.nickname = message.nickname self.description = message.description self.location = message.location From 9fa8af840191188b8d932a2696e636ad87dd67ed Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Tue, 10 Nov 2015 21:22:32 -0800 Subject: [PATCH 14/46] Buildbot catalog updating. --- appengine_config.py | 8 ++++---- jetway/buildbot/buildbot.py | 13 ++++++++++--- jetway/catalogs/catalogs.py | 16 ++++++++++++---- jetway/projects/services.py | 23 ++++++++++++++++++++--- jetway/users/users.py | 1 - 5 files changed, 46 insertions(+), 15 deletions(-) diff --git a/appengine_config.py b/appengine_config.py index 16b06d8..804aefb 100644 --- a/appengine_config.py +++ b/appengine_config.py @@ -52,10 +52,10 @@ GCS_SERVICE_ACCOUNT_EMAIL = service_account_key['client_email'] -_appid = os.getenv('APPLICATION_ID').replace('s~', '') -_sender_name = 'WebReview' -_sender_address = 'noreply@{}.appspotmail.com'.format(_appid) -EMAIL_SENDER = '{} <{}>'.format(_sender_name, _sender_address) +_appid = os.getenv('APPLICATION_ID', 'dev-webreview').split('~')[-1] +EMAIL_NAME = 'Web Review' +EMAIL_ADDRESS = 'noreply@{}.appspotmail.com'.format(_appid) +EMAIL_SENDER = '{} <{}>'.format(EMAIL_NAME, EMAIL_ADDRESS) IS_DEV_SERVER = os.getenv('SERVER_SOFTWARE', '').startswith('Dev') diff --git a/jetway/buildbot/buildbot.py b/jetway/buildbot/buildbot.py index a2159bb..7c21aee 100644 --- a/jetway/buildbot/buildbot.py +++ b/jetway/buildbot/buildbot.py @@ -76,17 +76,24 @@ def read_file(self, job_id, path, ref): raise ConnectionError(e) return resp.content - def write_file(self, job_id, path, contents, ref, sha, committer, author): + def write_file(self, job_id, path, contents, message, ref, sha, committer, author): data = { 'branch': ref, 'path': path, + 'message': message, 'content': contents, 'sha': sha, 'committer': committer, 'author': author, } try: - resp = requests.post(BASE + '/jobs/{}/contents/update'.format(job_id), json=data) + resp = requests.post( + BASE + '/jobs/{}/contents/update'.format(job_id), + json=data, + auth=self.auth) except Exception as e: raise ConnectionError(e) - return resp.body + result = resp.json() + if 'error' in result: + raise IntegrationError(result['error']) + return result diff --git a/jetway/catalogs/catalogs.py b/jetway/catalogs/catalogs.py index 87a65fd..f5aafa6 100644 --- a/jetway/catalogs/catalogs.py +++ b/jetway/catalogs/catalogs.py @@ -1,10 +1,12 @@ from . import messages from ..buildbot import buildbot from ..translations import translations +from babel.messages import catalog from babel.messages import pofile import babel -import io import cStringIO +import io +import webapp2 class Error(Exception): @@ -43,7 +45,7 @@ def load(self): self.content = data['content'].encode('utf-8') self.sha = data['sha'] - @property + @webapp2.cached_property def babel_catalog(self): # fp = cStringIO.StringIO() fp = io.BytesIO() @@ -92,15 +94,21 @@ def to_message(self, included=None): return message def update_translations(self, translation_messages, ref, sha, committer, author): + commit_message = '(auto commit from Web Review)' for message in translation_messages: - self.babel_catalog[message.msgid] = message.string + try: + self.babel_catalog[message.msgid].string = message.string + except KeyError: + babel_message = catalog.Message(message.msgid, message.string) + self.babel_catalog[message.msgid] = babel_message fp = io.BytesIO() - pofile.write_po(fp, self.locale) + pofile.write_po(fp, self.babel_catalog) content = fp.getvalue() return self._bot.write_file( self.project.buildbot_job_id, path=self.path, contents=content, + message=commit_message, ref=ref, sha=sha, committer=committer, diff --git a/jetway/projects/services.py b/jetway/projects/services.py index b476b80..e2869f7 100644 --- a/jetway/projects/services.py +++ b/jetway/projects/services.py @@ -1,10 +1,11 @@ from . import service_messages from ..buildbot import buildbot -from protorpc import remote from jetway import api from jetway.owners import owners -from jetway.projects import projects from jetway.projects import messages +from jetway.projects import projects +from protorpc import remote +import appengine_config class ProjectService(api.Service): @@ -212,7 +213,23 @@ def update_translations(self, request): catalog = project.get_catalog(request.catalog.locale) except buildbot.Error as e: raise api.Error(str(e)) - catalog.update_translations(request.catalog.translations) + committer = { + 'name': appengine_config.EMAIL_NAME, + 'email': appengine_config.EMAIL_ADDRESS, + } + author = { + 'name': self.me.name or 'Web Review User', + 'email': self.me.email, + } + try: + catalog.update_translations( + request.catalog.translations, + ref=request.catalog.ref, + sha=request.catalog.sha, + committer=committer, + author=author) + except buildbot.Error as e: + raise api.Error(str(e)) resp = service_messages.CatalogResponse() resp.catalog = catalog.to_message() return resp diff --git a/jetway/users/users.py b/jetway/users/users.py index cd47eb6..ad2b227 100644 --- a/jetway/users/users.py +++ b/jetway/users/users.py @@ -188,7 +188,6 @@ def update(self, message): raise UserExistsError('Nickname already in use.') except UserDoesNotExistError: pass - self.email = message.email self.name = message.name self.nickname = message.nickname self.description = message.description From 7d89f1ac38c80004bd0bcaf23adc4f70e68a9fcb Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Tue, 10 Nov 2015 23:24:29 -0800 Subject: [PATCH 15/46] Work on translations and staging. --- appengine_config.py | 12 ++++++++---- jetway/catalogs/catalogs.py | 7 ++++--- jetway/filesets/services.py | 3 +-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/appengine_config.py b/appengine_config.py index 804aefb..6066e16 100644 --- a/appengine_config.py +++ b/appengine_config.py @@ -52,16 +52,20 @@ GCS_SERVICE_ACCOUNT_EMAIL = service_account_key['client_email'] -_appid = os.getenv('APPLICATION_ID', 'dev-webreview').split('~')[-1] +IS_DEV_SERVER = os.getenv('SERVER_SOFTWARE', '').startswith('Dev') + +if IS_DEV_SERVER: + _appid = os.getenv('APPLICATION_ID').split('~')[-1] +else: + _appid = 'webreview-local-dev' + EMAIL_NAME = 'Web Review' EMAIL_ADDRESS = 'noreply@{}.appspotmail.com'.format(_appid) EMAIL_SENDER = '{} <{}>'.format(EMAIL_NAME, EMAIL_ADDRESS) -IS_DEV_SERVER = os.getenv('SERVER_SOFTWARE', '').startswith('Dev') - def get_gcs_bucket(): if IS_DEV_SERVER: - return 'grow-prod.appspot.com' + return 'grow-webreview-dev' return app_identity.get_default_gcs_bucket_name() if os.environ.get('TESTING'): diff --git a/jetway/catalogs/catalogs.py b/jetway/catalogs/catalogs.py index f5aafa6..bd81bad 100644 --- a/jetway/catalogs/catalogs.py +++ b/jetway/catalogs/catalogs.py @@ -47,7 +47,6 @@ def load(self): @webapp2.cached_property def babel_catalog(self): -# fp = cStringIO.StringIO() fp = io.BytesIO() fp.write(self.content) fp.seek(0) @@ -93,8 +92,10 @@ def to_message(self, included=None): message.percent_translated = self.percent_translated return message - def update_translations(self, translation_messages, ref, sha, committer, author): - commit_message = '(auto commit from Web Review)' + def update_translations(self, translation_messages, ref, sha, committer, author, + commit_message=None): + label = 'commit from Web Review - translations for {}' + commit_message = commit_message or label.format(self.locale) for message in translation_messages: try: self.babel_catalog[message.msgid].string = message.string diff --git a/jetway/filesets/services.py b/jetway/filesets/services.py index 5907985..863f36e 100644 --- a/jetway/filesets/services.py +++ b/jetway/filesets/services.py @@ -167,8 +167,7 @@ def _get_me(self, request): return users.User.get_by_email(email) def _get_or_create_fileset(self, request, me): - allow_fileset_by_commit = (request.fileset.commit - and self._is_authorized_buildbot()) + allow_fileset_by_commit = bool(request.fileset.commit) p = self._get_project(request) try: if allow_fileset_by_commit: From 3306dccedbc542f02443389be3751fdf347e2f80 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Wed, 11 Nov 2015 00:28:54 -0800 Subject: [PATCH 16/46] Use jobs service to list branches rather than restfulgit. --- jetway/buildbot/buildbot.py | 4 ++-- jetway/buildbot/messages.py | 1 + jetway/projects/projects.py | 15 ++++++++++----- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/jetway/buildbot/buildbot.py b/jetway/buildbot/buildbot.py index 7c21aee..2e4dfc4 100644 --- a/jetway/buildbot/buildbot.py +++ b/jetway/buildbot/buildbot.py @@ -46,9 +46,9 @@ def create_job(self, git_url, remote): raise IntegrationError(content['error']) return content - def list_branches(self, job_id): + def get_job(self, job_id): try: - resp = requests.get(BASE + '/git/repos/{}/branches'.format(job_id), auth=self.auth) + resp = requests.get(BASE + '/jobs/{}'.format(job_id), auth=self.auth) except Exception as e: raise ConnectionError(e) content = resp.json() diff --git a/jetway/buildbot/messages.py b/jetway/buildbot/messages.py index 3d735bf..00f4e5e 100644 --- a/jetway/buildbot/messages.py +++ b/jetway/buildbot/messages.py @@ -8,3 +8,4 @@ class CommitMessage(messages.Message): class BranchMessage(messages.Message): name = messages.StringField(1) commit = messages.MessageField(CommitMessage, 2) + ident = messages.StringField(3) diff --git a/jetway/projects/projects.py b/jetway/projects/projects.py index c7d3c3c..91d56df 100644 --- a/jetway/projects/projects.py +++ b/jetway/projects/projects.py @@ -344,13 +344,18 @@ def list_named_filesets(self): def list_branches(self): bot = buildbot.Buildbot() - data = bot.list_branches(self.buildbot_job_id) + job = bot.get_job(self.buildbot_job_id)['job'] results = [] - for branch_data in data: - branch_data = json.dumps(branch_data) - message_class = buildbot_messages.BranchMessage - branch_message = protojson.decode_message(message_class, branch_data) + for ref, data in job['ref_map'].iteritems(): + name = ref.replace('refs/heads/', '') + commit = buildbot_messages.CommitMessage(sha=data['sha']) + ident = self.ident + ':branch:' + name + branch_message = buildbot_messages.BranchMessage( + name=name, + commit=commit, + ident=ident) results.append(branch_message) + results = sorted(results, key=lambda message: message.name) return results def list_catalogs(self): From 83930ce03b1b6389ae67610de6946b97c955873b Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Wed, 11 Nov 2015 14:52:22 -0800 Subject: [PATCH 17/46] Remove dead code. --- jetway/builds/__init__.py | 0 jetway/builds/build_tasks.py | 39 ------------------------------------ jetway/builds/messages.py | 12 ----------- 3 files changed, 51 deletions(-) delete mode 100644 jetway/builds/__init__.py delete mode 100644 jetway/builds/build_tasks.py delete mode 100644 jetway/builds/messages.py diff --git a/jetway/builds/__init__.py b/jetway/builds/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/jetway/builds/build_tasks.py b/jetway/builds/build_tasks.py deleted file mode 100644 index 40208b9..0000000 --- a/jetway/builds/build_tasks.py +++ /dev/null @@ -1,39 +0,0 @@ -from google.appengine.ext import ndb -from . import messages - -""" -buildbot polls webreview -webreview returns list of projects to register, unregister, and update -buildbot registers and unregisters projects -buildbot updates webreview -""" - - -class BuildTask(ndb.Model): - project_key = ndb.KeyProperty() - action = ndb.MessageProperty(messages.ActionMessage) - - @classmethod - def create(cls, project, action): - build_task = cls(project_key=project.key, action=action) - build_task.put() - return build_task - - def delete(cls): - pass - - @classmethod - def search(cls): - pass - - def register(cls): - pass - - def unregister(cls): - pass - - def to_message(self): - message = messages.BuildTaskMessage() - message.project = self.project.to_message() - message.action = self.action - return message diff --git a/jetway/builds/messages.py b/jetway/builds/messages.py deleted file mode 100644 index 2025900..0000000 --- a/jetway/builds/messages.py +++ /dev/null @@ -1,12 +0,0 @@ -from protorpc import messages -from ..projects import messages as project_messages - - -class ActionMessage(messages.Message): - UNREGISTER = 0 - REGISTER = 1 - - -class BuildMessage(messages.Message): - project = messages.MessageField(project_messages.ProjectMessage, 1) - action = messages.EnumField(ActionMessage) From e8bb726f2cb972b2fbb1341da996736b37f35790 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Wed, 11 Nov 2015 15:03:03 -0800 Subject: [PATCH 18/46] Work on catalogs. --- jetway/catalogs/__init__.py | 1 + jetway/catalogs/babel_compatibility_patch.py | 64 ++++++++++++++++++++ jetway/catalogs/catalogs.py | 2 +- 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 jetway/catalogs/babel_compatibility_patch.py diff --git a/jetway/catalogs/__init__.py b/jetway/catalogs/__init__.py index e69de29..20ea213 100644 --- a/jetway/catalogs/__init__.py +++ b/jetway/catalogs/__init__.py @@ -0,0 +1 @@ +from . import babel_compatibility_patch diff --git a/jetway/catalogs/babel_compatibility_patch.py b/jetway/catalogs/babel_compatibility_patch.py new file mode 100644 index 0000000..315739f --- /dev/null +++ b/jetway/catalogs/babel_compatibility_patch.py @@ -0,0 +1,64 @@ +from babel import localedata +import pickle +import os + +# NOTE: Babel does not support "fuzzy" locales. A locale is considered "fuzzy" +# when a corresponding "localedata" file that matches a given locale's full +# identifier (e.g. "en_US") does not exist. Here's one example: "en_BD". CLDR +# does not have a localedata file matching "en_BD" (English in Bangladesh), but +# it does have individual files for "en" and also "bn_BD". As it turns +# out, localedata files that correspond to a locale's full identifier (e.g. +# "bn_BD.dat") are actually pretty light on the content (largely containing +# things like start-of-week information) and most of the "meat" of the data is +# contained in the main localedata file, e.g. "en.dat". +# +# Users may need to generate pages corresponding to locales that we don't +# have full localedata for, and until Babel supports fuzzy locales, we'll +# monkeypatch two Babel functions to provide partial support for fuzzy locales. +# +# With this monkeypatch, locales will be valid even if Babel doesn't have a +# localedata file matching a locale's full identifier, but locales will still +# fail with a ValueError if the user specifies a territory that does not exist. +# With this patch, a user can, however, specify an invalid language. Obviously, +# this patch should be removed when/if Babel adds support for fuzzy locales. +# Optionally, we may want to provide users with more control over whether a +# locale is valid or invalid, but we can revisit that later. + +# See: https://github.com/grow/pygrow/issues/93 + + +def fuzzy_load(name, merge_inherited=True): + localedata._cache_lock.acquire() + try: + data = localedata._cache.get(name) + if not data: + # Load inherited data + if name == 'root' or not merge_inherited: + data = {} + else: + parts = name.split('_') + if len(parts) == 1: + parent = 'root' + else: + parent = '_'.join(parts[:-1]) + data = fuzzy_load(parent).copy() + filename = os.path.join(localedata._dirname, '%s.dat' % name) + try: + fileobj = open(filename, 'rb') + try: + if name != 'root' and merge_inherited: + localedata.merge(data, pickle.load(fileobj)) + else: + data = pickle.load(fileobj) + localedata._cache[name] = data + finally: + fileobj.close() + except IOError: + pass + return data + finally: + localedata._cache_lock.release() + + +localedata.exists = lambda name: True +localedata.load = fuzzy_load diff --git a/jetway/catalogs/catalogs.py b/jetway/catalogs/catalogs.py index bd81bad..cd1ec25 100644 --- a/jetway/catalogs/catalogs.py +++ b/jetway/catalogs/catalogs.py @@ -94,7 +94,7 @@ def to_message(self, included=None): def update_translations(self, translation_messages, ref, sha, committer, author, commit_message=None): - label = 'commit from Web Review - translations for {}' + label = '(webreview) translations for {}' commit_message = commit_message or label.format(self.locale) for message in translation_messages: try: From 3c5278cd6dd002e884bd5ba84fb1168184ae0568 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Wed, 11 Nov 2015 15:03:23 -0800 Subject: [PATCH 19/46] Work on groups. --- jetway/groups/__init__.py | 0 jetway/groups/groups.py | 70 ++++++++++++++++++++++++++++++++++++ jetway/groups/memberships.py | 50 ++++++++++++++++++++++++++ jetway/groups/messages.py | 26 ++++++++++++++ jetway/users/users.py | 1 + 5 files changed, 147 insertions(+) create mode 100644 jetway/groups/__init__.py create mode 100644 jetway/groups/groups.py create mode 100644 jetway/groups/memberships.py create mode 100644 jetway/groups/messages.py diff --git a/jetway/groups/__init__.py b/jetway/groups/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/jetway/groups/groups.py b/jetway/groups/groups.py new file mode 100644 index 0000000..abc3043 --- /dev/null +++ b/jetway/groups/groups.py @@ -0,0 +1,70 @@ +from . import messages +from . import memberships +from google.appengine.ext import ndb +from google.appengine.ext.ndb import msgprop + + +class Error(Exception): + pass + + +class Group(ndb.Model): + memberships = ndb.StructuredProperty(repeated=True) + + @property + def ident(self): + return self.key.urlsafe() + + @classmethod + def get(cls, ident): + key = ndb.Key(urlsafe=ident) + group = key.get() + if group is None: + raise Error('Group does not exist.') + return group + + def validate(self): + num_admins = 0 + for mem in self.memberships: + if mem.role == messages.Role.ADMIN: + num_admins += 1 + if num_admins < 1: + raise memberships.MembershipConflictError('Must be at least one admin.') + + def create_membership(self, membership_message): + mem = memberships.Membership.from_message(membership_message) + mem.check_conflict(self.memberships) + self.memberships.append(mem) + self.validate() + self.put() + return self + + def delete_membership(self, membership_message): + mem = memberships.Membership.from_message(membership_message) + for i, each_mem in enumerate(self.memberships): + if each_mem == mem: + del self.memberships[i] + self.validate() + self.put() + return self + raise memberships.MembershipConflictError('Membership does not exist.') + + def list_memberships(self, kind=None): + mems = [] + for mem in self.memberships: + if kind is None: + mems.append(mem) + elif kind == messages.Kind.USER and mem.user_key: + mems.append(mem) + elif kind == messages.Kind.DOMAIN and mem.domain: + mems.append(mem) + return mems + + def to_message(self): + message = messages.GroupMessage() + message.ident = self.ident + message.users = [mem.to_message() + for mem in self.list_memberships(messages.Kind.USER)] + message.domains = [mem.to_message() + for mem in self.list_memberships(messages.Kind.DOMAIN)] + return message diff --git a/jetway/groups/memberships.py b/jetway/groups/memberships.py new file mode 100644 index 0000000..9815444 --- /dev/null +++ b/jetway/groups/memberships.py @@ -0,0 +1,50 @@ +from . import messages +from ..users import users +from google.appengine.ext import ndb +from google.appengine.ext.ndb import msgprop +import webapp2 + + +class Error(Exception): + pass + + +class MembershipConflictError(Error): + pass + + +class Membership(ndb.Model): + user_key = ndb.KeyProperty() + domain = ndb.StringProperty() + role = msgprop.EnumProperty(messages.Role) + + def check_conflict(self, other_memberships): + for mem in other_memberships: + if self.user_key and self.user_key == mem.user_key: + raise MembershipConflictError() + if self.domain and self.domain == mem.domain: + raise MembershipConflictError() + + @classmethod + def from_message(cls, message): + mem = cls() + mem.role = message.role + if message.user: + user = users.User.get_by_ident(message.user.ident) + mem.user_key = user.key + if message.domain: + mem.domain = message.domain + return mem + + @webapp2.cached_property + def user(self): + if self.user_key: + return self.user_key.get() + + def to_message(self): + message = messages.MembershipMessage() + if self.user_key: + message.user = self.user.to_message() + if self.domain: + message.domain = self.domain + return message diff --git a/jetway/groups/messages.py b/jetway/groups/messages.py new file mode 100644 index 0000000..55876d8 --- /dev/null +++ b/jetway/groups/messages.py @@ -0,0 +1,26 @@ +from ..users import messages as user_messages +from protorpc import messages + + +class Role(messages.Enum): + ADMIN = 1 + READ_ONLY = 2 + WRITE = 3 + WRITE_TRANSLATIONS = 4 + + +class Kind(messages.Enum): + USER = 1 + DOMAIN = 2 + + +class MembershipMessage(messages.Message): + user = messages.MessageField(user_messages.UserMessage, 1) + domain = messages.StringField(2) + role = messages.EnumField(Role, 3) + + +class GroupMessage(messages.Message): + ident = messages.StringField(2) + users = messages.MessageField(MembershipMessage, 1, repeated=True) + domains = messages.MessageField(MembershipMessage, 2, repeated=True) diff --git a/jetway/users/users.py b/jetway/users/users.py index ad2b227..1b6bf3c 100644 --- a/jetway/users/users.py +++ b/jetway/users/users.py @@ -132,6 +132,7 @@ def to_message(self): message = messages.UserMessage() if self.nickname: message.nickname = self.nickname + message.email = self.email message.ident = self.ident message.avatar_url = self.avatar_url message.description = self.description From 5526a6b520c3c3ada63cb0a534a03c21a0118123 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Wed, 11 Nov 2015 15:32:35 -0800 Subject: [PATCH 20/46] Groups and projects. Clean up. --- jetway/groups/groups.py | 8 +++++++- jetway/groups/messages.py | 2 +- jetway/projects/messages.py | 5 ----- jetway/projects/projects.py | 36 +++++++++++------------------------- 4 files changed, 19 insertions(+), 32 deletions(-) diff --git a/jetway/groups/groups.py b/jetway/groups/groups.py index abc3043..fb63ca2 100644 --- a/jetway/groups/groups.py +++ b/jetway/groups/groups.py @@ -9,7 +9,7 @@ class Error(Exception): class Group(ndb.Model): - memberships = ndb.StructuredProperty(repeated=True) + memberships = ndb.StructuredProperty(memberships.Membership, repeated=True) @property def ident(self): @@ -23,6 +23,12 @@ def get(cls, ident): raise Error('Group does not exist.') return group + @classmethod + def create(cls): + group = cls() + group.put() + return group + def validate(self): num_admins = 0 for mem in self.memberships: diff --git a/jetway/groups/messages.py b/jetway/groups/messages.py index 55876d8..af564ac 100644 --- a/jetway/groups/messages.py +++ b/jetway/groups/messages.py @@ -21,6 +21,6 @@ class MembershipMessage(messages.Message): class GroupMessage(messages.Message): - ident = messages.StringField(2) users = messages.MessageField(MembershipMessage, 1, repeated=True) domains = messages.MessageField(MembershipMessage, 2, repeated=True) + ident = messages.StringField(3) diff --git a/jetway/projects/messages.py b/jetway/projects/messages.py index a18920c..42de3f4 100644 --- a/jetway/projects/messages.py +++ b/jetway/projects/messages.py @@ -17,10 +17,6 @@ class Visibility(messages.Enum): DOMAIN = 5 -class CoverMessage(messages.Message): - content = messages.StringField(1) - - class Order(messages.Enum): NAME = 0 @@ -32,7 +28,6 @@ class ProjectMessage(messages.Message): description = messages.StringField(4) avatar_url = messages.StringField(6) visibility = messages.EnumField(Visibility, 7) - cover = messages.MessageField(CoverMessage, 8) name = messages.StringField(9) built = message_types.DateTimeField(10) buildbot_job_id = messages.StringField(11) diff --git a/jetway/projects/projects.py b/jetway/projects/projects.py index 91d56df..f0871d9 100644 --- a/jetway/projects/projects.py +++ b/jetway/projects/projects.py @@ -2,6 +2,7 @@ from ..buildbot import buildbot from ..buildbot import messages as buildbot_messages from ..catalogs import catalogs +from ..groups import groups from google.appengine.ext import ndb from google.appengine.ext.ndb import msgprop from jetway.avatars import avatars @@ -32,30 +33,17 @@ class ProjectDoesNotExistError(Error): pass -class Cover(ndb.Model): - content = ndb.StringProperty() - - @classmethod - def from_message(cls, message): - return cls(content=message.content) - - def to_message(self): - message = messages.CoverMessage() - message.content = self.content - return message - - class Project(ndb.Model): created = ndb.DateTimeProperty(auto_now_add=True) nickname = ndb.StringProperty() owner_key = ndb.KeyProperty() created_by_key = ndb.KeyProperty() description = ndb.StringProperty() - cover = ndb.StructuredProperty(Cover) visibility = msgprop.EnumProperty(messages.Visibility) built = ndb.DateTimeProperty() buildbot_job_id = ndb.StringProperty() git_url = ndb.StringProperty() + group_key = ndb.KeyProperty() @property def name(self): @@ -80,13 +68,6 @@ def computed_visibility(self): def ident(self): return str(self.key.id()) - @classmethod - def search_unknown_by_buildbot(cls): - query = cls.query() - query = query.filter(cls.known_by_buildbot == False) - results = query.fetch() - return results - @classmethod def create(cls, owner, nickname, created_by, description=None, git_url=None): try: @@ -204,6 +185,15 @@ def get_root(self): def owner(self): return owners.Owner.get_by_key(self.owner_key) + @property + def group(self): + if self.group_key is None: + group = groups.Group.create() + self.group_key = group.key + self.put() + return group + return self.group_key.get() + @property def avatar_url(self): return avatars.Avatar.create_url(self) @@ -223,15 +213,11 @@ def to_message(self): message.visibility = self.visibility message.git_url = self.git_url message.buildbot_job_id = self.buildbot_job_id - if self.cover: - message.cover = self.cover.to_message() message.built = self.built return message def update(self, message): self.description = message.description - if message.cover: - self.cover = Cover.from_message(message.cover) self.visibility = message.visibility if message.git_url != self.git_url: self._update_buildbot_job(message.git_url) From 51e2a88d759a8da8003fccfa3628d2c3e9f30dbd Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Wed, 11 Nov 2015 20:11:08 -0800 Subject: [PATCH 21/46] Get policies working. --- jetway/groups/groups.py | 10 ++++++++++ jetway/groups/messages.py | 2 +- jetway/policies/__init__.py | 0 jetway/policies/policies.py | 1 + jetway/policies/projects.py | 36 ++++++++++++++++++++++++++++++++++++ jetway/projects/projects.py | 2 +- jetway/projects/services.py | 18 +++++++----------- jetway/users/users.py | 4 ++++ 8 files changed, 60 insertions(+), 13 deletions(-) create mode 100644 jetway/policies/__init__.py create mode 100644 jetway/policies/policies.py create mode 100644 jetway/policies/projects.py diff --git a/jetway/groups/groups.py b/jetway/groups/groups.py index fb63ca2..dff0720 100644 --- a/jetway/groups/groups.py +++ b/jetway/groups/groups.py @@ -57,6 +57,7 @@ def delete_membership(self, membership_message): def list_memberships(self, kind=None): mems = [] + print 'foo', self.memberships for mem in self.memberships: if kind is None: mems.append(mem) @@ -74,3 +75,12 @@ def to_message(self): message.domains = [mem.to_message() for mem in self.list_memberships(messages.Kind.DOMAIN)] return message + + def get_membership(self, user): + mems = self.list_memberships() + for mem in mems: + print mem.user_key, user.key + if mem.user_key == user.key: + return mem + if mem.domain == user.domain: + return mem diff --git a/jetway/groups/messages.py b/jetway/groups/messages.py index af564ac..4d533ef 100644 --- a/jetway/groups/messages.py +++ b/jetway/groups/messages.py @@ -4,7 +4,7 @@ class Role(messages.Enum): ADMIN = 1 - READ_ONLY = 2 + READ = 2 WRITE = 3 WRITE_TRANSLATIONS = 4 diff --git a/jetway/policies/__init__.py b/jetway/policies/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/jetway/policies/policies.py b/jetway/policies/policies.py new file mode 100644 index 0000000..2e036f7 --- /dev/null +++ b/jetway/policies/policies.py @@ -0,0 +1 @@ +from .projects import * diff --git a/jetway/policies/projects.py b/jetway/policies/projects.py new file mode 100644 index 0000000..b226f14 --- /dev/null +++ b/jetway/policies/projects.py @@ -0,0 +1,36 @@ +from ..groups import groups +from ..groups import messages +from jetway import api + + +class Error(Exception): + pass + + +class ForbiddenError(Error, api.ForbiddenError): + pass + + +class ProjectPolicy(object): + + def __init__(self, user, project): + self.project = project + self.user = user + + def authorize_read(self): + if not self.can_read(): + raise ForbiddenError('{} cannot read {}'.format(self.user, self.project)) + + def can_read(self): + group = self.project.group + mem = group.get_membership(self.user) + if mem is None: + return False + return mem.role in [messages.Role.ADMIN, messages.Role.READ] + + def can_write(self): + group = self.project.group + mem = group.get_membership(self.user) + if mem is None: + return False + return mem.role in [messages.Role.ADMIN, messages.Role.WRITE] diff --git a/jetway/projects/projects.py b/jetway/projects/projects.py index f0871d9..fae9dec 100644 --- a/jetway/projects/projects.py +++ b/jetway/projects/projects.py @@ -187,7 +187,7 @@ def owner(self): @property def group(self): - if self.group_key is None: + if not self.group_key: group = groups.Group.create() self.group_key = group.key self.put() diff --git a/jetway/projects/services.py b/jetway/projects/services.py index e2869f7..3ba05d0 100644 --- a/jetway/projects/services.py +++ b/jetway/projects/services.py @@ -1,5 +1,6 @@ from . import service_messages from ..buildbot import buildbot +from ..policies import policies from jetway import api from jetway.owners import owners from jetway.projects import messages @@ -10,6 +11,9 @@ class ProjectService(api.Service): + def _get_policy(self, project): + return policies.ProjectPolicy(user=self.me, project=project) + def _get_project(self, request): try: if request.project.ident: @@ -56,6 +60,7 @@ def search(self, request): service_messages.UpdateProjectResponse) def update(self, request): project = self._get_project(request) + policy = self._get_policy(project) if not project.can(self.me, projects.Permission.ADMINISTER): raise api.ForbiddenError('Forbidden ({})'.format(self.me)) project.update(request.project) @@ -83,15 +88,6 @@ def delete(self, request): resp = service_messages.DeleteProjectResponse() return resp - @remote.method(service_messages.CanRequest, - service_messages.CanResponse) - def can(self, request): - project = self._get_project(request) - can = project.can(self.me, request.permission) - resp = service_messages.CanResponse() - resp.can = can - return resp - @remote.method(service_messages.GetProjectRequest, service_messages.CreateWatcherResponse) def watch(self, request): @@ -165,8 +161,8 @@ def delete_named_fileset(self, request): service_messages.ListBranchesResponse) def list_branches(self, request): project = self._get_project(request) - if not project.can(self.me, projects.Permission.READ): - raise api.ForbiddenError('Forbidden ({})'.format(self.me)) + policy = self._get_policy(project) + policy.authorize_read() try: branches = project.list_branches() except buildbot.Error as e: diff --git a/jetway/users/users.py b/jetway/users/users.py index 1b6bf3c..468ce20 100644 --- a/jetway/users/users.py +++ b/jetway/users/users.py @@ -28,6 +28,10 @@ class BaseUser(models.User): email = ndb.StringProperty() name = ndb.StringProperty() + @property + def domain(self): + return self.email.split('@')[-1] + def user_id(self): # Provides compatibility with oauth2client. return str(self.key.id()) From 32af7b51dbaf38bc4783ad77da2b2ccb58b84e3b Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Wed, 11 Nov 2015 20:34:44 -0800 Subject: [PATCH 22/46] Use policies on service. --- jetway/policies/projects.py | 33 ++++++++++++++++++++++-------- jetway/projects/projects.py | 9 +++++++-- jetway/projects/services.py | 40 ++++++++++++------------------------- 3 files changed, 45 insertions(+), 37 deletions(-) diff --git a/jetway/policies/projects.py b/jetway/policies/projects.py index b226f14..d677592 100644 --- a/jetway/policies/projects.py +++ b/jetway/policies/projects.py @@ -16,21 +16,38 @@ class ProjectPolicy(object): def __init__(self, user, project): self.project = project self.user = user + self.mem = self.project.group.get_membership(self.user) + self.is_owner = self.project.owner_key == user.key + + def authorize_admin(self): + if not self.can_write(): + raise ForbiddenError('{} is not an admin for {}'.format(self.user, self.project)) + + def authorize_write(self): + if not self.can_write(): + raise ForbiddenError('{} cannot write to {}'.format(self.user, self.project)) def authorize_read(self): if not self.can_read(): raise ForbiddenError('{} cannot read {}'.format(self.user, self.project)) + def can_administer(self): + if self.is_owner: + return True + if self.mem is None: + return False + return self.mem.role in [messages.Role.ADMIN] + def can_read(self): - group = self.project.group - mem = group.get_membership(self.user) - if mem is None: + if self.is_owner: + return True + if self.mem is None: return False - return mem.role in [messages.Role.ADMIN, messages.Role.READ] + return self.mem.role in [messages.Role.ADMIN, messages.Role.READ] def can_write(self): - group = self.project.group - mem = group.get_membership(self.user) - if mem is None: + if self.is_owner: + return True + if self.mem is None: return False - return mem.role in [messages.Role.ADMIN, messages.Role.WRITE] + return self.mem.role in [messages.Role.ADMIN, messages.Role.WRITE] diff --git a/jetway/projects/projects.py b/jetway/projects/projects.py index fae9dec..e875aab 100644 --- a/jetway/projects/projects.py +++ b/jetway/projects/projects.py @@ -187,12 +187,17 @@ def owner(self): @property def group(self): - if not self.group_key: + def _create_group(): group = groups.Group.create() self.group_key = group.key self.put() return group - return self.group_key.get() + if not self.group_key: + return _create_group() + group = self.group_key.get() + if group is None: + return _create_group() + return group @property def avatar_url(self): diff --git a/jetway/projects/services.py b/jetway/projects/services.py index 3ba05d0..acfe214 100644 --- a/jetway/projects/services.py +++ b/jetway/projects/services.py @@ -60,9 +60,7 @@ def search(self, request): service_messages.UpdateProjectResponse) def update(self, request): project = self._get_project(request) - policy = self._get_policy(project) - if not project.can(self.me, projects.Permission.ADMINISTER): - raise api.ForbiddenError('Forbidden ({})'.format(self.me)) + self._get_policy(project).authorize_admin() project.update(request.project) resp = service_messages.UpdateProjectResponse() resp.project = project.to_message() @@ -72,8 +70,7 @@ def update(self, request): service_messages.GetProjectResponse) def get(self, request): project = self._get_project(request) - if not project.can(self.me, projects.Permission.READ): - raise api.ForbiddenError('Forbidden ({})'.format(self.me)) + self._get_policy(project).authorize_read() resp = service_messages.GetProjectResponse() resp.project = project.to_message() return resp @@ -82,8 +79,7 @@ def get(self, request): service_messages.DeleteProjectResponse) def delete(self, request): project = self._get_project(request) - if not project.can(self.me, projects.Permission.ADMINISTER): - raise api.ForbiddenError('Forbidden ({})'.format(self.me)) + self._get_policy(project).authorize_admin() project.delete() resp = service_messages.DeleteProjectResponse() return resp @@ -92,8 +88,7 @@ def delete(self, request): service_messages.CreateWatcherResponse) def watch(self, request): project = self._get_project(request) - if not project.can(self.me, projects.Permission.READ): - raise api.ForbiddenError('Forbidden ({})'.format(self.me)) + self._get_policy(project).authorize_read() watcher = project.create_watcher(self.me) resp = service_messages.CreateWatcherResponse() resp.watcher = watcher.to_message() @@ -103,8 +98,7 @@ def watch(self, request): service_messages.ListWatchersResponse) def unwatch(self, request): project = self._get_project(request) - if not project.can(self.me, projects.Permission.READ): - raise api.ForbiddenError('Forbidden ({})'.format(self.me)) + self._get_policy(project).authorize_read() project.delete_watcher(self.me) watchers = project.list_watchers() resp = service_messages.ListWatchersResponse() @@ -115,8 +109,7 @@ def unwatch(self, request): service_messages.ListWatchersResponse) def list_watchers(self, request): project = self._get_project(request) - if not project.can(self.me, projects.Permission.READ): - raise api.ForbiddenError('Forbidden ({})'.format(self.me)) + self._get_policy(project).authorize_read() watchers = project.list_watchers() resp = service_messages.ListWatchersResponse() resp.watching = any(self.me == watcher.user for watcher in watchers) @@ -127,8 +120,7 @@ def list_watchers(self, request): service_messages.ListNamedFilesetsResponse) def list_named_filesets(self, request): project = self._get_project(request) - if not project.can(self.me, projects.Permission.READ): - raise api.ForbiddenError('Forbidden ({})'.format(self.me)) + self._get_policy(project).authorize_read() named_filesets = project.list_named_filesets() resp = service_messages.ListNamedFilesetsRequest() resp.named_filesets = [named_fileset.to_message() @@ -139,8 +131,7 @@ def list_named_filesets(self, request): service_messages.CreateNamedFilesetResponse) def create_named_fileset(self, request): project = self._get_project(request) - if not project.can(self.me, projects.Permission.WRITE): - raise api.ForbiddenError('Forbidden ({})'.format(self.me)) + self._get_policy(project).authorize_write() named_fileset = project.create_named_fileset( request.named_fileset.name, request.named_fileset.branch) resp = service_messages.CreateNamedFilesetResponse() @@ -151,8 +142,7 @@ def create_named_fileset(self, request): service_messages.DeleteNamedFilesetResponse) def delete_named_fileset(self, request): project = self._get_project(request) - if not project.can(self.me, projects.Permission.READ): - raise api.ForbiddenError('Forbidden ({})'.format(self.me)) + self._get_policy(project).authorize_write() project.delete_named_fileset(request.named_fileset.name) resp = service_messages.DeleteNamedFilesetResponse() return resp @@ -161,8 +151,7 @@ def delete_named_fileset(self, request): service_messages.ListBranchesResponse) def list_branches(self, request): project = self._get_project(request) - policy = self._get_policy(project) - policy.authorize_read() + self._get_policy(project).authorize_read() try: branches = project.list_branches() except buildbot.Error as e: @@ -175,8 +164,7 @@ def list_branches(self, request): service_messages.ListCatalogsResponse) def list_catalogs(self, request): project = self._get_project(request) - if not project.can(self.me, projects.Permission.READ): - raise api.ForbiddenError('Forbidden ({})'.format(self.me)) + self._get_policy(project).authorize_read() try: catalogs = project.list_catalogs() except buildbot.Error as e: @@ -189,8 +177,7 @@ def list_catalogs(self, request): service_messages.CatalogResponse) def get_catalog(self, request): project = self._get_project(request) - if not project.can(self.me, projects.Permission.READ): - raise api.ForbiddenError('Forbidden ({})'.format(self.me)) + self._get_policy(project).authorize_read() try: catalog = project.get_catalog(request.catalog.locale) except buildbot.Error as e: @@ -203,8 +190,7 @@ def get_catalog(self, request): service_messages.CatalogResponse) def update_translations(self, request): project = self._get_project(request) - if not project.can(self.me, projects.Permission.READ): - raise api.ForbiddenError('Forbidden ({})'.format(self.me)) + self._get_policy(project).authorize_write() try: catalog = project.get_catalog(request.catalog.locale) except buildbot.Error as e: From efe3e7a25fe91bb9e3dbcf8629277e8f32787eaa Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Wed, 11 Nov 2015 22:00:36 -0800 Subject: [PATCH 23/46] Replace legacy can with policies. --- jetway/filesets/services.py | 7 +++- jetway/groups/groups.py | 5 ++- jetway/groups/memberships.py | 7 +++- jetway/groups/messages.py | 2 + jetway/policies/projects.py | 4 +- jetway/projects/projects.py | 59 +++-------------------------- jetway/projects/service_messages.py | 10 +++++ jetway/projects/services.py | 29 ++++++++++++++ jetway/server/handlers.py | 5 ++- 9 files changed, 65 insertions(+), 63 deletions(-) diff --git a/jetway/filesets/services.py b/jetway/filesets/services.py index 863f36e..e364f5d 100644 --- a/jetway/filesets/services.py +++ b/jetway/filesets/services.py @@ -3,6 +3,7 @@ from jetway.filesets import messages from jetway.owners import owners from jetway.projects import projects +from jetway.policies import policies from jetway.users import users from protorpc import remote import appengine_config @@ -117,8 +118,9 @@ def search(self, request): project = projects.Project.get(owner, request.fileset.project.nickname) else: project = None + policy = policies.ProjectPolicy(self.me, project) if (not self._is_authorized_buildbot() - and not project.can(self.me, projects.Permission.READ)): + and not policy.can_read()): raise api.ForbiddenError('Forbidden.') results = filesets.Fileset.search(project=project) resp = messages.SearchFilesetResponse() @@ -196,8 +198,9 @@ def finalize(self, request): def sign_requests(self, request): me = self._get_me(request) fileset = self._get_or_create_fileset(request, me) + policy = policies.ProjectPolicy(me, fileset.project) if (not self._is_authorized_buildbot() - and not fileset.project.can(me, projects.Permission.WRITE)): + and not policy.can_write()): raise api.ForbiddenError('Forbidden.') signed_reqs = fileset.sign_requests(request.unsigned_requests) resp = messages.SignRequestsResponse() diff --git a/jetway/groups/groups.py b/jetway/groups/groups.py index dff0720..2ee5a46 100644 --- a/jetway/groups/groups.py +++ b/jetway/groups/groups.py @@ -10,6 +10,7 @@ class Error(Exception): class Group(ndb.Model): memberships = ndb.StructuredProperty(memberships.Membership, repeated=True) + project_key = ndb.KeyProperty() @property def ident(self): @@ -57,7 +58,6 @@ def delete_membership(self, membership_message): def list_memberships(self, kind=None): mems = [] - print 'foo', self.memberships for mem in self.memberships: if kind is None: mems.append(mem) @@ -70,6 +70,8 @@ def list_memberships(self, kind=None): def to_message(self): message = messages.GroupMessage() message.ident = self.ident + if self.project: + message.project = self.project.to_message() message.users = [mem.to_message() for mem in self.list_memberships(messages.Kind.USER)] message.domains = [mem.to_message() @@ -79,7 +81,6 @@ def to_message(self): def get_membership(self, user): mems = self.list_memberships() for mem in mems: - print mem.user_key, user.key if mem.user_key == user.key: return mem if mem.domain == user.domain: diff --git a/jetway/groups/memberships.py b/jetway/groups/memberships.py index 9815444..c717c18 100644 --- a/jetway/groups/memberships.py +++ b/jetway/groups/memberships.py @@ -30,7 +30,12 @@ def from_message(cls, message): mem = cls() mem.role = message.role if message.user: - user = users.User.get_by_ident(message.user.ident) + if message.user.email: + user = users.User.get_or_create_by_email(message.user.email) + elif message.user.ident: + user = users.User.get_by_ident(message.user.email) + else: + raise ValueError('User not found.') mem.user_key = user.key if message.domain: mem.domain = message.domain diff --git a/jetway/groups/messages.py b/jetway/groups/messages.py index 4d533ef..4c10775 100644 --- a/jetway/groups/messages.py +++ b/jetway/groups/messages.py @@ -1,3 +1,4 @@ +from ..projects import messages as project_messages from ..users import messages as user_messages from protorpc import messages @@ -24,3 +25,4 @@ class GroupMessage(messages.Message): users = messages.MessageField(MembershipMessage, 1, repeated=True) domains = messages.MessageField(MembershipMessage, 2, repeated=True) ident = messages.StringField(3) + project = messages.MessageField(project_messages.ProjectMessage, 4) diff --git a/jetway/policies/projects.py b/jetway/policies/projects.py index d677592..34cc8c9 100644 --- a/jetway/policies/projects.py +++ b/jetway/policies/projects.py @@ -1,13 +1,13 @@ from ..groups import groups from ..groups import messages -from jetway import api +from jetway import api_errors class Error(Exception): pass -class ForbiddenError(Error, api.ForbiddenError): +class ForbiddenError(Error, api_errors.ForbiddenError): pass diff --git a/jetway/projects/projects.py b/jetway/projects/projects.py index e875aab..c28d4c2 100644 --- a/jetway/projects/projects.py +++ b/jetway/projects/projects.py @@ -3,6 +3,7 @@ from ..buildbot import messages as buildbot_messages from ..catalogs import catalogs from ..groups import groups +from ..policies import policies from google.appengine.ext import ndb from google.appengine.ext.ndb import msgprop from jetway.avatars import avatars @@ -191,12 +192,14 @@ def _create_group(): group = groups.Group.create() self.group_key = group.key self.put() + group.project = self return group if not self.group_key: return _create_group() group = self.group_key.get() if group is None: return _create_group() + group.project = self return group @property @@ -246,65 +249,13 @@ def search_users(self, is_public=True): @classmethod def filter(cls, results, user, permission=messages.Permission.READ): + policy = policies.ProjectPolicy(user, self) filtered = [] for project in results: - if project.can(user, permission): + if policy.can_read(): filtered.append(project) return filtered - def can(self, user, permission=messages.Permission.READ): - if not user: - return False - # TODO: Implement proper domain-level access controls. - username, domain = user.email.split('@') - if username in appengine_config.DOMAIN_ACCESS_USERS: - return True - if (appengine_config.DEFAULT_USER_DOMAINS and - domain in appengine_config.DEFAULT_USER_DOMAINS): - return True - # Permit the owner. - if self.owner == user: - return True - # Permit domain users. - if (appengine_config.DEFAULT_USER_DOMAINS - and self.computed_visibility == messages.Visibility.DOMAIN - and user.email.split('@')[-1] in appengine_config.DEFAULT_USER_DOMAINS): - return - if self.computed_visibility in [messages.Visibility.PUBLIC, messages.Visibility.COVER]: - if appengine_config.DEFAULT_USER_DOMAINS: - return domain in appengine_config.DEFAULT_USER_DOMAINS - else: - return True - query = teams.Team.query(ndb.OR(# Project teams. - ndb.AND(teams.Team.project_keys == self.key, - teams.Team.user_keys == user.key), - # Org Owners team. - ndb.AND(teams.Team.kind == teams.messages.Kind.ORG_OWNERS, - teams.Team.owner_key == self.owner.key, - teams.Team.user_keys == user.key))) - found_teams = query.fetch() - # Permit users part of the project's organization. - if self.computed_visibility == messages.Visibility.ORGANIZATION: - return bool(found_teams) - if self.computed_visibility in [messages.Visibility.PRIVATE, messages.Visibility.DOMAIN]: - # TODO: Remove this once exposing default permissions in UI. - if (appengine_config.DEFAULT_USER_DOMAINS - and user.email.split('@')[-1] in appengine_config.DEFAULT_USER_DOMAINS): - return True - for team in found_teams: - membership = team.get_membership(user) - if not membership: - continue - # Org owners have access to all projects. - if team.kind == teams.messages.Kind.PROJECT_OWNERS: - return True - elif team.kind == teams.messages.Kind.ORG_OWNERS: - return True - # If the user is in a team that has this project, return True. - if self.key in team.project_keys: - return True - return False - def create_watcher(self, user): return watchers.Watcher.create(project=self, user=user) diff --git a/jetway/projects/service_messages.py b/jetway/projects/service_messages.py index 1ee094d..82f70fe 100644 --- a/jetway/projects/service_messages.py +++ b/jetway/projects/service_messages.py @@ -1,6 +1,7 @@ from ..buildbot import messages as buildbot_messages from ..catalogs import messages as catalog_messages from ..filesets.named_fileset_messages import * +from ..groups import messages as group_messages from .messages import * from .watcher_messages import * from protorpc import messages @@ -125,3 +126,12 @@ class CatalogRequest(messages.Message): class CatalogResponse(messages.Message): catalog = messages.MessageField(catalog_messages.CatalogMessage, 1) + + +class GroupResponse(messages.Message): + group = messages.MessageField(group_messages.GroupMessage, 1) + + +class MembershipRequest(messages.Message): + project = messages.MessageField(ProjectMessage, 1) + membership = messages.MessageField(group_messages.MembershipMessage, 2) diff --git a/jetway/projects/services.py b/jetway/projects/services.py index acfe214..1b27267 100644 --- a/jetway/projects/services.py +++ b/jetway/projects/services.py @@ -215,3 +215,32 @@ def update_translations(self, request): resp = service_messages.CatalogResponse() resp.catalog = catalog.to_message() return resp + + @remote.method(service_messages.ProjectRequest, + service_messages.GroupResponse) + def get_group(self, request): + project = self._get_project(request) + self._get_policy(project).authorize_read() + resp = service_messages.GroupResponse() + resp.group = project.group.to_message() + return resp + + @remote.method(service_messages.MembershipRequest, + service_messages.GroupResponse) + def create_membership(self, request): + project = self._get_project(request) + self._get_policy(project).authorize_admin() + project.group.create_membership(request.membership) + resp = service_messages.GroupResponse() + resp.group = project.group.to_message() + return resp + + @remote.method(service_messages.MembershipRequest, + service_messages.GroupResponse) + def delete_membership(self, request): + project = self._get_project(request) + self._get_policy(project).authorize_admin() + project.group.delete_membership(request.membership) + resp = service_messages.GroupResponse() + resp.group = project.group.to_message() + return resp diff --git a/jetway/server/handlers.py b/jetway/server/handlers.py index b36fc89..cc33894 100644 --- a/jetway/server/handlers.py +++ b/jetway/server/handlers.py @@ -3,6 +3,7 @@ from jetway.files import files from jetway.filesets import filesets from jetway.owners import owners +from jetway.policies import policies from jetway.projects import projects from jetway.server import utils import jinja2 @@ -51,8 +52,8 @@ def get(self): if fileset_name is None: raise filesets.FilesetDoesNotExistError fileset = filesets.Fileset.get_by_name_or_ident(fileset_name) - if (not self._is_open_path(self.request.path) - and not fileset.project.can(self.me, projects.Permission.READ)): + policy = policies.ProjectPolicy(self.me, fileset.project) + if not self._is_open_path(self.request.path) and not policy.can_read(): if self.me: text = '{} does not have access to this page.'.format(self.me) self.error(403, 'Forbidden', text) From 61002e5dc06d7710b9a2452b564d86c9e3c3756c Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Wed, 11 Nov 2015 23:45:36 -0800 Subject: [PATCH 24/46] Delete teams in favor of groups. --- jetway/groups/groups.py | 8 +- jetway/launches/launches.py | 16 +-- jetway/main.py | 2 - jetway/orgs/orgs.py | 35 ------ jetway/owners/owners.py | 14 --- jetway/projects/projects.py | 27 +---- jetway/teams/__init__.py | 0 jetway/teams/messages.py | 146 ----------------------- jetway/teams/services.py | 177 ---------------------------- jetway/teams/teams.py | 228 ------------------------------------ jetway/users/users.py | 17 ++- 11 files changed, 18 insertions(+), 652 deletions(-) delete mode 100644 jetway/teams/__init__.py delete mode 100644 jetway/teams/messages.py delete mode 100644 jetway/teams/services.py delete mode 100644 jetway/teams/teams.py diff --git a/jetway/groups/groups.py b/jetway/groups/groups.py index 2ee5a46..878d3f7 100644 --- a/jetway/groups/groups.py +++ b/jetway/groups/groups.py @@ -25,8 +25,10 @@ def get(cls, ident): return group @classmethod - def create(cls): + def create(cls, project=None): group = cls() + if project: + group.project_key = project group.put() return group @@ -36,7 +38,9 @@ def validate(self): if mem.role == messages.Role.ADMIN: num_admins += 1 if num_admins < 1: - raise memberships.MembershipConflictError('Must be at least one admin.') + pass + # TODO: Decide if we need this check. + # raise memberships.MembershipConflictError('Must be at least one admin.') def create_membership(self, membership_message): mem = memberships.Membership.from_message(membership_message) diff --git a/jetway/launches/launches.py b/jetway/launches/launches.py index 2482a57..5057567 100644 --- a/jetway/launches/launches.py +++ b/jetway/launches/launches.py @@ -2,7 +2,6 @@ from jetway.filesets import filesets from jetway.projects import projects from jetway.launches import messages -from jetway.teams import teams class Error(Exception): @@ -127,19 +126,8 @@ def num_comments(self): @property def reviewers(self): - results = teams.Team.search(projects=[self.project]) - team_user_keys = set() - for team in results: - for membership in team.memberships: - if membership.review_required: - team_user_keys.add(membership.user_key) - team_users = ndb.get_multi(list(team_user_keys)) - reviewers = [] - reviewers.extend(self.additional_reviewers) - for user in team_users: - reviewers.append(Reviewer( - user_key=user.key)) - return reviewers + # TODO: Implement. + return [] def to_message(self): message = messages.LaunchMessage() diff --git a/jetway/main.py b/jetway/main.py index be24e5b..170b478 100644 --- a/jetway/main.py +++ b/jetway/main.py @@ -12,7 +12,6 @@ from .projects import services as project_services from .server import handlers as server_handlers from .server import utils -from .teams import services as team_services from .users import services as user_services from protorpc.wsgi import service import endpoints @@ -56,7 +55,6 @@ ('/_api/owners.*', owner_services.OwnerService), ('/_api/orgs.*', org_services.OrgService), ('/_api/projects.*', project_services.ProjectService), - ('/_api/teams.*', team_services.TeamService), ('/_api/users.*', user_services.UserService), ), registry_path='/_api/protorpc') diff --git a/jetway/orgs/orgs.py b/jetway/orgs/orgs.py index 8fa92b3..c7ff3c2 100644 --- a/jetway/orgs/orgs.py +++ b/jetway/orgs/orgs.py @@ -1,7 +1,6 @@ from . import memberships from . import messages from ..avatars import avatars -from ..teams import teams from google.appengine.ext import ndb import os @@ -45,17 +44,12 @@ def create(cls, nickname, created_by): nickname=nickname, created_by_key=created_by.key) org.put() - teams.Team.create(org, None, created_by=created_by, - kind=teams.messages.Kind.ORG_OWNERS) return org def delete(self): from jetway.projects import projects results = projects.Project.search(owner=self) if results: - raise OrgConflictError('Cannot delete organizations that have projects.') - results = teams.Team.search(owner=self) - for team in results: team.delete() self.key.delete() @@ -94,35 +88,6 @@ def list(cls): query = cls.query() return query.fetch() - def search_members(self): - team_objs = teams.Team.search(owner=self) - user_keys = [] - for team in team_objs: - user_keys += team.user_keys - return ndb.get_multi(list(set(user_keys))) - - def get_team_membership(self, user): - team_objs = self.list_teams() - team_keys = [team.key for team in team_objs] - query = teams.TeamMembership.query() - query = query.filter(teams.TeamMembership.parent_key.IN(team_keys)) - query = query.filter(teams.TeamMembership.user_key == user.key) - results = query.fetch(1) - result = results[0] if len(results) else None - if result is None: - text = '{} is not a member of any team in {}.' - raise memberships.MembershipDoesNotExistError(text.format(user, self)) - return result - - def create_team_membership(self, team, user, role): - # multipel teams, so parent isnt always the same - try: - self.get_team_membership(user) - text = '{} is already a member of a team in {}.' - raise memberships.MembershipExistsError(text.format(user, self)) - except memberships.MembershipDoesNotExistError: - return team.create_membership(user, role) - @property def avatar_url(self): return avatars.Avatar.create_url(self) diff --git a/jetway/owners/owners.py b/jetway/owners/owners.py index a32845e..866898d 100644 --- a/jetway/owners/owners.py +++ b/jetway/owners/owners.py @@ -3,7 +3,6 @@ from jetway.orgs import orgs from jetway.owners import messages from jetway.users import users -from jetway.teams import teams from jetway import api_errors as api @@ -107,16 +106,3 @@ def to_message(self): message.website_url = self._entity.website_url message.avatar_url = self.avatar_url return message - - def create_team(self, nickname, created_by): - return teams.Team.create(self, nickname, created_by) - - def search_teams(self): - return teams.Team.search(owner=self) - - def get_team(self, nickname): - return teams.Team.get(self, nickname) - - def delete_team(self, nickname): - team = self.get_team(nickname) - team.delete() diff --git a/jetway/projects/projects.py b/jetway/projects/projects.py index c28d4c2..3b31eb0 100644 --- a/jetway/projects/projects.py +++ b/jetway/projects/projects.py @@ -11,7 +11,6 @@ from jetway.filesets import named_filesets from jetway.owners import owners from jetway.projects import messages -from jetway.teams import teams from protorpc import protojson import appengine_config import json @@ -85,9 +84,6 @@ def create(cls, owner, nickname, created_by, description=None, git_url=None): description=description) project.put() project._update_buildbot_job(project.git_url) - teams.Team.create(owner, None, - created_by=created_by, project=project, - kind=teams.messages.Kind.PROJECT_OWNERS) return project @classmethod @@ -140,22 +136,11 @@ def search(cls, owner=None, order=None): def delete(self): from jetway.launches import launches - team_results = teams.Team.search( - projects=[self], - kind=teams.messages.Kind.DEFAULT) launch_results = launches.Launch.search(project=self) fileset_results = filesets.Fileset.search(project=self) @ndb.transactional(retries=1, xg=True) def _delete_project(): - try: - project_team = teams.Team.get( - self.ident, teams.messages.Kind.PROJECT_OWNERS) - project_team.delete() - except teams.TeamDoesNotExistError: - pass - for team in team_results: - team.remove_project(self) for launch in launch_results: launch.delete() for fileset in fileset_results: @@ -171,9 +156,6 @@ def create_fileset(self, name, commit=None): def get_fileset(self, name): return filesets.Fileset.get(project=self, name=name) - def get_team(self): - return teams.Team.get(self.ident, teams.messages.Kind.PROJECT_OWNERS) - def search_filesets(self): query = filesets.Fileset.query() query = query.filter(filesets.Fileset.project_key == self.key) @@ -232,18 +214,13 @@ def update(self, message): self.git_url = message.git_url self.put() - def search_teams(self, users=None): - return teams.Team.search(projects=[self], users=None) - def list_users_to_notify(self): return self.search_users(is_public=None) def search_users(self, is_public=True): - team = self.get_team() users = [] - for membership in team.memberships: - if (is_public is None - or membership.is_public == is_public): + for membership in self.group.memberships: + if membership.user_key: users.append(membership.user) return users diff --git a/jetway/teams/__init__.py b/jetway/teams/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/jetway/teams/messages.py b/jetway/teams/messages.py deleted file mode 100644 index 9188f51..0000000 --- a/jetway/teams/messages.py +++ /dev/null @@ -1,146 +0,0 @@ -from protorpc import messages -from protorpc import message_types -from jetway.owners import messages as owner_messages -from jetway.projects import messages as project_messages -from jetway.users import messages as user_messages - - -class Role(messages.Enum): - ADMIN = 1 - READ_ONLY = 2 - WRITE_FULL = 3 - WRITE_TRANSLATIONS = 4 - WRITE_CONTENT = 5 - - -class Kind(messages.Enum): - DEFAULT = 1 - ORG_OWNERS = 2 - PROJECT_OWNERS = 3 - - -class PermissionsMessage(messages.Message): - administer = messages.BooleanField(1, default=False) - - -class TeamMembershipMessage(messages.Message): - user = messages.MessageField(user_messages.UserMessage, 1) - role = messages.EnumField(Role, 2) - is_public = messages.BooleanField(3) - review_required = messages.BooleanField(4) - - -class TeamMessage(messages.Message): - nickname = messages.StringField(1) - perms = messages.MessageField(PermissionsMessage, 2) - projects = messages.MessageField(project_messages.ProjectMessage, 3, repeated=True) - description = messages.StringField(4) - modified = message_types.DateTimeField(5) - memberships = messages.MessageField(TeamMembershipMessage, 6, repeated=True) - role = messages.EnumField(Role, 7) - num_projects = messages.IntegerField(8) - owner = messages.MessageField(owner_messages.OwnerMessage, 9) - ident = messages.StringField(10) - kind = messages.EnumField(Kind, 11) - letter = messages.StringField(12) - title = messages.StringField(13) - - -### - - -class CreateTeamRequest(messages.Message): - team = messages.MessageField(TeamMessage, 1) - owner = messages.MessageField(owner_messages.OwnerMessage, 2) - - -class CreateTeamResponse(messages.Message): - team = messages.MessageField(TeamMessage, 1) - - -class SearchTeamRequest(messages.Message): - team = messages.MessageField(TeamMessage, 1) - owner = messages.MessageField(owner_messages.OwnerMessage, 2) - project = messages.MessageField(project_messages.ProjectMessage, 3) - - -class SearchTeamResponse(messages.Message): - teams = messages.MessageField(TeamMessage, 1, repeated=True) - - -class GetTeamRequest(messages.Message): - team = messages.MessageField(TeamMessage, 1) - owner = messages.MessageField(owner_messages.OwnerMessage, 2) - - -class GetTeamResponse(messages.Message): - team = messages.MessageField(TeamMessage, 1) - - -class UpdateTeamRequest(messages.Message): - team = messages.MessageField(TeamMessage, 1) - owner = messages.MessageField(owner_messages.OwnerMessage, 2) - - -class UpdateTeamResponse(messages.Message): - team = messages.MessageField(TeamMessage, 1) - - -class DeleteTeamRequest(messages.Message): - team = messages.MessageField(TeamMessage, 1) - owner = messages.MessageField(owner_messages.OwnerMessage, 2) - project = messages.MessageField(project_messages.ProjectMessage, 3) - - -class DeleteTeamResponse(messages.Message): - pass - - -class AddProjectRequest(messages.Message): - team = messages.MessageField(TeamMessage, 1) - owner = messages.MessageField(owner_messages.OwnerMessage, 2) - project = messages.MessageField(project_messages.ProjectMessage, 3) - - -class AddProjectResponse(messages.Message): - team = messages.MessageField(TeamMessage, 1) - - -class RemoveProjectRequest(messages.Message): - team = messages.MessageField(TeamMessage, 1) - owner = messages.MessageField(owner_messages.OwnerMessage, 2) - project = messages.MessageField(project_messages.ProjectMessage, 3) - - -class RemoveProjectResponse(messages.Message): - team = messages.MessageField(TeamMessage, 1) - - -class CreateMembershipRequest(messages.Message): - team = messages.MessageField(TeamMessage, 1) - owner = messages.MessageField(owner_messages.OwnerMessage, 2) - membership = messages.MessageField(TeamMembershipMessage, 3) - - -class CreateMembershipResponse(messages.Message): - team = messages.MessageField(TeamMessage, 1) - - -class DeleteMembershipRequest(messages.Message): - team = messages.MessageField(TeamMessage, 1) - owner = messages.MessageField(owner_messages.OwnerMessage, 2) - membership = messages.MessageField(TeamMembershipMessage, 3) - - -class DeleteMembershipResponse(messages.Message): - team = messages.MessageField(TeamMessage, 1) - - -class UpdateMembershipRequest(messages.Message): - team = messages.MessageField(TeamMessage, 1) - owner = messages.MessageField(owner_messages.OwnerMessage, 2) - membership = messages.MessageField(TeamMembershipMessage, 3) - - -class UpdateMembershipResponse(messages.Message): - team = messages.MessageField(TeamMessage, 1) diff --git a/jetway/teams/services.py b/jetway/teams/services.py deleted file mode 100644 index 2c9b812..0000000 --- a/jetway/teams/services.py +++ /dev/null @@ -1,177 +0,0 @@ -from jetway import api -from jetway.teams import teams -from jetway.owners import owners -from jetway.teams import messages -from jetway.projects import projects -from jetway.users import users -from protorpc import remote - - -class TeamService(api.Service): - - def _check_permission(self, team, permission): - if team.kind == teams.messages.Kind.PROJECT_OWNERS: - project = team.projects[0] - if not project.can(self.me, permission): - raise api.ForbiddenError('Forbidden.') - - def _get_projects(self, request): - project_ents = [] - if request.team.projects: - for project in request.team.projects: - owner = self._get_owner(request) - project_ents.append(self._get_project(owner, project.nickname)) - return project_ents - - def _get_user(self, request): - if request.membership.user.ident: - return users.User.get_by_ident(request.membership.user.ident) - elif request.membership.user.email: - return users.User.get_or_create_by_email(request.membership.user.email) - else: - return users.User.get(request.user.nickname) - - def _get_project(self, owner, nickname): - try: - return projects.Project.get(owner, nickname) - except projects.ProjectDoesNotExistError as e: - raise api.NotFoundError(str(e)) - - def _get_team(self, request): - try: - if request.team.ident: - return teams.Team.get(request.team.ident, request.team.kind) - owner = self.get_owner(request) - return owner.get_team(request.team.nickname) - except teams.TeamDoesNotExistError as e: - raise api.NotFoundError(str(e)) - - def _get_owner(self, request): - return owners.Owner.get(request.team.owner.nickname) - - @remote.method( - messages.CreateTeamRequest, - messages.CreateTeamResponse) - def create(self, request): - owner = self._get_owner(request) - try: - team = owner.create_team(request.team.nickname, created_by=self.me) - except teams.TeamExistsError as e: - raise api.ConflictError(str(e)) - message = messages.CreateTeamResponse() - message.team = team.to_message() - return message - - @remote.method( - messages.SearchTeamRequest, - messages.SearchTeamResponse) - def search(self, request): - owner = self._get_owner(request) if request.team.owner else None - project_ents = self._get_projects(request) - results = teams.Team.search(owner=owner, projects=project_ents, kind=request.team.kind) - message = messages.SearchTeamResponse() - message.teams = [team.to_message() for team in results] - return message - - @remote.method( - messages.DeleteTeamRequest, - messages.DeleteTeamResponse) - def delete(self, request): - team = self._get_team(request) - team.delete() - message = messages.DeleteTeamResponse() - return message - - @remote.method( - messages.GetTeamRequest, - messages.GetTeamResponse) - def get(self, request): - team = self._get_team(request) - message = messages.GetTeamResponse() - message.team = team.to_message() - return message - - @remote.method( - messages.UpdateTeamRequest, - messages.UpdateTeamResponse) - def update(self, request): - team = self._get_team(request) - team.update(request.team) - message = messages.UpdateTeamResponse() - message.team = team.to_message() - return message - - @remote.method( - messages.AddProjectRequest, - messages.AddProjectResponse) - def add_project(self, request): - team = self._get_team(request) - self._check_permission(team, projects.Permission.ADMINISTER) - project = self._get_project(team.owner, request.project.nickname) - try: - team.add_project(project) - except teams.ProjectConflictError as e: - raise api.ConflictError(str(e)) - resp = messages.AddProjectResponse() - resp.team = team.to_message() - return resp - - @remote.method( - messages.RemoveProjectRequest, - messages.RemoveProjectResponse) - def remove_project(self, request): - owner = self.get_owner(request) - team = owner.get_team(request.team.nickname) - self._check_permission(team, projects.Permission.ADMINISTER) - project = self._get_project(owner, request.project.nickname) - try: - team.remove_project(project) - except teams.ProjectConflictError as e: - raise api.ConflictError(str(e)) - resp = messages.RemoveProjectResponse() - resp.team = team.to_message() - return resp - - @remote.method( - messages.CreateMembershipRequest, - messages.CreateMembershipResponse) - def create_membership(self, request): - team = self._get_team(request) - self._check_permission(team, projects.Permission.ADMINISTER) - user = self._get_user(request) - try: - team.create_membership(user, role=request.membership.role) - resp = messages.CreateMembershipResponse() - resp.team = team.to_message() - return resp - except teams.MembershipConflictError as e: - raise api.ConflictError(str(e)) - - @remote.method( - messages.DeleteMembershipRequest, - messages.DeleteMembershipResponse) - def delete_membership(self, request): - team = self._get_team(request) - self._check_permission(team, projects.Permission.ADMINISTER) - user = self._get_user(request) - try: - team.delete_membership(user) - resp = messages.DeleteMembershipResponse() - resp.team = team.to_message() - return resp - except teams.MembershipConflictError as e: - raise api.NotFoundError(str(e)) - - @remote.method( - messages.UpdateMembershipRequest, - messages.UpdateMembershipResponse) - def update_membership(self, request): - team = self._get_team(request) - self._check_permission(team, projects.Permission.ADMINISTER) - user = self._get_user(request) - team.update_membership(user, - role=request.membership.role, - is_public=request.membership.is_public) - resp = messages.UpdateMembershipResponse() - resp.team = team.to_message() - return resp diff --git a/jetway/teams/teams.py b/jetway/teams/teams.py deleted file mode 100644 index f2e5ece..0000000 --- a/jetway/teams/teams.py +++ /dev/null @@ -1,228 +0,0 @@ -from google.appengine.ext import ndb -from google.appengine.ext.ndb import msgprop -import uuid -from jetway.teams import messages - - -class Error(Exception): - pass - - -class TeamExistsError(Error): - pass - - -class TeamDoesNotExistError(Error): - pass - - -class MembershipConflictError(Error): - pass - - -class ProjectConflictError(Error): - pass - - -class CannotDeleteMembershipError(Error): - pass - - -class TeamMembership(ndb.Model): - role = msgprop.EnumProperty(messages.Role, default=messages.Role.READ_ONLY) - is_public = ndb.BooleanProperty(default=False) - domain_key = ndb.KeyProperty() - user_key = ndb.KeyProperty() - - @property - def domain(self): - return self.domain_key.get() - - @property - def user(self): - return self.user_key.get() - - def to_message(self): - message = messages.TeamMembershipMessage() - message.user = self.user.to_message() - message.is_public = self.is_public - message.role = self.role - return message - - -class Team(ndb.Model): - owner_key = ndb.KeyProperty() - nickname = ndb.StringProperty() - description = ndb.StringProperty() - created = ndb.DateTimeProperty(auto_now_add=True) - modified = ndb.DateTimeProperty(auto_now=True) - created_by_key = ndb.KeyProperty() - project_keys = ndb.KeyProperty(repeated=True) - user_keys = ndb.KeyProperty(repeated=True) - memberships = ndb.StructuredProperty(TeamMembership, repeated=True) - role = msgprop.EnumProperty(messages.Role) - kind = msgprop.EnumProperty(messages.Kind, default=messages.Kind.DEFAULT) - - def __repr__(self): - return ''.format(self.ident) - - @classmethod - def create(cls, owner, nickname, created_by, project=None, - kind=messages.Kind.DEFAULT): - letter = cls.get_letter_from_kind(kind) - ident = project.ident if project else str(uuid.uuid4())[:10].replace('-', '') - ident = '{}/{}'.format(letter, str(ident)) - key = ndb.Key('Team', ident) - team = cls( - key=key, - owner_key=owner.key, - created_by_key=created_by.key, - nickname=nickname, - kind=kind) - if project: - team.project_keys = [project.key] - if kind != messages.Kind.DEFAULT: - team.create_membership(created_by, role=messages.Role.ADMIN) - return team - - @classmethod - def get_letter_from_kind(cls, kind): - if kind == messages.Kind.PROJECT_OWNERS: - return 'p' - elif kind == messages.Kind.ORG_OWNERS: - return 'o' - else: - return 'd' - - @classmethod - def get(cls, ident, kind): - letter = cls.get_letter_from_kind(kind) - ident = '{}/{}'.format(letter, ident) - team = ndb.Key('Team', ident).get() - if team is None: - raise TeamDoesNotExistError('Team "{}" does not exist.'.format(ident)) - return team - - @classmethod - def search(cls, owner=None, projects=None, users=None, kind=None): - query = cls.query() - if kind is not None: - query = query.filter(cls.kind == kind) - if owner is not None: - query = query.filter(cls.owner_key == owner.key) - if projects is not None: - for project in projects: - query = query.filter(cls.project_keys == project.key) - if users is not None: - for user in users: - query = query.filter(cls.user_keys == user.key) - return query.fetch() - - @property - def owner(self): - from jetway.owners import owners - return owners.Owner.get_by_key(self.owner_key) - - @property - def ident(self): - return str(self.key.id()).split('/')[-1] - - def list_memberships(self): - return TeamMembership.list(parent=self) - - @property - def projects(self): - return ndb.get_multi(self.project_keys) - - @property - def letter(self): - return Team.get_letter_from_kind(self.kind) - - @property - def title(self): - if self.kind == messages.Kind.ORG_OWNERS: - return '{} owners'.format(self.owner.nickname) - if self.kind == messages.Kind.PROJECT_OWNERS and self.projects: - project = self.projects[0] - return '{}/{} project team'.format(project.owner.nickname, project.nickname) - return self.nickname - - def to_message(self): - message = messages.TeamMessage() - message.ident = self.ident - message.title = self.title - message.nickname = self.nickname - message.projects = [project.to_message() for project in self.projects] - message.description = self.description - message.modified = self.modified - message.memberships = [m.to_message() for m in self.memberships] - message.title = self.title - message.num_projects = len(self.projects) - message.owner = self.owner.to_message() - message.letter = self.letter - message.role = self.role - message.kind = self.kind - return message - - def update(self, message): - self.nickname = message.nickname - self.description = message.description - self.role = message.role - self.put() - - def delete(self): - self.key.delete() -# mems = self.list_memberships() -# keys = [mem.key for mem in mems] -# keys.append(self.key) -# ndb.delete_multi(keys) - - def add_project(self, project): - if project.key in self.project_keys: - raise ProjectConflictError('Project "{}" already exists in team.'.format(project)) - self.project_keys.append(project.key) - self.put() - - def remove_project(self, project): - try: - self.project_keys.remove(project.key) - except ValueError: - raise ProjectConflictError('Project "{}" does not exist in team.'.format(project)) - self.put() - - def get_membership(self, user): - for membership in self.memberships: - if membership.user_key == user.key: - return membership - - def create_membership(self, user, role=None, is_public=False): - if role is None: - role = messages.Role.READ_ONLY -# if role is not None and self.kind != messages.Kind.PROJECT: -# raise ValueError('Role cannot be set for non-project teams.') - for membership in self.memberships: - if membership.user_key == user.key: - raise MembershipConflictError('Membership already exists.') - membership = TeamMembership(user_key=user.key, role=role, is_public=is_public) - self.memberships.append(membership) - self.user_keys = [m.user_key for m in self.memberships] - self.put() - - def delete_membership(self, user): - # TODO: check for last remaining admin in project teams - if self.kind == messages.Kind.ORG_OWNERS and len(self.memberships) == 1: - raise MembershipConflictError('Cannot remove the last remaining user.') - for i, membership in enumerate(self.memberships): - if membership.user_key == user.key: - del self.memberships[i] - self.put() - self.user_keys = [m.user_key for m in self.memberships] - return - raise MembershipConflictError('Membership does not exist.') - - def update_membership(self, user, role, is_public=False): - for membership in self.memberships: - if membership.user_key == user.key: - membership.role = role - membership.is_public = is_public - self.put() diff --git a/jetway/users/users.py b/jetway/users/users.py index 468ce20..7ed475e 100644 --- a/jetway/users/users.py +++ b/jetway/users/users.py @@ -1,11 +1,10 @@ -import random from google.appengine.ext import ndb from jetway.avatars import avatars from jetway.files import files -from jetway.teams import teams from jetway.users import messages from webapp2_extras import security from webapp2_extras.appengine.auth import models +import random class Error(Exception): @@ -201,21 +200,21 @@ def update(self, message): self.put() def search_teams(self): - query = teams.Team.query() - query = query.filter(teams.Team.user_keys == self.key) + from jetway.groups import groups + query = groups.Group.query() + query = query.filter(groups.Group.memberships.user_key == self.key) return query.fetch() def search_orgs(self): - team_ents = self.search_teams() - org_keys = list(set([team.owner_key for team in team_ents - if team.kind != teams.messages.Kind.PROJECT_OWNERS])) - return filter(None, ndb.get_multi(org_keys)) + # TODO: Implement. + return [] def search_projects(self): team_ents = self.search_teams() project_keys = [] for team in team_ents: - project_keys += team.project_keys + if team.project_key: + project_keys.append(team.project_key) team_projects = ndb.get_multi(list(set(project_keys))) from jetway.projects import projects user_projects = projects.Project.search(owner=self) From 14d760357cd505928f03a6fe5ceed791dafed522 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Thu, 12 Nov 2015 01:10:16 -0800 Subject: [PATCH 25/46] Raise membership conflict errors. --- jetway/groups/groups.py | 3 +-- jetway/groups/memberships.py | 4 ++-- jetway/policies/projects.py | 2 +- jetway/projects/services.py | 11 +++++++++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/jetway/groups/groups.py b/jetway/groups/groups.py index 878d3f7..b1079b8 100644 --- a/jetway/groups/groups.py +++ b/jetway/groups/groups.py @@ -4,8 +4,7 @@ from google.appengine.ext.ndb import msgprop -class Error(Exception): - pass +Error = memberships.Error class Group(ndb.Model): diff --git a/jetway/groups/memberships.py b/jetway/groups/memberships.py index c717c18..5c26252 100644 --- a/jetway/groups/memberships.py +++ b/jetway/groups/memberships.py @@ -21,9 +21,9 @@ class Membership(ndb.Model): def check_conflict(self, other_memberships): for mem in other_memberships: if self.user_key and self.user_key == mem.user_key: - raise MembershipConflictError() + raise MembershipConflictError('{} is already a member.'.format(self.user.nickname)) if self.domain and self.domain == mem.domain: - raise MembershipConflictError() + raise MembershipConflictError('{} is already a member.'.format(self.domain)) @classmethod def from_message(cls, message): diff --git a/jetway/policies/projects.py b/jetway/policies/projects.py index 34cc8c9..26faac9 100644 --- a/jetway/policies/projects.py +++ b/jetway/policies/projects.py @@ -43,7 +43,7 @@ def can_read(self): return True if self.mem is None: return False - return self.mem.role in [messages.Role.ADMIN, messages.Role.READ] + return self.mem.role in [messages.Role.ADMIN, messages.Role.READ, None] def can_write(self): if self.is_owner: diff --git a/jetway/projects/services.py b/jetway/projects/services.py index 1b27267..5ddab9e 100644 --- a/jetway/projects/services.py +++ b/jetway/projects/services.py @@ -2,6 +2,7 @@ from ..buildbot import buildbot from ..policies import policies from jetway import api +from jetway.groups import groups from jetway.owners import owners from jetway.projects import messages from jetway.projects import projects @@ -230,7 +231,10 @@ def get_group(self, request): def create_membership(self, request): project = self._get_project(request) self._get_policy(project).authorize_admin() - project.group.create_membership(request.membership) + try: + project.group.create_membership(request.membership) + except groups.Error as e: + raise api.Error(str(e)) resp = service_messages.GroupResponse() resp.group = project.group.to_message() return resp @@ -240,7 +244,10 @@ def create_membership(self, request): def delete_membership(self, request): project = self._get_project(request) self._get_policy(project).authorize_admin() - project.group.delete_membership(request.membership) + try: + project.group.delete_membership(request.membership) + except groups.Error as e: + raise api.Error(str(e)) resp = service_messages.GroupResponse() resp.group = project.group.to_message() return resp From b556c5664c8e5646151717c8215d317e6092d1e3 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Thu, 12 Nov 2015 11:25:02 -0800 Subject: [PATCH 26/46] Add subdomain. --- jetway/filesets/filesets.py | 9 ++++++++- jetway/filesets/messages.py | 1 + jetway/projects/projects.py | 2 +- jetway/server/utils.py | 23 ++++++++++++----------- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/jetway/filesets/filesets.py b/jetway/filesets/filesets.py index aaa2f89..ad484be 100644 --- a/jetway/filesets/filesets.py +++ b/jetway/filesets/filesets.py @@ -67,7 +67,7 @@ class File(ndb.Model): def to_message(self): message = messages.FileMessage() - message.path = self.apth + message.path = self.path message.created_by = self.created_by message.modified_by = self.modified_by message.ext = self.ext @@ -238,6 +238,7 @@ def to_message(self): message.commit = self.commit message.finalized = self.finalized message.status = self.status + message.subdomain = self.subdomain if self.created_by_key: message.created_by = self.created_by.to_message() if self.log: @@ -253,6 +254,12 @@ def url(self): return utils.make_url(self.name, self.project.nickname, self.project.owner.nickname, ident=self.ident) + @property + def subdomain(self): + return utils.make_subdomain( + self.name, self.project.nickname, self.project.owner.nickname, + ident=self.ident) + @property def root(self): gcs_bucket = appengine_config.get_gcs_bucket() diff --git a/jetway/filesets/messages.py b/jetway/filesets/messages.py index 134c4f9..10d0523 100644 --- a/jetway/filesets/messages.py +++ b/jetway/filesets/messages.py @@ -103,6 +103,7 @@ class FilesetMessage(messages.Message): commit = messages.MessageField(CommitMessage, 14) finalized = messages.BooleanField(15) status = messages.EnumField(FilesetStatus, 16) + subdomain = messages.StringField(17) class NamedFilesetMessage(messages.Message): diff --git a/jetway/projects/projects.py b/jetway/projects/projects.py index 3b31eb0..b1a29f4 100644 --- a/jetway/projects/projects.py +++ b/jetway/projects/projects.py @@ -107,7 +107,7 @@ def get(cls, owner=None, nickname=None): return project def _update_buildbot_job(self, git_url): - if git_url is None: + if not git_url: self.buildbot_job_id = None self.put() return diff --git a/jetway/server/utils.py b/jetway/server/utils.py index e7ab262..d9b0ced 100644 --- a/jetway/server/utils.py +++ b/jetway/server/utils.py @@ -30,6 +30,15 @@ def parse_hostname(hostname, path=None, multitenant=False): return tuple(part if part else None for part in results[0]) +def make_subdomain(name, project, owner, ident=None, multitenant=False): + if multitenant: + return '{name}--{project}--{owner}'.format( + name=name, project=project, owner=owner) + elif name: + return name + return ident + + def make_url(name, project, owner, path=None, multitenant=False, include_port=appengine_config.IS_DEV_SERVER, @@ -42,14 +51,6 @@ def make_url(name, project, owner, path=None, sep = '-dot-' else: sep = '.' - if multitenant: - return '{scheme}://{name}--{project}--{owner}{sep}{hostname}'.format( - scheme=scheme, name=name, sep=sep, hostname=preview_hostname, - owner=owner, project=project) - else: - if name: - return '{scheme}://{name}{sep}{hostname}'.format( - scheme=scheme, name=name, sep=sep, hostname=preview_hostname) - else: - return '{scheme}://{ident}{sep}{hostname}'.format( - scheme=scheme, ident=ident, sep=sep, hostname=preview_hostname) + subdomain = make_subdomain(name, project, owner, ident=ident, multitenant=multitenant) + return '{scheme}://{subdomain}{sep}{hostname}'.format( + scheme=scheme, subdomain=subdomain, sep=sep, hostname=preview_hostname) From 32257c69ea85fd7d5ea9bb4782a0ded9b38e2e5a Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Mon, 16 Nov 2015 17:19:32 -0800 Subject: [PATCH 27/46] jetway -> app --- app.yaml | 8 ++++---- {jetway => app}/__init__.py | 0 {jetway => app}/api.py | 0 {jetway => app}/api_errors.py | 0 {jetway => app}/auth/__init__.py | 0 {jetway => app}/auth/handlers.py | 2 +- {jetway => app}/avatars/__init__.py | 0 {jetway => app}/avatars/avatars.py | 4 ++-- {jetway => app}/avatars/messages.py | 4 ++-- {jetway => app}/avatars/services.py | 6 +++--- {jetway => app}/buildbot/__init__.py | 0 {jetway => app}/buildbot/buildbot.py | 0 {jetway => app}/buildbot/messages.py | 0 {jetway => app}/catalogs/__init__.py | 0 .../catalogs/babel_compatibility_patch.py | 0 {jetway => app}/catalogs/catalogs.py | 0 {jetway => app}/catalogs/messages.py | 0 {jetway => app}/comments/__init__.py | 0 {jetway => app}/comments/comments.py | 4 ++-- {jetway => app}/comments/messages.py | 2 +- {jetway => app}/comments/services.py | 6 +++--- {jetway => app}/domains/__init__.py | 0 {jetway => app}/domains/domains.py | 2 +- {jetway => app}/files/__init__.py | 0 {jetway => app}/files/files.py | 4 ++-- {jetway => app}/files/messages.py | 0 {jetway => app}/filesets/__init__.py | 0 {jetway => app}/filesets/filesets.py | 10 +++++----- {jetway => app}/filesets/filesets_test.py | 4 ++-- {jetway => app}/filesets/messages.py | 8 ++++---- .../filesets/named_fileset_messages.py | 0 {jetway => app}/filesets/named_filesets.py | 0 {jetway => app}/filesets/named_filesets_test.py | 2 +- {jetway => app}/filesets/services.py | 14 +++++++------- {jetway => app}/filesets/templates/_base.html | 0 .../filesets/templates/finalized_email.html | 0 {jetway => app}/filesets/utils.py | 2 +- {jetway => app}/frontend/__init__.py | 0 {jetway => app}/frontend/handlers.py | 10 +++------- {jetway => app}/frontend/static/html/home.html | 0 {jetway => app}/frontend/static/html/new.html | 0 .../frontend/static/html/orgs.new.html | 0 {jetway => app}/frontend/static/html/owner.html | 0 .../frontend/static/html/project.builds.html | 0 {jetway => app}/frontend/static/html/project.html | 0 .../frontend/static/html/project.settings.html | 0 .../frontend/static/html/project.team.html | 0 .../static/html/project.translations.html | 0 .../frontend/static/html/project.watchers.html | 0 .../frontend/static/html/projects.html | 0 .../frontend/static/html/settings-new.html | 0 .../frontend/static/html/settings.accounts.html | 0 .../frontend/static/html/settings.html | 0 .../frontend/static/html/settings.index.html | 0 .../frontend/static/html/settings.referrals.html | 0 {jetway => app}/frontend/static/images/badge.png | Bin {jetway => app}/frontend/static/js/app.js | 0 {jetway => app}/frontend/static/js/controllers.js | 0 {jetway => app}/frontend/static/js/filters.js | 0 {jetway => app}/frontend/static/js/rpc.js | 0 {jetway => app}/frontend/static/sass/_global.scss | 0 {jetway => app}/frontend/static/sass/main.scss | 0 {jetway => app}/frontend/templates/index.html | 0 {jetway => app}/groups/__init__.py | 0 {jetway => app}/groups/groups.py | 0 {jetway => app}/groups/memberships.py | 0 {jetway => app}/groups/messages.py | 0 {jetway => app}/launches/__init__.py | 0 {jetway => app}/launches/launches.py | 8 ++++---- {jetway => app}/launches/messages.py | 8 ++++---- {jetway => app}/launches/services.py | 12 ++++++------ {jetway => app}/logs/__init__.py | 0 {jetway => app}/logs/logs.py | 4 ++-- {jetway => app}/logs/messages.py | 2 +- {jetway => app}/main.py | 0 {jetway => app}/main_test.py | 10 +++++----- {jetway => app}/models/models.py | 0 {jetway => app}/orgs/__init__.py | 0 {jetway => app}/orgs/memberships.py | 0 {jetway => app}/orgs/messages.py | 0 {jetway => app}/orgs/orgs.py | 2 +- {jetway => app}/orgs/service_messages.py | 4 ++-- {jetway => app}/orgs/services.py | 8 ++++---- {jetway => app}/owners/__init__.py | 0 {jetway => app}/owners/messages.py | 0 {jetway => app}/owners/owners.py | 10 +++++----- {jetway => app}/owners/owners_test.py | 8 ++++---- {jetway => app}/owners/services.py | 6 +++--- {jetway => app}/policies/__init__.py | 0 {jetway => app}/policies/policies.py | 0 {jetway => app}/policies/projects.py | 2 +- {jetway => app}/projects/__init__.py | 0 {jetway => app}/projects/messages.py | 2 +- {jetway => app}/projects/projects.py | 12 ++++++------ {jetway => app}/projects/projects_test.py | 2 +- {jetway => app}/projects/service_messages.py | 0 {jetway => app}/projects/services.py | 10 +++++----- {jetway => app}/projects/watcher_messages.py | 0 {jetway => app}/projects/watchers.py | 0 {jetway => app}/server/__init__.py | 0 {jetway => app}/server/handlers.py | 14 +++++++------- {jetway => app}/server/templates/error.html | 0 {jetway => app}/server/utils.py | 0 {jetway => app}/server/utils_test.py | 4 ++-- {jetway => app}/sheets/__init__.py | 0 {jetway => app}/sheets/handlers.py | 0 {jetway => app}/sheets/sheets.py | 0 {jetway => app}/testing.py | 10 +++++----- {jetway => app}/translations/__init__.py | 0 {jetway => app}/translations/messages.py | 0 {jetway => app}/translations/translations.py | 0 {jetway => app}/users/__init__.py | 0 {jetway => app}/users/messages.py | 4 ++-- {jetway => app}/users/services.py | 12 ++++++------ {jetway => app}/users/users.py | 10 +++++----- {jetway => app}/utils/__init__.py | 0 {jetway => app}/utils/gcs.py | 0 bower.json | 14 ++------------ scripts/test | 4 ++-- 119 files changed, 130 insertions(+), 144 deletions(-) rename {jetway => app}/__init__.py (100%) rename {jetway => app}/api.py (100%) rename {jetway => app}/api_errors.py (100%) rename {jetway => app}/auth/__init__.py (100%) rename {jetway => app}/auth/handlers.py (99%) rename {jetway => app}/avatars/__init__.py (100%) rename {jetway => app}/avatars/avatars.py (97%) rename {jetway => app}/avatars/messages.py (73%) rename {jetway => app}/avatars/services.py (83%) rename {jetway => app}/buildbot/__init__.py (100%) rename {jetway => app}/buildbot/buildbot.py (100%) rename {jetway => app}/buildbot/messages.py (100%) rename {jetway => app}/catalogs/__init__.py (100%) rename {jetway => app}/catalogs/babel_compatibility_patch.py (100%) rename {jetway => app}/catalogs/catalogs.py (100%) rename {jetway => app}/catalogs/messages.py (100%) rename {jetway => app}/comments/__init__.py (100%) rename {jetway => app}/comments/comments.py (96%) rename {jetway => app}/comments/messages.py (96%) rename {jetway => app}/comments/services.py (93%) rename {jetway => app}/domains/__init__.py (100%) rename {jetway => app}/domains/domains.py (78%) rename {jetway => app}/files/__init__.py (100%) rename {jetway => app}/files/files.py (98%) rename {jetway => app}/files/messages.py (100%) rename {jetway => app}/filesets/__init__.py (100%) rename {jetway => app}/filesets/filesets.py (98%) rename {jetway => app}/filesets/filesets_test.py (95%) rename {jetway => app}/filesets/messages.py (96%) rename {jetway => app}/filesets/named_fileset_messages.py (100%) rename {jetway => app}/filesets/named_filesets.py (100%) rename {jetway => app}/filesets/named_filesets_test.py (94%) rename {jetway => app}/filesets/services.py (96%) rename {jetway => app}/filesets/templates/_base.html (100%) rename {jetway => app}/filesets/templates/finalized_email.html (100%) rename {jetway => app}/filesets/utils.py (97%) rename {jetway => app}/frontend/__init__.py (100%) rename {jetway => app}/frontend/handlers.py (94%) rename {jetway => app}/frontend/static/html/home.html (100%) rename {jetway => app}/frontend/static/html/new.html (100%) rename {jetway => app}/frontend/static/html/orgs.new.html (100%) rename {jetway => app}/frontend/static/html/owner.html (100%) rename {jetway => app}/frontend/static/html/project.builds.html (100%) rename {jetway => app}/frontend/static/html/project.html (100%) rename {jetway => app}/frontend/static/html/project.settings.html (100%) rename {jetway => app}/frontend/static/html/project.team.html (100%) rename {jetway => app}/frontend/static/html/project.translations.html (100%) rename {jetway => app}/frontend/static/html/project.watchers.html (100%) rename {jetway => app}/frontend/static/html/projects.html (100%) rename {jetway => app}/frontend/static/html/settings-new.html (100%) rename {jetway => app}/frontend/static/html/settings.accounts.html (100%) rename {jetway => app}/frontend/static/html/settings.html (100%) rename {jetway => app}/frontend/static/html/settings.index.html (100%) rename {jetway => app}/frontend/static/html/settings.referrals.html (100%) rename {jetway => app}/frontend/static/images/badge.png (100%) rename {jetway => app}/frontend/static/js/app.js (100%) rename {jetway => app}/frontend/static/js/controllers.js (100%) rename {jetway => app}/frontend/static/js/filters.js (100%) rename {jetway => app}/frontend/static/js/rpc.js (100%) rename {jetway => app}/frontend/static/sass/_global.scss (100%) rename {jetway => app}/frontend/static/sass/main.scss (100%) rename {jetway => app}/frontend/templates/index.html (100%) rename {jetway => app}/groups/__init__.py (100%) rename {jetway => app}/groups/groups.py (100%) rename {jetway => app}/groups/memberships.py (100%) rename {jetway => app}/groups/messages.py (100%) rename {jetway => app}/launches/__init__.py (100%) rename {jetway => app}/launches/launches.py (96%) rename {jetway => app}/launches/messages.py (92%) rename {jetway => app}/launches/services.py (94%) rename {jetway => app}/logs/__init__.py (100%) rename {jetway => app}/logs/logs.py (97%) rename {jetway => app}/logs/messages.py (92%) rename {jetway => app}/main.py (100%) rename {jetway => app}/main_test.py (89%) rename {jetway => app}/models/models.py (100%) rename {jetway => app}/orgs/__init__.py (100%) rename {jetway => app}/orgs/memberships.py (100%) rename {jetway => app}/orgs/messages.py (100%) rename {jetway => app}/orgs/orgs.py (98%) rename {jetway => app}/orgs/service_messages.py (92%) rename {jetway => app}/orgs/services.py (94%) rename {jetway => app}/owners/__init__.py (100%) rename {jetway => app}/owners/messages.py (100%) rename {jetway => app}/owners/owners.py (93%) rename {jetway => app}/owners/owners_test.py (75%) rename {jetway => app}/owners/services.py (94%) rename {jetway => app}/policies/__init__.py (100%) rename {jetway => app}/policies/policies.py (100%) rename {jetway => app}/policies/projects.py (97%) rename {jetway => app}/projects/__init__.py (100%) rename {jetway => app}/projects/messages.py (97%) rename {jetway => app}/projects/projects.py (97%) rename {jetway => app}/projects/projects_test.py (94%) rename {jetway => app}/projects/service_messages.py (100%) rename {jetway => app}/projects/services.py (98%) rename {jetway => app}/projects/watcher_messages.py (100%) rename {jetway => app}/projects/watchers.py (100%) rename {jetway => app}/server/__init__.py (100%) rename {jetway => app}/server/handlers.py (92%) rename {jetway => app}/server/templates/error.html (100%) rename {jetway => app}/server/utils.py (100%) rename {jetway => app}/server/utils_test.py (96%) rename {jetway => app}/sheets/__init__.py (100%) rename {jetway => app}/sheets/handlers.py (100%) rename {jetway => app}/sheets/sheets.py (100%) rename {jetway => app}/testing.py (84%) rename {jetway => app}/translations/__init__.py (100%) rename {jetway => app}/translations/messages.py (100%) rename {jetway => app}/translations/translations.py (100%) rename {jetway => app}/users/__init__.py (100%) rename {jetway => app}/users/messages.py (94%) rename {jetway => app}/users/services.py (93%) rename {jetway => app}/users/users.py (97%) rename {jetway => app}/utils/__init__.py (100%) rename {jetway => app}/utils/gcs.py (100%) diff --git a/app.yaml b/app.yaml index fc605ed..1824f5d 100644 --- a/app.yaml +++ b/app.yaml @@ -32,7 +32,7 @@ handlers: secure: always - url: /_ah/spi/.* - script: jetway.main.endpoints_app + script: app.main.endpoints_app - url: /_app/[^/]*/config/(.*\.(svg|png|gif|jpg))$ static_files: config/\1 @@ -45,13 +45,13 @@ handlers: static_dir: dist/js - url: /_app/[^/]*/static/html - static_dir: jetway/frontend/static/html + static_dir: app/frontend/static/html - url: /_app/[^/]*/static/images - static_dir: jetway/frontend/static/images + static_dir: app/frontend/static/images - url: /.* - script: jetway.main.app + script: app.main.app skip_files: - ^(.*/)?#.*# diff --git a/jetway/__init__.py b/app/__init__.py similarity index 100% rename from jetway/__init__.py rename to app/__init__.py diff --git a/jetway/api.py b/app/api.py similarity index 100% rename from jetway/api.py rename to app/api.py diff --git a/jetway/api_errors.py b/app/api_errors.py similarity index 100% rename from jetway/api_errors.py rename to app/api_errors.py diff --git a/jetway/auth/__init__.py b/app/auth/__init__.py similarity index 100% rename from jetway/auth/__init__.py rename to app/auth/__init__.py diff --git a/jetway/auth/handlers.py b/app/auth/handlers.py similarity index 99% rename from jetway/auth/handlers.py rename to app/auth/handlers.py index 69dbae7..8f4dac2 100644 --- a/jetway/auth/handlers.py +++ b/app/auth/handlers.py @@ -1,6 +1,6 @@ from apiclient import discovery from google.appengine.api import memcache -from jetway.users import users +from app.users import users from oauth2client import appengine from webapp2_extras import auth as webapp2_auth from webapp2_extras import security diff --git a/jetway/avatars/__init__.py b/app/avatars/__init__.py similarity index 100% rename from jetway/avatars/__init__.py rename to app/avatars/__init__.py diff --git a/jetway/avatars/avatars.py b/app/avatars/avatars.py similarity index 97% rename from jetway/avatars/avatars.py rename to app/avatars/avatars.py index 9a1364b..63d3a88 100644 --- a/jetway/avatars/avatars.py +++ b/app/avatars/avatars.py @@ -1,7 +1,7 @@ from google.appengine.ext import blobstore from google.appengine.ext import ndb -from jetway.files import files -from jetway.files import messages as file_messages +from app.files import files +from app.files import messages as file_messages import appengine_config import datetime import time diff --git a/jetway/avatars/messages.py b/app/avatars/messages.py similarity index 73% rename from jetway/avatars/messages.py rename to app/avatars/messages.py index ec44901..0735109 100644 --- a/jetway/avatars/messages.py +++ b/app/avatars/messages.py @@ -1,6 +1,6 @@ from protorpc import messages -from jetway.owners import messages as owner_messages -from jetway.projects import messages as project_messages +from app.owners import messages as owner_messages +from app.projects import messages as project_messages class CreateUploadUrlRequest(messages.Message): diff --git a/jetway/avatars/services.py b/app/avatars/services.py similarity index 83% rename from jetway/avatars/services.py rename to app/avatars/services.py index 9616e1b..6290853 100644 --- a/jetway/avatars/services.py +++ b/app/avatars/services.py @@ -1,7 +1,7 @@ from protorpc import remote -from jetway import api -from jetway.avatars import avatars -from jetway.avatars import messages +from app import api +from app.avatars import avatars +from app.avatars import messages class AvatarService(api.Service): diff --git a/jetway/buildbot/__init__.py b/app/buildbot/__init__.py similarity index 100% rename from jetway/buildbot/__init__.py rename to app/buildbot/__init__.py diff --git a/jetway/buildbot/buildbot.py b/app/buildbot/buildbot.py similarity index 100% rename from jetway/buildbot/buildbot.py rename to app/buildbot/buildbot.py diff --git a/jetway/buildbot/messages.py b/app/buildbot/messages.py similarity index 100% rename from jetway/buildbot/messages.py rename to app/buildbot/messages.py diff --git a/jetway/catalogs/__init__.py b/app/catalogs/__init__.py similarity index 100% rename from jetway/catalogs/__init__.py rename to app/catalogs/__init__.py diff --git a/jetway/catalogs/babel_compatibility_patch.py b/app/catalogs/babel_compatibility_patch.py similarity index 100% rename from jetway/catalogs/babel_compatibility_patch.py rename to app/catalogs/babel_compatibility_patch.py diff --git a/jetway/catalogs/catalogs.py b/app/catalogs/catalogs.py similarity index 100% rename from jetway/catalogs/catalogs.py rename to app/catalogs/catalogs.py diff --git a/jetway/catalogs/messages.py b/app/catalogs/messages.py similarity index 100% rename from jetway/catalogs/messages.py rename to app/catalogs/messages.py diff --git a/jetway/comments/__init__.py b/app/comments/__init__.py similarity index 100% rename from jetway/comments/__init__.py rename to app/comments/__init__.py diff --git a/jetway/comments/comments.py b/app/comments/comments.py similarity index 96% rename from jetway/comments/comments.py rename to app/comments/comments.py index d1ba770..f6a5fe6 100644 --- a/jetway/comments/comments.py +++ b/app/comments/comments.py @@ -1,7 +1,7 @@ from google.appengine.ext import ndb from google.appengine.ext.ndb import msgprop -from jetway.comments import messages -from jetway.launches import launches +from app.comments import messages +from app.launches import launches class Error(Exception): diff --git a/jetway/comments/messages.py b/app/comments/messages.py similarity index 96% rename from jetway/comments/messages.py rename to app/comments/messages.py index ba4898d..3137718 100644 --- a/jetway/comments/messages.py +++ b/app/comments/messages.py @@ -1,5 +1,5 @@ from protorpc import messages -from jetway.users import messages as user_messages +from app.users import messages as user_messages from protorpc import message_types diff --git a/jetway/comments/services.py b/app/comments/services.py similarity index 93% rename from jetway/comments/services.py rename to app/comments/services.py index 8e67fe9..85cace5 100644 --- a/jetway/comments/services.py +++ b/app/comments/services.py @@ -1,6 +1,6 @@ -from jetway import api -from jetway.comments import messages -from jetway.comments import comments +from app import api +from app.comments import messages +from app.comments import comments from protorpc import remote diff --git a/jetway/domains/__init__.py b/app/domains/__init__.py similarity index 100% rename from jetway/domains/__init__.py rename to app/domains/__init__.py diff --git a/jetway/domains/domains.py b/app/domains/domains.py similarity index 78% rename from jetway/domains/domains.py rename to app/domains/domains.py index 080b209..c3acb34 100644 --- a/jetway/domains/domains.py +++ b/app/domains/domains.py @@ -1,4 +1,4 @@ -from jetway import models +from app import models from google.appengine.ext import ndb diff --git a/jetway/files/__init__.py b/app/files/__init__.py similarity index 100% rename from jetway/files/__init__.py rename to app/files/__init__.py diff --git a/jetway/files/files.py b/app/files/files.py similarity index 98% rename from jetway/files/files.py rename to app/files/files.py index d0f1da3..207a2e3 100644 --- a/jetway/files/files.py +++ b/app/files/files.py @@ -2,8 +2,8 @@ import cloudstorage from datetime import datetime from google.appengine.ext import blobstore -from jetway.files import messages -from jetway.utils import gcs +from app.files import messages +from app.utils import gcs import os import time diff --git a/jetway/files/messages.py b/app/files/messages.py similarity index 100% rename from jetway/files/messages.py rename to app/files/messages.py diff --git a/jetway/filesets/__init__.py b/app/filesets/__init__.py similarity index 100% rename from jetway/filesets/__init__.py rename to app/filesets/__init__.py diff --git a/jetway/filesets/filesets.py b/app/filesets/filesets.py similarity index 98% rename from jetway/filesets/filesets.py rename to app/filesets/filesets.py index ad484be..ebdc4b4 100644 --- a/jetway/filesets/filesets.py +++ b/app/filesets/filesets.py @@ -1,11 +1,11 @@ from . import utils as fileset_utils from google.appengine.ext import ndb from google.appengine.ext.ndb import msgprop -from jetway.files import files -from jetway.files import messages as file_messages -from jetway.filesets import messages -from jetway.logs import logs -from jetway.server import utils +from app.files import files +from app.files import messages as file_messages +from app.filesets import messages +from app.logs import logs +from app.server import utils import appengine_config import os import webapp2 diff --git a/jetway/filesets/filesets_test.py b/app/filesets/filesets_test.py similarity index 95% rename from jetway/filesets/filesets_test.py rename to app/filesets/filesets_test.py index c2467e3..c70cd48 100644 --- a/jetway/filesets/filesets_test.py +++ b/app/filesets/filesets_test.py @@ -1,5 +1,5 @@ -from jetway import testing -from jetway.files import messages +from app import testing +from app.files import messages import base64 import mimetypes import md5 diff --git a/jetway/filesets/messages.py b/app/filesets/messages.py similarity index 96% rename from jetway/filesets/messages.py rename to app/filesets/messages.py index 10d0523..13dbd81 100644 --- a/jetway/filesets/messages.py +++ b/app/filesets/messages.py @@ -1,7 +1,7 @@ -from jetway.files import messages as file_messages -from jetway.logs import messages as log_messages -from jetway.projects import messages as project_messages -from jetway.users import messages as user_messages +from app.files import messages as file_messages +from app.logs import messages as log_messages +from app.projects import messages as project_messages +from app.users import messages as user_messages from protorpc import message_types from protorpc import messages diff --git a/jetway/filesets/named_fileset_messages.py b/app/filesets/named_fileset_messages.py similarity index 100% rename from jetway/filesets/named_fileset_messages.py rename to app/filesets/named_fileset_messages.py diff --git a/jetway/filesets/named_filesets.py b/app/filesets/named_filesets.py similarity index 100% rename from jetway/filesets/named_filesets.py rename to app/filesets/named_filesets.py diff --git a/jetway/filesets/named_filesets_test.py b/app/filesets/named_filesets_test.py similarity index 94% rename from jetway/filesets/named_filesets_test.py rename to app/filesets/named_filesets_test.py index 66165ac..267eff2 100644 --- a/jetway/filesets/named_filesets_test.py +++ b/app/filesets/named_filesets_test.py @@ -1,4 +1,4 @@ -from jetway import testing +from app import testing from . import named_filesets import unittest diff --git a/jetway/filesets/services.py b/app/filesets/services.py similarity index 96% rename from jetway/filesets/services.py rename to app/filesets/services.py index e364f5d..cea2bf4 100644 --- a/jetway/filesets/services.py +++ b/app/filesets/services.py @@ -1,10 +1,10 @@ -from jetway import api -from jetway.filesets import filesets -from jetway.filesets import messages -from jetway.owners import owners -from jetway.projects import projects -from jetway.policies import policies -from jetway.users import users +from app import api +from app.filesets import filesets +from app.filesets import messages +from app.owners import owners +from app.projects import projects +from app.policies import policies +from app.users import users from protorpc import remote import appengine_config import endpoints diff --git a/jetway/filesets/templates/_base.html b/app/filesets/templates/_base.html similarity index 100% rename from jetway/filesets/templates/_base.html rename to app/filesets/templates/_base.html diff --git a/jetway/filesets/templates/finalized_email.html b/app/filesets/templates/finalized_email.html similarity index 100% rename from jetway/filesets/templates/finalized_email.html rename to app/filesets/templates/finalized_email.html diff --git a/jetway/filesets/utils.py b/app/filesets/utils.py similarity index 97% rename from jetway/filesets/utils.py rename to app/filesets/utils.py index aabcede..05b6fbb 100644 --- a/jetway/filesets/utils.py +++ b/app/filesets/utils.py @@ -1,5 +1,5 @@ from google.appengine.api import mail -from jetway.users import users +from app.users import users import appengine_config import cStringIO import jinja2 diff --git a/jetway/frontend/__init__.py b/app/frontend/__init__.py similarity index 100% rename from jetway/frontend/__init__.py rename to app/frontend/__init__.py diff --git a/jetway/frontend/handlers.py b/app/frontend/handlers.py similarity index 94% rename from jetway/frontend/handlers.py rename to app/frontend/handlers.py index 8a550b1..359e0ba 100644 --- a/jetway/frontend/handlers.py +++ b/app/frontend/handlers.py @@ -1,7 +1,7 @@ from google.appengine.ext import blobstore -from jetway.avatars import avatars -from jetway.users import users -from jetway.auth import handlers as auth_handlers +from app.avatars import avatars +from app.users import users +from app.auth import handlers as auth_handlers import appengine_config import jinja2 import os @@ -59,10 +59,6 @@ def post(self, letter, ident): avatar = avatars.Avatar.create(letter, ident, gs_object_name) -class MeHandler(BaseHandler): - pass - - class GitRedirectHandler(webapp2.RequestHandler): def dispatch(self): diff --git a/jetway/frontend/static/html/home.html b/app/frontend/static/html/home.html similarity index 100% rename from jetway/frontend/static/html/home.html rename to app/frontend/static/html/home.html diff --git a/jetway/frontend/static/html/new.html b/app/frontend/static/html/new.html similarity index 100% rename from jetway/frontend/static/html/new.html rename to app/frontend/static/html/new.html diff --git a/jetway/frontend/static/html/orgs.new.html b/app/frontend/static/html/orgs.new.html similarity index 100% rename from jetway/frontend/static/html/orgs.new.html rename to app/frontend/static/html/orgs.new.html diff --git a/jetway/frontend/static/html/owner.html b/app/frontend/static/html/owner.html similarity index 100% rename from jetway/frontend/static/html/owner.html rename to app/frontend/static/html/owner.html diff --git a/jetway/frontend/static/html/project.builds.html b/app/frontend/static/html/project.builds.html similarity index 100% rename from jetway/frontend/static/html/project.builds.html rename to app/frontend/static/html/project.builds.html diff --git a/jetway/frontend/static/html/project.html b/app/frontend/static/html/project.html similarity index 100% rename from jetway/frontend/static/html/project.html rename to app/frontend/static/html/project.html diff --git a/jetway/frontend/static/html/project.settings.html b/app/frontend/static/html/project.settings.html similarity index 100% rename from jetway/frontend/static/html/project.settings.html rename to app/frontend/static/html/project.settings.html diff --git a/jetway/frontend/static/html/project.team.html b/app/frontend/static/html/project.team.html similarity index 100% rename from jetway/frontend/static/html/project.team.html rename to app/frontend/static/html/project.team.html diff --git a/jetway/frontend/static/html/project.translations.html b/app/frontend/static/html/project.translations.html similarity index 100% rename from jetway/frontend/static/html/project.translations.html rename to app/frontend/static/html/project.translations.html diff --git a/jetway/frontend/static/html/project.watchers.html b/app/frontend/static/html/project.watchers.html similarity index 100% rename from jetway/frontend/static/html/project.watchers.html rename to app/frontend/static/html/project.watchers.html diff --git a/jetway/frontend/static/html/projects.html b/app/frontend/static/html/projects.html similarity index 100% rename from jetway/frontend/static/html/projects.html rename to app/frontend/static/html/projects.html diff --git a/jetway/frontend/static/html/settings-new.html b/app/frontend/static/html/settings-new.html similarity index 100% rename from jetway/frontend/static/html/settings-new.html rename to app/frontend/static/html/settings-new.html diff --git a/jetway/frontend/static/html/settings.accounts.html b/app/frontend/static/html/settings.accounts.html similarity index 100% rename from jetway/frontend/static/html/settings.accounts.html rename to app/frontend/static/html/settings.accounts.html diff --git a/jetway/frontend/static/html/settings.html b/app/frontend/static/html/settings.html similarity index 100% rename from jetway/frontend/static/html/settings.html rename to app/frontend/static/html/settings.html diff --git a/jetway/frontend/static/html/settings.index.html b/app/frontend/static/html/settings.index.html similarity index 100% rename from jetway/frontend/static/html/settings.index.html rename to app/frontend/static/html/settings.index.html diff --git a/jetway/frontend/static/html/settings.referrals.html b/app/frontend/static/html/settings.referrals.html similarity index 100% rename from jetway/frontend/static/html/settings.referrals.html rename to app/frontend/static/html/settings.referrals.html diff --git a/jetway/frontend/static/images/badge.png b/app/frontend/static/images/badge.png similarity index 100% rename from jetway/frontend/static/images/badge.png rename to app/frontend/static/images/badge.png diff --git a/jetway/frontend/static/js/app.js b/app/frontend/static/js/app.js similarity index 100% rename from jetway/frontend/static/js/app.js rename to app/frontend/static/js/app.js diff --git a/jetway/frontend/static/js/controllers.js b/app/frontend/static/js/controllers.js similarity index 100% rename from jetway/frontend/static/js/controllers.js rename to app/frontend/static/js/controllers.js diff --git a/jetway/frontend/static/js/filters.js b/app/frontend/static/js/filters.js similarity index 100% rename from jetway/frontend/static/js/filters.js rename to app/frontend/static/js/filters.js diff --git a/jetway/frontend/static/js/rpc.js b/app/frontend/static/js/rpc.js similarity index 100% rename from jetway/frontend/static/js/rpc.js rename to app/frontend/static/js/rpc.js diff --git a/jetway/frontend/static/sass/_global.scss b/app/frontend/static/sass/_global.scss similarity index 100% rename from jetway/frontend/static/sass/_global.scss rename to app/frontend/static/sass/_global.scss diff --git a/jetway/frontend/static/sass/main.scss b/app/frontend/static/sass/main.scss similarity index 100% rename from jetway/frontend/static/sass/main.scss rename to app/frontend/static/sass/main.scss diff --git a/jetway/frontend/templates/index.html b/app/frontend/templates/index.html similarity index 100% rename from jetway/frontend/templates/index.html rename to app/frontend/templates/index.html diff --git a/jetway/groups/__init__.py b/app/groups/__init__.py similarity index 100% rename from jetway/groups/__init__.py rename to app/groups/__init__.py diff --git a/jetway/groups/groups.py b/app/groups/groups.py similarity index 100% rename from jetway/groups/groups.py rename to app/groups/groups.py diff --git a/jetway/groups/memberships.py b/app/groups/memberships.py similarity index 100% rename from jetway/groups/memberships.py rename to app/groups/memberships.py diff --git a/jetway/groups/messages.py b/app/groups/messages.py similarity index 100% rename from jetway/groups/messages.py rename to app/groups/messages.py diff --git a/jetway/launches/__init__.py b/app/launches/__init__.py similarity index 100% rename from jetway/launches/__init__.py rename to app/launches/__init__.py diff --git a/jetway/launches/launches.py b/app/launches/launches.py similarity index 96% rename from jetway/launches/launches.py rename to app/launches/launches.py index 5057567..6ee8d63 100644 --- a/jetway/launches/launches.py +++ b/app/launches/launches.py @@ -1,7 +1,7 @@ from google.appengine.ext import ndb -from jetway.filesets import filesets -from jetway.projects import projects -from jetway.launches import messages +from app.filesets import filesets +from app.projects import projects +from app.launches import messages class Error(Exception): @@ -121,7 +121,7 @@ def fileset(self): @property def num_comments(self): - from jetway.comments import comments + from app.comments import comments return comments.Comment.count(parent=self, kind=comments.messages.Kind.LAUNCH) @property diff --git a/jetway/launches/messages.py b/app/launches/messages.py similarity index 92% rename from jetway/launches/messages.py rename to app/launches/messages.py index 60c92da..0d185cd 100644 --- a/jetway/launches/messages.py +++ b/app/launches/messages.py @@ -1,9 +1,9 @@ from protorpc import messages from protorpc import message_types -from jetway.projects import messages as project_messages -from jetway.filesets import messages as fileset_messages -from jetway.owners import messages as owner_messages -from jetway.users import messages as user_messages +from app.projects import messages as project_messages +from app.filesets import messages as fileset_messages +from app.owners import messages as owner_messages +from app.users import messages as user_messages class ApprovalMessage(messages.Message): diff --git a/jetway/launches/services.py b/app/launches/services.py similarity index 94% rename from jetway/launches/services.py rename to app/launches/services.py index c273ac4..626abcb 100644 --- a/jetway/launches/services.py +++ b/app/launches/services.py @@ -1,9 +1,9 @@ -from jetway import api -from jetway.launches import launches -from jetway.owners import owners -from jetway.launches import messages -from jetway.projects import projects -from jetway.users import users +from app import api +from app.launches import launches +from app.owners import owners +from app.launches import messages +from app.projects import projects +from app.users import users from protorpc import remote diff --git a/jetway/logs/__init__.py b/app/logs/__init__.py similarity index 100% rename from jetway/logs/__init__.py rename to app/logs/__init__.py diff --git a/jetway/logs/logs.py b/app/logs/logs.py similarity index 97% rename from jetway/logs/logs.py rename to app/logs/logs.py index 8e35762..0d80054 100644 --- a/jetway/logs/logs.py +++ b/app/logs/logs.py @@ -1,6 +1,6 @@ from google.appengine.ext import ndb -from jetway.logs import messages -from jetway.users import users +from app.logs import messages +from app.users import users class LogAuthor(ndb.Model): diff --git a/jetway/logs/messages.py b/app/logs/messages.py similarity index 92% rename from jetway/logs/messages.py rename to app/logs/messages.py index da2a445..103cfc2 100644 --- a/jetway/logs/messages.py +++ b/app/logs/messages.py @@ -1,6 +1,6 @@ from protorpc import messages from protorpc import message_types -from jetway.users import messages as user_messages +from app.users import messages as user_messages class AuthorMessage(messages.Message): diff --git a/jetway/main.py b/app/main.py similarity index 100% rename from jetway/main.py rename to app/main.py diff --git a/jetway/main_test.py b/app/main_test.py similarity index 89% rename from jetway/main_test.py rename to app/main_test.py index 4c60f23..af7205f 100644 --- a/jetway/main_test.py +++ b/app/main_test.py @@ -1,8 +1,8 @@ -from jetway import main -from jetway import testing -from jetway.owners import owners -from jetway.projects import projects -from jetway.users import users +from app import main +from app import testing +from app.owners import owners +from app.projects import projects +from app.users import users import os import unittest import webapp2 diff --git a/jetway/models/models.py b/app/models/models.py similarity index 100% rename from jetway/models/models.py rename to app/models/models.py diff --git a/jetway/orgs/__init__.py b/app/orgs/__init__.py similarity index 100% rename from jetway/orgs/__init__.py rename to app/orgs/__init__.py diff --git a/jetway/orgs/memberships.py b/app/orgs/memberships.py similarity index 100% rename from jetway/orgs/memberships.py rename to app/orgs/memberships.py diff --git a/jetway/orgs/messages.py b/app/orgs/messages.py similarity index 100% rename from jetway/orgs/messages.py rename to app/orgs/messages.py diff --git a/jetway/orgs/orgs.py b/app/orgs/orgs.py similarity index 98% rename from jetway/orgs/orgs.py rename to app/orgs/orgs.py index c7ff3c2..b707ab0 100644 --- a/jetway/orgs/orgs.py +++ b/app/orgs/orgs.py @@ -47,7 +47,7 @@ def create(cls, nickname, created_by): return org def delete(self): - from jetway.projects import projects + from app.projects import projects results = projects.Project.search(owner=self) if results: team.delete() diff --git a/jetway/orgs/service_messages.py b/app/orgs/service_messages.py similarity index 92% rename from jetway/orgs/service_messages.py rename to app/orgs/service_messages.py index 7c237c6..ec8d68b 100644 --- a/jetway/orgs/service_messages.py +++ b/app/orgs/service_messages.py @@ -1,6 +1,6 @@ from protorpc import messages -from jetway.orgs import messages as org_messages -from jetway.users import messages as user_messages +from app.orgs import messages as org_messages +from app.users import messages as user_messages class CreateOrgRequest(messages.Message): diff --git a/jetway/orgs/services.py b/app/orgs/services.py similarity index 94% rename from jetway/orgs/services.py rename to app/orgs/services.py index e829c3f..65970e4 100644 --- a/jetway/orgs/services.py +++ b/app/orgs/services.py @@ -1,8 +1,8 @@ from protorpc import remote -from jetway import api -from jetway.orgs import orgs -from jetway.orgs import service_messages -from jetway.users import users +from app import api +from app.orgs import orgs +from app.orgs import service_messages +from app.users import users import logging diff --git a/jetway/owners/__init__.py b/app/owners/__init__.py similarity index 100% rename from jetway/owners/__init__.py rename to app/owners/__init__.py diff --git a/jetway/owners/messages.py b/app/owners/messages.py similarity index 100% rename from jetway/owners/messages.py rename to app/owners/messages.py diff --git a/jetway/owners/owners.py b/app/owners/owners.py similarity index 93% rename from jetway/owners/owners.py rename to app/owners/owners.py index 866898d..e4aec8d 100644 --- a/jetway/owners/owners.py +++ b/app/owners/owners.py @@ -1,9 +1,9 @@ from google.appengine.ext import ndb -from jetway.avatars import avatars -from jetway.orgs import orgs -from jetway.owners import messages -from jetway.users import users -from jetway import api_errors as api +from app.avatars import avatars +from app.orgs import orgs +from app.owners import messages +from app.users import users +from app import api_errors as api class Error(Exception): diff --git a/jetway/owners/owners_test.py b/app/owners/owners_test.py similarity index 75% rename from jetway/owners/owners_test.py rename to app/owners/owners_test.py index 8cdfc54..7616ea1 100644 --- a/jetway/owners/owners_test.py +++ b/app/owners/owners_test.py @@ -1,7 +1,7 @@ -from jetway import testing -from jetway.orgs import orgs -from jetway.owners import owners -from jetway.users import users +from app import testing +from app.orgs import orgs +from app.owners import owners +from app.users import users import unittest diff --git a/jetway/owners/services.py b/app/owners/services.py similarity index 94% rename from jetway/owners/services.py rename to app/owners/services.py index 6a79be2..d6569ed 100644 --- a/jetway/owners/services.py +++ b/app/owners/services.py @@ -1,7 +1,7 @@ from protorpc import remote -from jetway import api -from jetway.owners import owners -from jetway.owners import messages +from app import api +from app.owners import owners +from app.owners import messages class OwnerService(api.Service): diff --git a/jetway/policies/__init__.py b/app/policies/__init__.py similarity index 100% rename from jetway/policies/__init__.py rename to app/policies/__init__.py diff --git a/jetway/policies/policies.py b/app/policies/policies.py similarity index 100% rename from jetway/policies/policies.py rename to app/policies/policies.py diff --git a/jetway/policies/projects.py b/app/policies/projects.py similarity index 97% rename from jetway/policies/projects.py rename to app/policies/projects.py index 26faac9..69cb31a 100644 --- a/jetway/policies/projects.py +++ b/app/policies/projects.py @@ -1,6 +1,6 @@ from ..groups import groups from ..groups import messages -from jetway import api_errors +from app import api_errors class Error(Exception): diff --git a/jetway/projects/__init__.py b/app/projects/__init__.py similarity index 100% rename from jetway/projects/__init__.py rename to app/projects/__init__.py diff --git a/jetway/projects/messages.py b/app/projects/messages.py similarity index 97% rename from jetway/projects/messages.py rename to app/projects/messages.py index 42de3f4..683d8d6 100644 --- a/jetway/projects/messages.py +++ b/app/projects/messages.py @@ -1,6 +1,6 @@ from protorpc import messages from protorpc import message_types -from jetway.owners import messages as owner_messages +from app.owners import messages as owner_messages class Permission(messages.Enum): diff --git a/jetway/projects/projects.py b/app/projects/projects.py similarity index 97% rename from jetway/projects/projects.py rename to app/projects/projects.py index b1a29f4..104cd0f 100644 --- a/jetway/projects/projects.py +++ b/app/projects/projects.py @@ -6,11 +6,11 @@ from ..policies import policies from google.appengine.ext import ndb from google.appengine.ext.ndb import msgprop -from jetway.avatars import avatars -from jetway.filesets import filesets -from jetway.filesets import named_filesets -from jetway.owners import owners -from jetway.projects import messages +from app.avatars import avatars +from app.filesets import filesets +from app.filesets import named_filesets +from app.owners import owners +from app.projects import messages from protorpc import protojson import appengine_config import json @@ -135,7 +135,7 @@ def search(cls, owner=None, order=None): return query.fetch() def delete(self): - from jetway.launches import launches + from app.launches import launches launch_results = launches.Launch.search(project=self) fileset_results = filesets.Fileset.search(project=self) diff --git a/jetway/projects/projects_test.py b/app/projects/projects_test.py similarity index 94% rename from jetway/projects/projects_test.py rename to app/projects/projects_test.py index 8ccd096..9688a30 100644 --- a/jetway/projects/projects_test.py +++ b/app/projects/projects_test.py @@ -1,4 +1,4 @@ -from jetway import testing +from app import testing import unittest diff --git a/jetway/projects/service_messages.py b/app/projects/service_messages.py similarity index 100% rename from jetway/projects/service_messages.py rename to app/projects/service_messages.py diff --git a/jetway/projects/services.py b/app/projects/services.py similarity index 98% rename from jetway/projects/services.py rename to app/projects/services.py index 5ddab9e..e62e0b1 100644 --- a/jetway/projects/services.py +++ b/app/projects/services.py @@ -1,11 +1,11 @@ from . import service_messages from ..buildbot import buildbot from ..policies import policies -from jetway import api -from jetway.groups import groups -from jetway.owners import owners -from jetway.projects import messages -from jetway.projects import projects +from app import api +from app.groups import groups +from app.owners import owners +from app.projects import messages +from app.projects import projects from protorpc import remote import appengine_config diff --git a/jetway/projects/watcher_messages.py b/app/projects/watcher_messages.py similarity index 100% rename from jetway/projects/watcher_messages.py rename to app/projects/watcher_messages.py diff --git a/jetway/projects/watchers.py b/app/projects/watchers.py similarity index 100% rename from jetway/projects/watchers.py rename to app/projects/watchers.py diff --git a/jetway/server/__init__.py b/app/server/__init__.py similarity index 100% rename from jetway/server/__init__.py rename to app/server/__init__.py diff --git a/jetway/server/handlers.py b/app/server/handlers.py similarity index 92% rename from jetway/server/handlers.py rename to app/server/handlers.py index cc33894..b880488 100644 --- a/jetway/server/handlers.py +++ b/app/server/handlers.py @@ -1,11 +1,11 @@ from google.appengine.ext import ndb -from jetway.auth import handlers as auth_handlers -from jetway.files import files -from jetway.filesets import filesets -from jetway.owners import owners -from jetway.policies import policies -from jetway.projects import projects -from jetway.server import utils +from app.auth import handlers as auth_handlers +from app.files import files +from app.filesets import filesets +from app.owners import owners +from app.policies import policies +from app.projects import projects +from app.server import utils import jinja2 import os diff --git a/jetway/server/templates/error.html b/app/server/templates/error.html similarity index 100% rename from jetway/server/templates/error.html rename to app/server/templates/error.html diff --git a/jetway/server/utils.py b/app/server/utils.py similarity index 100% rename from jetway/server/utils.py rename to app/server/utils.py diff --git a/jetway/server/utils_test.py b/app/server/utils_test.py similarity index 96% rename from jetway/server/utils_test.py rename to app/server/utils_test.py index 76ed1b8..ebe1793 100644 --- a/jetway/server/utils_test.py +++ b/app/server/utils_test.py @@ -1,5 +1,5 @@ -from jetway import testing -from jetway.server import utils +from app import testing +from app.server import utils import unittest diff --git a/jetway/sheets/__init__.py b/app/sheets/__init__.py similarity index 100% rename from jetway/sheets/__init__.py rename to app/sheets/__init__.py diff --git a/jetway/sheets/handlers.py b/app/sheets/handlers.py similarity index 100% rename from jetway/sheets/handlers.py rename to app/sheets/handlers.py diff --git a/jetway/sheets/sheets.py b/app/sheets/sheets.py similarity index 100% rename from jetway/sheets/sheets.py rename to app/sheets/sheets.py diff --git a/jetway/testing.py b/app/testing.py similarity index 84% rename from jetway/testing.py rename to app/testing.py index 0eaac26..9ee268e 100644 --- a/jetway/testing.py +++ b/app/testing.py @@ -1,9 +1,9 @@ from google.appengine.ext import testbed -from jetway.filesets import messages as fileset_messages -from jetway.orgs import orgs -from jetway.owners import owners -from jetway.projects import projects -from jetway.users import users +from app.filesets import messages as fileset_messages +from app.orgs import orgs +from app.owners import owners +from app.projects import projects +from app.users import users import appengine_config import unittest diff --git a/jetway/translations/__init__.py b/app/translations/__init__.py similarity index 100% rename from jetway/translations/__init__.py rename to app/translations/__init__.py diff --git a/jetway/translations/messages.py b/app/translations/messages.py similarity index 100% rename from jetway/translations/messages.py rename to app/translations/messages.py diff --git a/jetway/translations/translations.py b/app/translations/translations.py similarity index 100% rename from jetway/translations/translations.py rename to app/translations/translations.py diff --git a/jetway/users/__init__.py b/app/users/__init__.py similarity index 100% rename from jetway/users/__init__.py rename to app/users/__init__.py diff --git a/jetway/users/messages.py b/app/users/messages.py similarity index 94% rename from jetway/users/messages.py rename to app/users/messages.py index f70abf8..6d69502 100644 --- a/jetway/users/messages.py +++ b/app/users/messages.py @@ -1,6 +1,6 @@ from protorpc import messages -from jetway.orgs import messages as org_messages -from jetway.projects import messages as project_messages +from app.orgs import messages as org_messages +from app.projects import messages as project_messages class UserMessage(messages.Message): diff --git a/jetway/users/services.py b/app/users/services.py similarity index 93% rename from jetway/users/services.py rename to app/users/services.py index 71771af..08e884e 100644 --- a/jetway/users/services.py +++ b/app/users/services.py @@ -1,9 +1,9 @@ -from jetway import api -from jetway.users import messages -from jetway.owners import owners -from jetway.projects import projects -from jetway.users import users -from jetway.projects import watcher_messages +from app import api +from app.users import messages +from app.owners import owners +from app.projects import projects +from app.users import users +from app.projects import watcher_messages from protorpc import remote diff --git a/jetway/users/users.py b/app/users/users.py similarity index 97% rename from jetway/users/users.py rename to app/users/users.py index 7ed475e..dc03bb6 100644 --- a/jetway/users/users.py +++ b/app/users/users.py @@ -1,7 +1,7 @@ from google.appengine.ext import ndb -from jetway.avatars import avatars -from jetway.files import files -from jetway.users import messages +from app.avatars import avatars +from app.files import files +from app.users import messages from webapp2_extras import security from webapp2_extras.appengine.auth import models import random @@ -200,7 +200,7 @@ def update(self, message): self.put() def search_teams(self): - from jetway.groups import groups + from app.groups import groups query = groups.Group.query() query = query.filter(groups.Group.memberships.user_key == self.key) return query.fetch() @@ -216,7 +216,7 @@ def search_projects(self): if team.project_key: project_keys.append(team.project_key) team_projects = ndb.get_multi(list(set(project_keys))) - from jetway.projects import projects + from app.projects import projects user_projects = projects.Project.search(owner=self) results = filter(None, team_projects) for project in user_projects: diff --git a/jetway/utils/__init__.py b/app/utils/__init__.py similarity index 100% rename from jetway/utils/__init__.py rename to app/utils/__init__.py diff --git a/jetway/utils/gcs.py b/app/utils/gcs.py similarity index 100% rename from jetway/utils/gcs.py rename to app/utils/gcs.py diff --git a/bower.json b/bower.json index 8bd3819..f45c7dd 100644 --- a/bower.json +++ b/bower.json @@ -1,14 +1,4 @@ { - "name": "jetway", - "private": true, - "dependencies": { - "angular": "1.2.8", - "angular-bootstrap": "*", - "angular-ui-router": "*", - "angular-xeditable": "*", - "bootstrap": "3.2.0" - }, - "resolutions": { - "angular": ">=1.3.0" - } + "name": "webreview", + "private": true } diff --git a/scripts/test b/scripts/test index b3b8e73..281e7c9 100755 --- a/scripts/test +++ b/scripts/test @@ -12,8 +12,8 @@ if [ -d env ]; then --cover-erase \ --cover-html \ --cover-html-dir=htmlcov \ - --cover-package=jetway \ - jetway/ + --cover-package=app \ + app/ deactivate else echo 'Run ./scripts/setup first.' From 7b82aa7660c8be9e4a0718bf05b5f35e35feeaf1 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Mon, 16 Nov 2015 17:34:10 -0800 Subject: [PATCH 28/46] Delete dead frontend. --- app/frontend/static/html/home.html | 5 - app/frontend/static/html/new.html | 29 - app/frontend/static/html/orgs.new.html | 34 - app/frontend/static/html/owner.html | 5 - app/frontend/static/html/project.builds.html | 21 - app/frontend/static/html/project.html | 37 - .../static/html/project.settings.html | 24 - app/frontend/static/html/project.team.html | 39 - .../static/html/project.translations.html | 16 - .../static/html/project.watchers.html | 11 - app/frontend/static/html/projects.html | 4 - app/frontend/static/html/settings-new.html | 16 - .../static/html/settings.accounts.html | 10 - app/frontend/static/html/settings.html | 31 - app/frontend/static/html/settings.index.html | 26 - .../static/html/settings.referrals.html | 53 -- app/frontend/static/images/badge.png | Bin 3745 -> 0 bytes app/frontend/static/js/app.js | 332 -------- app/frontend/static/js/controllers.js | 728 ------------------ app/frontend/static/js/filters.js | 10 - app/frontend/static/js/rpc.js | 44 -- app/frontend/static/sass/_global.scss | 21 - app/frontend/static/sass/main.scss | 97 --- 23 files changed, 1593 deletions(-) delete mode 100644 app/frontend/static/html/home.html delete mode 100644 app/frontend/static/html/new.html delete mode 100644 app/frontend/static/html/orgs.new.html delete mode 100644 app/frontend/static/html/owner.html delete mode 100644 app/frontend/static/html/project.builds.html delete mode 100644 app/frontend/static/html/project.html delete mode 100644 app/frontend/static/html/project.settings.html delete mode 100644 app/frontend/static/html/project.team.html delete mode 100644 app/frontend/static/html/project.translations.html delete mode 100644 app/frontend/static/html/project.watchers.html delete mode 100644 app/frontend/static/html/projects.html delete mode 100644 app/frontend/static/html/settings-new.html delete mode 100644 app/frontend/static/html/settings.accounts.html delete mode 100644 app/frontend/static/html/settings.html delete mode 100644 app/frontend/static/html/settings.index.html delete mode 100644 app/frontend/static/html/settings.referrals.html delete mode 100644 app/frontend/static/images/badge.png delete mode 100644 app/frontend/static/js/app.js delete mode 100644 app/frontend/static/js/controllers.js delete mode 100644 app/frontend/static/js/filters.js delete mode 100644 app/frontend/static/js/rpc.js delete mode 100644 app/frontend/static/sass/_global.scss delete mode 100644 app/frontend/static/sass/main.scss diff --git a/app/frontend/static/html/home.html b/app/frontend/static/html/home.html deleted file mode 100644 index ccff46c..0000000 --- a/app/frontend/static/html/home.html +++ /dev/null @@ -1,5 +0,0 @@ -

Sites

- - -
{{project.name}} -
diff --git a/app/frontend/static/html/new.html b/app/frontend/static/html/new.html deleted file mode 100644 index 9c4b704..0000000 --- a/app/frontend/static/html/new.html +++ /dev/null @@ -1,29 +0,0 @@ -
-
-

Create site

-
- -
-
- - -
-
/
-
- - -
-
- -
- - -
-
- -
- -
-
-
diff --git a/app/frontend/static/html/orgs.new.html b/app/frontend/static/html/orgs.new.html deleted file mode 100644 index 3473296..0000000 --- a/app/frontend/static/html/orgs.new.html +++ /dev/null @@ -1,34 +0,0 @@ -
-

Create organization

-
-
-
-
-
- - -
-
- - -

Administrative notifications such as project changes will be sent to this address. -

-
-
- - Cancel -
-
-
-
-

Take control of your team's web site production workflow by using organizations. -

Organizations let you: -

    -
  • Control review and approval workflow for launches. -
  • Group projects together by functional area. -
  • Manage teams and control access. -
  • View activity streams for all your projects. -
-
-
-
diff --git a/app/frontend/static/html/owner.html b/app/frontend/static/html/owner.html deleted file mode 100644 index 52738e2..0000000 --- a/app/frontend/static/html/owner.html +++ /dev/null @@ -1,5 +0,0 @@ -

- - {{owner.nickname}} -

-
diff --git a/app/frontend/static/html/project.builds.html b/app/frontend/static/html/project.builds.html deleted file mode 100644 index 802cd22..0000000 --- a/app/frontend/static/html/project.builds.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - -
- {{fileset.commit.branch}} {{fileset.commit.sha|limitTo:6}} - - {{fileset.commit.message || fileset.ident}} - - – {{fileset.name}} - - - - {{fileset.modified|date:"medium"}} - -
-
-

No builds yet.

-
diff --git a/app/frontend/static/html/project.html b/app/frontend/static/html/project.html deleted file mode 100644 index f5b80ae..0000000 --- a/app/frontend/static/html/project.html +++ /dev/null @@ -1,37 +0,0 @@ -
-
- - - - -
-
- -

- - {{project.owner.nickname}} / {{project.nickname}} -

- -
{{project.description}}
- - - - diff --git a/app/frontend/static/html/project.settings.html b/app/frontend/static/html/project.settings.html deleted file mode 100644 index 82052bc..0000000 --- a/app/frontend/static/html/project.settings.html +++ /dev/null @@ -1,24 +0,0 @@ -
-

Connection

-
-
- - -
-
- - -
-
- - -
-
- -
-
-
- -

Delete

-
Transfer ownership
-
Delete project
diff --git a/app/frontend/static/html/project.team.html b/app/frontend/static/html/project.team.html deleted file mode 100644 index 6b14879..0000000 --- a/app/frontend/static/html/project.team.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - -
- - - {{membership.user.email}} - - -
- -
-
- -
- -
-
- -
-
- -
-
- -
-
diff --git a/app/frontend/static/html/project.translations.html b/app/frontend/static/html/project.translations.html deleted file mode 100644 index 23d454b..0000000 --- a/app/frontend/static/html/project.translations.html +++ /dev/null @@ -1,16 +0,0 @@ -
- - - - - - - - - - - -
Locale% translated
- -
-
diff --git a/app/frontend/static/html/project.watchers.html b/app/frontend/static/html/project.watchers.html deleted file mode 100644 index 7b081af..0000000 --- a/app/frontend/static/html/project.watchers.html +++ /dev/null @@ -1,11 +0,0 @@ -
- -
- -

- - - - -
{{watcher.user.nickname}}
-

diff --git a/app/frontend/static/html/projects.html b/app/frontend/static/html/projects.html deleted file mode 100644 index da04bd5..0000000 --- a/app/frontend/static/html/projects.html +++ /dev/null @@ -1,4 +0,0 @@ - - -
{{project.nickname}} -
diff --git a/app/frontend/static/html/settings-new.html b/app/frontend/static/html/settings-new.html deleted file mode 100644 index 90fb0df..0000000 --- a/app/frontend/static/html/settings-new.html +++ /dev/null @@ -1,16 +0,0 @@ -
-

Settings

-
-
- - -
-
- - -
-
- -
-
-
diff --git a/app/frontend/static/html/settings.accounts.html b/app/frontend/static/html/settings.accounts.html deleted file mode 100644 index 85b6c33..0000000 --- a/app/frontend/static/html/settings.accounts.html +++ /dev/null @@ -1,10 +0,0 @@ -
-

Connected accounts (experimental)

-

Coming soon. -

- -
-

Delete Grow account (experimental)

-

Obliterate your account from Grow. Projects you own must be transferred or deleted first. You can download your files on a project-by-project basis if you would like to keep your data. -

-

diff --git a/app/frontend/static/html/settings.html b/app/frontend/static/html/settings.html deleted file mode 100644 index 04939f3..0000000 --- a/app/frontend/static/html/settings.html +++ /dev/null @@ -1,31 +0,0 @@ -
-

- - {{me.nickname}} -

-
-
- -
-
-
-
-
diff --git a/app/frontend/static/html/settings.index.html b/app/frontend/static/html/settings.index.html deleted file mode 100644 index 8bf2d1c..0000000 --- a/app/frontend/static/html/settings.index.html +++ /dev/null @@ -1,26 +0,0 @@ -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
Saving...
-
-
diff --git a/app/frontend/static/html/settings.referrals.html b/app/frontend/static/html/settings.referrals.html deleted file mode 100644 index f3e8b27..0000000 --- a/app/frontend/static/html/settings.referrals.html +++ /dev/null @@ -1,53 +0,0 @@ -
-

Share a referral

-

The ability to create and share referrals is in progress, and is coming soon! -

- -
-

Email a referral

- -

Refer someone to Grow to gain karma points and project hosting credits. The more people that sign up under your referrals, the more free projects you'll get. - -

-
- -
- -
- -
-
- -
-

An invitation email will be sent from Grow as "{{me.nickname}} via Grow". -

- -
- {{error.error_message}} -
- -
- -
- -

Your referrals

- -

Here's everyone you've referred. If a referral is unredeemed, you can remind the person or cancel the referral. - - - - - - -
Created - Sent to - Status -
{{referral.created|date:'mediumDate'}} - {{referral.email}} - - {{referral.redeemed}} - -
-

diff --git a/app/frontend/static/images/badge.png b/app/frontend/static/images/badge.png deleted file mode 100644 index 5196d8d8e4083867cb16a9631828d22bef058a48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3745 zcmV;S4qowzP)+)GF z=W1|;^%0fP1A-BCl>jP;Z@K#0uCb=5>c>2A(EgKRdb+B*ivGX;{zq5$v}tN;LWUUP zZ)xMN1Kq!WKlJ?h^PiTKlzduQSs91Rpz7-C{cK2M)ZAOL=s3 z^tDl=MxC25VZu*cx^yv6{s9P(l~2HW^ypEW!oou9p+kqhcDY=$@UAXg^tNf^F{nW9 zB_t$#mzKxqp~hGq`@+J)s%jb+=eKR! zmWo;Ij-k5X2Udt$qrAMldF$4#I}M-$FwlTtztj7m;Hb|7R3HNe3_$(+_gC%Dg7)s+ zyOy-Hv?8a|IowcP@QWg_)~s2Rj?LfS@n)zJOVMDWQ9EqdFz#`O4^%&fd z%a(>vdF`xi!Cd{DD zGiS~i;A(Ba!sf<|{h2*)b=gLdMR?zVjflAu30<#rECJZMuUxtENL5u;cSD82Dec%D zpsdu?)PJ((Ps=u)TIkucr{cij;o(xdcJ11^%_0p<@?Esu*w`3_&Ef(4_1A_PTLELO zUAy)h?8^*P@^{o24DwYSI&@GR80uA6NfpE{mn#_u7hfA{402#W1bE@Xh557+^(vg# z@6DSxsJgmZHAUgg@o{jm6_U0-oC7N^nLw1B0{d+mjsf>WYX!$(OExYUUe1vnqgLK; zR+(F|+&(6NShC7vob~_~m_V$gJT*{>{2yLqN95|&s|Y*WkTT$cN$Hpxz||GA>~=3t z#$owHWDDxEL(rRQkkjGHab@36w$$lPrK2A2EN4J-_*72x{t zdT3!6V5J^CdNfOLLLK;1 zY8@wmHXoqqx#WaUQX%Poqn0W^nZ*6fDht9$^C z*eqJuN~B~kZ=s=~Zm>XZib(I?y*>KFcOu|(k-AQuI-!t|5KZHY=HqZUzS40L=qp)G zN+`vY_)#Gh6chS7N2db#>jG>Jt?We*a{c;si&kb(N{aIGSs>uhdl+Mr$?I`6NTk#0 z9IB&zosM=rWrx|Hz^e1}9BH3NWDd=KPCL-->OMCim6esh^oAX@2rv>bk+;Co18VZ= za=9!T9L-=6lK?z{m?Ldc{pn}fIIM_5u3ft}%ooKf2}F8q`)=L32~D0JGc20Ng$oy2 z)6MA^#v}$(^rRhPSx*H zxy2ymy99Ka!Q+1jX>4rl<3}IIL)e@aNip>4(LXwVTOmW@MK_-*SqnKoMhF&%z#&VQM~)ncGBPs!4lZnFtAY?%pv)XfN!Ir~AG-ipY_d}n zMGDglZeT@4MV0w8Z#8Gm9F&-t=w>Dnut3t%($LhYQ=4VyeUTwUhFlBMh%lH~%c&9p zlIW3UH&LX>HR~ZUF)>$s^=|+`u&!$M?Aac_GS!LMNSk((Bzt;IY|kC@U+=&1iaqhOS+^Ua?p#dVEH$H+$(6Lq!X-mN#?M0!$V# zR*rK|n?*8b5db};9X5pzXUv%4sI08~l@gH1bA5FF3CtHfByslaS&6OaB{Rf92h#}q zo<4nw9zT98tuietDnchuo@@pl02~xp)Ai!T3*Axb^y$;{;e{g_%m7XOT5?I=tdCM+ ztxQ=tnF8gMFk=T|j!x!0m=(a{l$niQa#N?sSny}@;>CGd%^zK7wOUa^LV}c906>7h z1S=XkckV2iHvkuy%G%mm^z7L)DSt&k0}BH&!${eA$aw)UCrp?i9TS#0>q&LUb&cui z>EHU`Unofir?0$3)%+gLJe(Z%R06t91%U5@H24Fre8fIZX?=bDCwd;D z05Cu#2*8*&Z5qPtC%u=Up`k&_u0MJ51jWY2!tqgQX{iK2kn-|!31kl*JdnVJ$3SZ zG18SQS0pnBPu)NQ6o3T+3`R$b`T6-05Lc{Nfr^WZrRNSC8ynH}>(||&Qj0{Hx>rk= zF3r`!8adQeNS+wm>$*=EltrfN2|O)n_>|?b95>&rP_YQaomg3F&Ogg zcKc%b6_7mNirz<<3%0?$k!X*YJcq*}`CkA)V6)Sw-#+$MRaNC)3u8}e8nVE%62yy>okC=kpfIP+lc$WzZ` z0uVU@b3q$0h3p$=Q~>sR+qP|=VTb=5G9+k(`>IIntNeB7(4pUpd>r03tYasD0Oy@f zl(ui*Ub=4x(H}Vabbn|IZ>AxMR%okbJR-@-$r}t%1*bFv7B(qwHg4QF5Bo^>v?v(1 z>3253Cjk)qWHSLAsl#J&`)L^yo8BKXGc*5esIZj)3nV^1zIxN9O(}S@o>eFf1X9?C zBWQC6eiQ)|eA4~dm$Q+6ym9Dl4Eh_`1pWr1Swn@b2v{IVNl8VRxf1YZt>_imGQ+ve z84<`$ksk|yLbeI8pg;UwfqhtD5hq==bcVd)56i;^3l^k19FEDeX3hG+ zP*v*^ub;sxDk_RSdGh4yva+(TFkr%XZQ$lI{4f()dJMpVtURnXC9gf=0PE+mW5;e! zO-=m7%Q^cl~b', - abstract: true - }) - .state('orgs.new', { - url: '/new', - controller: OrgNewController, - templateUrl: _prefix + '/static/html/orgs.new.html' - }) - - .state('settings', { - abstract: true, - url: '/settings', - templateUrl: _prefix + '/static/html/settings.html', - controller: SettingsController - }) - .state('settings.index', { - url: '', - templateUrl: _prefix + '/static/html/settings.index.html' - }) - .state('settings.accounts', { - url: '/accounts', - templateUrl: _prefix + '/static/html/settings.accounts.html' - }) - .state('settings.memberships', { - url: '/memberships', - templateUrl: _prefix + '/static/html/settings.memberships.html' - }) - .state('settings.org', { - url: '/org', - abstract: true - }) - .state('settings.org.index', { - url: '/:org' - }) -/* - .state('settings.referrals', { - url: '/referrals', - templateUrl: _prefix + '/static/html/settings.referrals.html' - controller: ReferralsController - }) -*/ - - .state('deployments', { - abstract: true, - url: '/deployments', - template: '' - }) - .state('deployments.new', { - url: '/new?owner', - controller: DeploymentNewController, - templateUrl: _prefix + '/static/html/deployments.new.html' - }) - .state('deployments.deployment', { - url: '/:deployment', - controller: DeploymentController, - templateUrl: _prefix + '/static/html/deployment.html' - }) - - .state('launches', { - url: '/launches', - abstract: true, - template: '' - }) - .state('launches.new', { - url: '/new?owner&project', - controller: LaunchNewController, - templateUrl: _prefix + '/static/html/launches.new.html' - }) - .state('launches.launch', { - url: '/:launch', - controller: LaunchController, - templateUrl: _prefix + '/static/html/launch.html' - }) - - .state('owner', { - abstract: true, - url: '/:owner', - controller: OwnerController, - templateUrl: _prefix + '/static/html/owner.html' - }) - .state('owner.projects', { - url: '', - controller: ProjectsController, - templateUrl: _prefix + '/static/html/projects.html' - }) - .state('owner.launches', { - url: '/launches', - controller: LaunchesController, - templateUrl: _prefix + '/static/html/launches.html' - }) - .state('owner.deployments', { - url: '/deployments', - controller: DeploymentsController, - templateUrl: _prefix + '/static/html/deployments.html' - }) - /* - .state('owner.team', { - url: '/teams/:team', - controller: TeamController, - templateUrl: _prefix + '/static/html/team.html' - }) - */ - .state('owner.teams', { - url: '/teams', - controller: TeamsController, - templateUrl: _prefix + '/static/html/teams.html' - }) - .state('owner.settings', { - url: '/settings', - controller: OwnerSettingsController, - templateUrl: _prefix + '/static/html/owner.settings.html' - }) - - .state('teams', { - url: '/teams', - abstract: true, - template: '' - }) - .state('teams.new', { - url: '/new?owner', - controller: TeamNewController, - templateUrl: _prefix + '/static/html/teams.new.html' - }) - .state('teams.team', { - url: '/:letter/:team', - controller: TeamController, - templateUrl: _prefix + '/static/html/team.html' - }) - - .state('project', { - url: '/:owner/:project', - abstract: true, - templateUrl: _prefix + '/static/html/project.html', - controller: ProjectController - }) - .state('project.index', { - url: '', - controller: ProjectIndexController, - templateUrl: _prefix + '/static/html/project.builds.html' - }) - .state('project.translations', { - abstract: true, - url: '/translations', - templateUrl: _prefix + '/static/html/project.translations.html' - }) - .state('project.translations.index', { - url: '' - }) - .state('project.translations.locale', { - url: '/:locale' - }) - .state('project.team', { - url: '/team', - templateUrl: _prefix + '/static/html/project.team.html' - }) - .state('project.settings', { - url: '/settings', - templateUrl: _prefix + '/static/html/project.settings.html' - }) - - .state('file', { - url: '/:owner/:project/:branch/file/{file:.*}', - controller: FileController, - templateUrl: _prefix + '/static/html/file.html' - }) -}) - -.run(function($rootScope, $state, $stateParams, grow, editableOptions) { - editableOptions.theme = 'bs3'; - $rootScope.$state = $state; - $rootScope.$stateParams = $stateParams; - $rootScope.grow = grow; -}) - -.factory('grow', function($http){ - var grow = {}; - grow.urlencode = function(obj) { - var parts = []; - for (var p in obj) { - parts.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p])); - } - return parts.join('&'); - }; - grow.Permission = { - READ: 'READ', - WRITE: 'WRITE', - ADMINISTER: 'ADMINISTER' - }; - grow.Status = { - WAITING: 'waiting', - LOADING: 'loading', - ERROR: 'error', - SUCCESS: 'success' - }; - grow.rpc = function(path, body) { - var rpcMessage = null; - var rpcStatus = grow.Status.WAITING; - return { - execute: function(callback, opt_$scope) { - rpcStatus = grow.Status.LOADING; - var http = new XMLHttpRequest(); - http.open('POST', '/_api/' + path, true); - http.setRequestHeader('Content-Type', 'application/json'); - http.send(JSON.stringify(body)); - http.onreadystatechange = function() { - if (http.readyState == 4) { - var resp = JSON.parse(http.responseText); - if (resp['error_message']) { - rpcStatus = grow.Status.ERROR; - rpcMessage = resp['error_message']; - } else { - rpcStatus = grow.Status.SUCCESS; - callback(resp); - } - } - }; - return { - message: function() { - return rpcMessage; - }, - status: function() { - return rpcStatus; - } - }; - } - }; - }; - grow.uploadAvatar = function(owner, project, $event) { - var imageEl = $event.target; - var fileEl = document.createElement('input'); - fileEl.type = 'file'; - fileEl.onchange = function(e) { - var file = fileEl.files[0]; - if (!file) { - return; - } - var fileReader = new FileReader(); - fileReader.onload = function(e) { - // var md5 = CryptoJS.algo.MD5.create(); - // md5.update(fileReader.result); - // md5.update(CryptoJS.lib.WordArray.create(fileReader.result)); - // var md5Hash = md5.finalize().toString(); - grow.rpc('avatars.create_upload_url', { - 'project': project, - 'owner': owner - // 'headers': { - // 'content_type': file.type, - // 'content_length': file.size.toString() - // 'content_md5': md5Hash - // } - }).execute(function(resp) { - //var signedRequest = resp['signed_request']; - //var xhr = new XMLHttpRequest(); - //xhr.open('POST', resp['upload_url'], true); - //var url = signedRequest['url'] + '?' + grow.urlencode(signedRequest['params']); - //xhr.open(signedRequest['verb'], url, true); - //xhr.setRequestHeader('Content-Type', signedRequest['headers']['content_type']); - //xhr.setRequestHeader('Content-MD5', signedRequest['headers']['content_md5']); - //xhr.setRequestHeader('Content-Length', signedRequest['headers']['content_length']); - // xhr.send(formData); - var parentEl = $event.target.parentNode; - var formData = new FormData(); - formData.append('file', file); - parentEl.className += ' spinner-loading'; - $http.post(resp['upload_url'], formData, { - headers: {'Content-Type': undefined}, - transformRequest: function(data) { return data; } - }).success(function(resp) { - var base = imageEl.src.split('?')[0]; - imageEl.src = base + '?' + new Date().getTime(); - parentEl.className = parentEl.className.replace(' spinner-loading', ''); - var ident = parentEl.getAttribute('data-avatar-ident'); - var avatarEls = document.querySelectorAll('[data-avatar-ident="' + ident + '"] img'); - [].forEach.call( - avatarEls, - function(el) { - if (imageEl != el) { - el.src = imageEl.src; - } - }); - }); - }); - }; - fileReader.readAsText(file); - }; - fileEl.click(); - }; - grow.signOut = function(url) { - window.location = url; - }; - window['grow'] = grow; - return grow; -}) - -.directive('editableIf', function() { - return { - link: function($scope, el, attrs) { - var editableText = attrs.editableText; - angular.element(el).attr('editable-text', null); - console.log(el); - } - }; -}) - -.controller('HeaderController', HeaderController) diff --git a/app/frontend/static/js/controllers.js b/app/frontend/static/js/controllers.js deleted file mode 100644 index 8544524..0000000 --- a/app/frontend/static/js/controllers.js +++ /dev/null @@ -1,728 +0,0 @@ -var HeaderController = function($scope, $rootScope, grow) { - grow.rpc('me.get').execute(function(resp) { - $rootScope.me = resp['me']; - $scope.$apply(); - }); - grow.rpc('me.search_orgs').execute(function(resp) { - $rootScope.orgs = resp['orgs']; - $rootScope.$apply(); - }); -}; - - -var HomeController = function($scope, $rootScope) { - $rootScope.$watch('me', function() { - if ($rootScope.me) { - grow.rpc('me.search_projects').execute(function(resp) { - $scope.projects = resp['projects']; - $scope.$apply(); - }); - } - }); -}; - - -var ExploreController = function($scope) { - $scope.rpc = grow.rpc('projects.search').execute(function(resp) { - $scope.projects = resp['projects']; - $scope.$apply(); - }); -}; - - -var ProjectsController = function($scope, $stateParams, $rootScope, $http, grow) { - var owner = {'nickname': $stateParams['owner']}; - - $scope.rpcs.owner = grow.rpc('owners.get', {'owner': owner}).execute(function(resp) { - $scope.owner = resp['owner']; - $scope.$apply(); - }); - - $scope.rpc = grow.rpc('projects.search', { - 'project': {'owner': owner} - }).execute(function(resp) { - $scope.projects = resp['projects']; - $scope.$apply(); - }); -}; - - -var OwnerController = function($scope, $stateParams, $rootScope, $http, grow) { - $scope.rpcs = {}; - $scope.ownerName = $stateParams['owner']; - var owner = {'nickname': $scope.ownerName}; - - $scope.rpcs.owner = grow.rpc('owners.get', {'owner': owner}).execute(function(resp) { - $scope.owner = resp['owner']; - $scope.$apply(); - if (resp['owner']['kind'] == 'ORG') { - loadOrg(resp['owner']); - } else { - loadUser(resp['owner']); - } - }); - - $scope.updateOwner = function(owner) { - grow.rpc('owners.update', {'owner': owner}).execute(function(resp) { - $scope.owner = resp['owner']; - }, $scope); - }; - - var loadOrg = function(org) { - grow.rpc('orgs.search_members', {'org': org}).execute(function(resp) { - $scope.users = resp['users']; - $scope.$apply(); - }); - }; - - var loadUser = function(user) { - grow.rpc('users.search_orgs', {'user': user}).execute(function(resp) { - $scope.orgs = resp['orgs']; - $scope.$apply(); - }); - }; -}; - - -var ProjectController = function($scope, $stateParams, $state, $rootScope, grow) { - $scope.rpcs = {}; - var project = { - 'owner': {'nickname': $stateParams['owner']}, - 'nickname': $stateParams['project'] - }; - - $scope.unwatch = function(project) { - grow.rpc('projects.unwatch', { - 'project': project - }).execute(function(resp) { - $scope.watchers = resp['watchers']; - $scope.watching = false; - $scope.$apply(); - }); - }; - - $scope.watch = function(project) { - grow.rpc('projects.watch', { - 'project': project - }).execute(function(resp) { - if (!$scope.watchers) { - $scope.watchers = []; - } - $scope.watchers.push(resp['watcher']); - $scope.watching = true; - $scope.$apply(); - }); - }; - - $scope.rpcs.watchers = grow.rpc('projects.list_watchers', { - 'project': project - }).execute(function(resp) { - $scope.watchers = resp['watchers']; - $scope.watching = resp['watching']; - $scope.$apply(); - }); - - $scope.rpcs.project = grow.rpc('projects.get', { - 'project': project - }).execute(function(resp) { - $scope.project = resp['project']; - $scope.$apply(); - }); - - $scope.updateProject = function(project) { - grow.rpc('projects.update', {'project': project}).execute(function(resp) { - $scope.project = resp['project']; - $scope.$apply(); - }); - }; - - $scope.setVisibility = function(visibility) { - var p = project; - p['visibility'] = visibility; - $scope.updateProject(project); - }; - - grow.rpc('filesets.search', { - 'fileset': { - 'project': project - } - }).execute(function(resp) { - $scope.filesets = resp['filesets']; - $scope.$apply(); - }); - - // Project team. - - $scope.membership = { - 'role': 'READ_ONLY' - }; - var team = null; - - $scope.$watch('project', function() { - var project = $scope.project; - if (project && project['ident']) { - team = {'kind': 'PROJECT_OWNERS', 'ident': project['ident']}; - grow.rpc('teams.get', { - 'team': team - }).execute(function(resp) { - $scope.team = resp['team']; - $scope.$apply(); - }); - } - }); - - $scope.updateMembership = function(membership) { - grow.rpc('teams.update_membership', { - 'team': team, - 'membership': membership - }).execute(function(resp) { - $scope.$apply(); - }); - }; - - $scope.createMembership = function(membership) { - grow.rpc('teams.create_membership', { - 'team': team, - 'membership': membership - }).execute(function(resp) { - $scope.team = resp['team']; - $scope.$apply(); - }); - }; - - $scope.deleteMembership = function(membership) { - grow.rpc('teams.delete_membership', { - 'team': team, - 'membership': membership - }).execute(function(resp) { - $scope.team = resp['team']; - $scope.$apply(); - }); - }; - - // Delete. - $scope.deleteProject = function(project) { - grow.rpc('projects.delete', {'project': project}).execute(function(resp) { - $state.go('owner.projects', {'owner': project['owner']['nickname']}); - $scope.$apply(); - }); - }; -}; - - -var WorkspacesController = function($scope, $stateParams, $state, grow) { - var project = { - 'owner': {'nickname': $stateParams['owner']}, - 'nickname': $stateParams['project'] - }; - $scope.project = project; - $scope.rpc = grow.rpc('filesets.search', { - 'fileset': { - 'project': project - } - }).execute(function(resp) { - $scope.filesets = resp['filesets']; - $scope.$apply(); - }); -}; - - -var ProjectIndexController = function($scope, $stateParams, $state, grow) { - var project = { - 'owner': {'nickname': $stateParams['owner']}, - 'nickname': $stateParams['project'] - }; - grow.rpc('users.search', { - 'project': project - }).execute(function(resp) { - $scope.users = resp['users']; - $scope.$apply(); - }); -}; - - -var NewController = function($scope, $state, $rootScope, grow) { - $scope.owners = []; - $rootScope.$watch('me', function() { - if ($rootScope.me) { - grow.rpc('users.search_orgs', { - 'user': $rootScope.me - }).execute(function(resp) { - if (resp['orgs']) { - resp['orgs'].forEach(function(org) { - $scope.owners.push(org['nickname']); - }); - } - $scope.owners.unshift($rootScope.me.nickname); - $scope.project = {'owner': {'nickname': $rootScope.me.nickname}}; - $scope.$apply(); - }); - } - }); - - $scope.createProject = function(project) { - grow.rpc('projects.create', {'project': project}).execute( - function(resp) { - $state.go('project.index', { - 'owner': project.owner.nickname, - 'project': resp['project']['nickname'] - }); - $scope.$apply(); - }); - }; -}; - - -var OrgNewController = function($scope, $state, grow) { - $scope.createOrg = function(org) { - grow.rpc('orgs.create', {'org': org}).execute(function(resp) { - $state.go('owner.projects', {'owner': org['nickname']}); - $scope.$apply(); - }); - }; -}; - - -var SettingsOrgsController = function($scope, grow) { - -}; - - -var SettingsController = function($scope, grow) { - $scope.regenerateGitPassword = function() { - grow.rpc('me.regenerate_git_password').execute(function(resp) { - $scope.git_password = resp['git_password']; - $scope.$apply(); - }); - }; - $scope.updateMe = function(me) { - grow.rpc('me.update', {'me': me}).execute(function(resp) { - $scope.me = resp['me']; - $scope.$apply(); - }); - }; -}; - - -var TeamsController = function($scope, $stateParams, $state, grow) { - var team = { - 'owner': {'nickname': $stateParams['owner']} - }; - if ($stateParams['project']) { - team['projects'] = [{'nickname': $stateParams['project']}]; - } - $scope.rpc = grow.rpc('teams.search', { - 'team': team - }).execute(function(resp) { - $scope.teams = resp['teams']; - $scope.$apply(); - }); -}; - - -var TeamController = function($scope, $stateParams, $state, grow) { - $scope.rpcs = {}; - - var kind = 'DEFAULT'; - switch ($stateParams['letter']) { - case 'o': - kind = 'ORG_OWNERS'; - break; - case 'p': - kind = 'PROJECT_OWNERS'; - break; - } - var team = {'ident': $stateParams['team'], 'kind': kind}; - - $scope.rpcs.teams = grow.rpc('teams.get', { - 'team': team - }).execute(function(resp) { - $scope.team = resp['team']; - $scope.$apply(); - }); - - $scope.createMembership = function(membership) { - grow.rpc('teams.create_membership', { - 'team': team, - 'membership': membership - }).execute(function(resp) { - $scope.team = resp['team']; - $scope.$apply(); - }); - }; - - $scope.deleteMembership = function(membership) { - grow.rpc('teams.delete_membership', { - 'team': team, - 'membership': membership - }).execute(function(resp) { - $scope.team = resp['team']; - $scope.$apply(); - }); - }; - - $scope.addProject = function(project) { - grow.rpc('teams.add_project', { - 'team': team, - 'project': project - }).execute(function(resp) { - $scope.team = resp['team']; - $scope.$apply(); - }); - }; - - $scope.removeProject = function(project) { - project['owner'] = owner; - grow.rpc('teams.remove_project', { - 'team': team, - 'project': project - }).execute(function(resp) { - $scope.team = resp['team']; - $scope.$apply(); - }); - }; - - $scope.updateTeam = function(team) { - grow.rpc('teams.update', { - 'team': team - }).execute(function(resp) { - $scope.team = resp['team']; - }); - }; - - $scope.deleteTeam = function(team) { - grow.rpc('teams.delete', { - 'team': team - }).execute(function(resp) { - $state.go('owner.teams', {'owner': team['owner']['nickname']}); - $scope.$apply(); - }); - }; -}; - - -var FileController = function($scope, $stateParams, grow) { - grow.rpc('filesets.get_pagespeed_result', { - 'fileset': { - 'name': $stateParams['branch'], - 'project': { - 'owner': {'nickname': $stateParams['owner']}, - 'nickname': $stateParams['project'] - } - }, - 'file': {'path': '/' + $stateParams['file']} - }).execute(function(resp) { - $scope.pagespeed_result = resp['pagespeed_result']; - $scope.$apply(); - }); -}; - - -var TeamNewController = function($scope, $stateParams, $state, grow) { - $scope.team = { - 'owner': {'nickname': $stateParams['owner']} - }; - $scope.createTeam = function(team) { - grow.rpc('teams.create', {'team': team}).execute(function(resp) { - $state.go('teams.team', { - 'letter': resp['team']['letter'], - 'team': resp['team']['ident'] - }); - $scope.$apply(); - }); - }; -}; - - -var LaunchesController = function($scope, $stateParams, grow) { - var owner = {'nickname': $stateParams['owner']}; - $scope.rpc = grow.rpc('launches.search', { - 'launch': { - 'project': { - 'owner': owner, - 'nickname': $stateParams['project'] - } - } - }).execute(function(resp) { - $scope.launches = resp['launches']; - $scope.$apply(); - }); -}; - - -var LaunchController = function($scope, $stateParams, grow) { - $scope.comments = []; - - grow.rpc('comments.search', { - 'comment': { - 'parent': {'ident': $stateParams['launch']}, - 'kind': 'LAUNCH' - } - }).execute(function(resp) { - $scope.comments = resp['comments']; - $scope.$apply(); - }); - - grow.rpc('launches.get', { - 'launch': { - 'ident': $stateParams['launch'] - } - }).execute(function(resp) { - $scope.launch = resp['launch']; - - grow.rpc('filesets.search', { - 'fileset': { - 'project': resp['launch']['project'] - } - }).execute(function(resp) { - if ($scope.launch['fileset']) { - resp['filesets'].forEach(function(fileset) { - if ($scope.launch['fileset']['ident'] == fileset['ident']) { - $scope.launch['fileset'] = fileset; - } - }); - } - $scope.filesets = resp['filesets']; - $scope.$apply(); - }); - - grow.rpc('deployments.search', { - 'deployment': { - 'owner': resp['launch']['project']['owner'] - } - }).execute(function(resp) { - if ($scope.launch['deployment']) { - resp['deployments'].forEach(function(deployment) { - if ($scope.launch['deployment']['ident'] == deployment['ident']) { - $scope.launch['deployment'] = deployment; - } - }); - } - $scope.deployments = resp['deployments']; - $scope.$apply(); - }); - - $scope.$apply(); - }); - - $scope.deleteApproval = function(launch) { - grow.rpc('launches.delete_approval', { - 'launch': launch - }).execute(function(resp) { - $scope.launch = resp['launch']; - $scope.$apply(); - }); - }; - - $scope.createApproval = function(launch) { - grow.rpc('launches.create_approval', { - 'launch': launch - }).execute(function(resp) { - $scope.launch = resp['launch']; - $scope.$apply(); - }); - }; - - $scope.updateLaunch = function(launch) { - grow.rpc('launches.update', { - 'launch': launch - }).execute(function(resp) { - $scope.$apply(); - }); - }; - - $scope.deleteComment = function(comment) { - grow.rpc('comments.delete', {'comment': comment}).execute( - function(resp) { - $scope.comments = $scope.comments.filter(function(c) { - return c['ident'] != comment['ident']; - }); - $scope.$apply(); - }); - }; - - $scope.createComment = function(comment) { - comment['parent'] = {'ident': $stateParams['launch']}; - comment['kind'] = 'LAUNCH'; - grow.rpc('comments.create', {'comment': comment}).execute( - function(resp) { - $scope.comments.push(resp['comment']); - $scope.$apply(); - }); - }; -}; - - -var LaunchNewController = function($scope, $state, $stateParams, grow) { - $scope.launch = { - 'project': { - 'owner': {'nickname': $stateParams['owner']}, - 'nickname': $stateParams['project'] - } - }; - $scope.createLaunch = function(launch) { - grow.rpc('launches.create', { - 'launch': launch - }).execute(function(resp) { - $state.go('launches.launch', {'launch': resp['launch']['ident']}); - $scope.$apply(); - }); - }; -}; - - -var DeploymentsController = function($scope, $state, $stateParams, grow) { - var owner = {'nickname': $stateParams['owner']}; - $scope.rpc = grow.rpc('deployments.search', { - 'deployment': { - 'owner': owner - } - }).execute(function(resp) { - $scope.deployments = resp['deployments']; - $scope.$apply(); - }); -}; - - -var DeploymentController = function($scope, $state, $stateParams, grow) { - $scope.rpc = grow.rpc('deployments.get', { - 'deployment': {'ident': $stateParams['deployment']} - }).execute( - function(resp) { - $scope.deployment = resp['deployment']; - $scope.$apply(); - }); - - $scope.updateDeployment = function(deployment) { - console.log(deployment); - grow.rpc('deployments.update', { - 'deployment': deployment - }).execute(function(resp) { - $scope.deployment = resp['deployment']; - $scope.$apply(); - }); - }; -}; - - -var DeploymentNewController = function($scope, $state, $stateParams, grow) { - $scope.deployment = {'owner': {'nickname': $stateParams['owner']}}; - $scope.destinations = [ - {'nickname': 'GOOGLE_STORAGE', 'title': 'Google Cloud Storage', 'image_url': 'http://preview.growsdk.org/static/images/banner/banner_gcs.svg'}, - {'nickname': 'GITHUB_PAGES', 'title': 'GitHub Pages', 'image_url': 'http://preview.growsdk.org/static/images/banner/banner_github.svg'} - ]; - $scope.selectDestination = function(destination) { - console.log(destination); - var isSelected = $scope.deployment.destination == destination; - if (isSelected) { - $scope.deployment.destination = null; - } else { - $scope.deployment.destination = destination; - } - }; - - $scope.createDeployment = function(deployment) { - grow.rpc('deployments.create', {'deployment': deployment}).execute( - function(resp) { - $state.go('deployments.deployment', { - 'deployment': resp['deployment']['ident'] - }); - $scope.$apply(); - }); - }; -}; - - -var OwnerSettingsController = function($scope, $state, $rootScope, $stateParams, grow) { - var org = {'nickname': $stateParams['owner']}; - $scope.deleteOrg = function() { - grow.rpc('orgs.delete', {'org': org}).execute(function(resp) { - $state.go('owner.projects', {'owner': $rootScope.me['nickname']}); - $scope.$apply(); - }); - }; -}; - - -var CollaboratorsController = function($scope, $state, $stateParams, grow) { - $scope.membership = { - 'role': 'READ_ONLY' - }; - var team = null; - - $scope.$watch('project', function() { - var project = $scope.project; - if (project && project['ident']) { - team = {'kind': 'PROJECT_OWNERS', 'ident': project['ident']}; - grow.rpc('teams.get', { - 'team': team - }).execute(function(resp) { - $scope.team = resp['team']; - $scope.$apply(); - }); - } - }); - - $scope.updateMembership = function(membership) { - grow.rpc('teams.update_membership', { - 'team': team, - 'membership': membership - }).execute(function(resp) { - $scope.$apply(); - }); - }; - - $scope.createMembership = function(membership) { - grow.rpc('teams.create_membership', { - 'team': team, - 'membership': membership - }).execute(function(resp) { - $scope.team = resp['team']; - $scope.$apply(); - }); - }; - - $scope.deleteMembership = function(membership) { - grow.rpc('teams.delete_membership', { - 'team': team, - 'membership': membership - }).execute(function(resp) { - $scope.team = resp['team']; - $scope.$apply(); - }); - }; - - var query = { - 'kind': 'DEFAULT', - 'owner': {'nickname': $stateParams['owner']}, - 'projects': [{'nickname': $stateParams['project']}] - }; - $scope.rpc = grow.rpc('teams.search', { - 'team': query - }).execute(function(resp) { - $scope.teams = resp['teams']; - $scope.$apply(); - }); -}; - - -var ProjectSettingsController = function($scope, $state, $rootScope, $stateParams, grow) { - $scope.createNamedFileset = function(project, namedFileset) { - grow.rpc('projects.list_named_filesets', {'project': project}).execute(function(resp) { - $scope.namedFilesets = resp['named_filesets']; - $scope.$apply(); - }); - }; - $scope.listNamedFilesets = function(project) { - grow.rpc('projects.list_named_filesets', {'project': project}).execute(function(resp) { - $scope.namedFilesets = resp['named_filesets']; - $scope.$apply(); - }); - }; - $scope.deleteProject = function(project) { - grow.rpc('projects.delete', {'project': project}).execute(function(resp) { - $state.go('owner.projects', {'owner': project['owner']['nickname']}); - $scope.$apply(); - }); - }; -}; diff --git a/app/frontend/static/js/filters.js b/app/frontend/static/js/filters.js deleted file mode 100644 index 147bfc9..0000000 --- a/app/frontend/static/js/filters.js +++ /dev/null @@ -1,10 +0,0 @@ -angular.module('jetwayFilters', []).filter('prettyRole', function() { - return function(role) { - var rolesToTitles = { - 'ADMIN': 'Administrator', - 'READ_ONLY': 'Read', - 'WRITE_FULL': 'Write' - }; - return rolesToTitles[role] || 'Unknown'; - }; -}); diff --git a/app/frontend/static/js/rpc.js b/app/frontend/static/js/rpc.js deleted file mode 100644 index f922aa2..0000000 --- a/app/frontend/static/js/rpc.js +++ /dev/null @@ -1,44 +0,0 @@ -var Status = function() { - this.code_ = Status.Code.NONE; - this.message_ = null ; -}; - - -Status.Code = { - NONE: 0, - LOADING: 1, - SUCCESS: 2, - ERROR: -1 -}; - - -Status.prototype.getMessage = function() { - if (!this.code_) { - return null; - } else if (this.message_) { - return this.message_; - } else { - switch (this.code_) { - case Status.Code.LOADING: - return 'Loading...'; - break; - case Status.Code.SUCCESS: - return 'Done!'; - break; - case Status.Code.ERROR: - return 'Error'; - break; - }; - } -}; - - -Status.prototype.setCode = function(code, opt_message) { - this.code_ = code; - this.message_ = (opt_message != undefined) ? opt_message : null; -}; - - -Status.prototype.getCode = function() { - return this.code_; -}; diff --git a/app/frontend/static/sass/_global.scss b/app/frontend/static/sass/_global.scss deleted file mode 100644 index ec00392..0000000 --- a/app/frontend/static/sass/_global.scss +++ /dev/null @@ -1,21 +0,0 @@ -.right { - float: right; -} - -nav.navbar { - border-radius: 0; - border-right: none; - border-left: none; - - .navbar-right.navbar-nav > li > a { - padding: 7px 0; - } - - .navbar-brand { - img { - margin: -12px -5px -10px 0; - height: 32px; - width: 32px; - } - } -} diff --git a/app/frontend/static/sass/main.scss b/app/frontend/static/sass/main.scss deleted file mode 100644 index 4fd53b8..0000000 --- a/app/frontend/static/sass/main.scss +++ /dev/null @@ -1,97 +0,0 @@ -@import '_global.scss'; - -ul.navbar-form { - padding-right: 0; -} - -body, -h1, -h2, -h3, -p { - font-family: 'RobotoDraft', 'Roboto', arial, sans-serif; - font-weight: 300; -} - -h1 { - font-size: 26px; - margin-bottom: 40px; -} - -h1, -h2, -h3, -h4 { - font-weight: 300; -} - -table.table-margin { - margin-top: 20px; -} - -a { - color: #097be8; -} - -img.avatar { - width: 60px; - height: 60px; - border-radius: 6px; - margin: 0 20px 0 0; -} - -img.avatar-sm { - height: 30px; - width: 30px; - margin-right: 10px; -} - -img.avatar-xs { - margin-right: 0; - height: 20px; - width: 20px; -} - -.avatar.avatar-xs { - margin: 0 5px 0 0; - height: 15px; - width: 15px; -} - -.meta { - color: #666; - font-size: 12px; -} - -.filesets b { - font-size: 16px; - margin-bottom: 5px; -} - -.filesets { -} - -.project-name { - margin-bottom: 20px; - clear: both; - - &:after { - clear: both; - display: table; - content: ''; - } - - .left { - float: left; - } - - .sep { - padding: 0 15px; - margin-top: 25px; - font-size: 26px; - } -} - -.project-details { - clear: both; -} From 20b0a3f278e8af4f836ec32363292323c908797b Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Tue, 17 Nov 2015 12:52:41 -0800 Subject: [PATCH 29/46] Add translation branch support. --- app/projects/messages.py | 10 +--------- app/projects/projects.py | 10 +++------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/app/projects/messages.py b/app/projects/messages.py index 683d8d6..8563fc1 100644 --- a/app/projects/messages.py +++ b/app/projects/messages.py @@ -9,14 +9,6 @@ class Permission(messages.Enum): ADMINISTER = 3 -class Visibility(messages.Enum): - PUBLIC = 1 - ORGANIZATION = 2 - PRIVATE = 3 - COVER = 4 - DOMAIN = 5 - - class Order(messages.Enum): NAME = 0 @@ -27,11 +19,11 @@ class ProjectMessage(messages.Message): owner = messages.MessageField(owner_messages.OwnerMessage, 3) description = messages.StringField(4) avatar_url = messages.StringField(6) - visibility = messages.EnumField(Visibility, 7) name = messages.StringField(9) built = message_types.DateTimeField(10) buildbot_job_id = messages.StringField(11) git_url = messages.StringField(12) + translation_branch = messages.StringField(13) ### diff --git a/app/projects/projects.py b/app/projects/projects.py index 104cd0f..5d69a07 100644 --- a/app/projects/projects.py +++ b/app/projects/projects.py @@ -39,11 +39,11 @@ class Project(ndb.Model): owner_key = ndb.KeyProperty() created_by_key = ndb.KeyProperty() description = ndb.StringProperty() - visibility = msgprop.EnumProperty(messages.Visibility) built = ndb.DateTimeProperty() buildbot_job_id = ndb.StringProperty() git_url = ndb.StringProperty() group_key = ndb.KeyProperty() + translation_branch = ndb.StringProperty() @property def name(self): @@ -60,10 +60,6 @@ def name_padded(self): def __repr__(self): return self.name - @property - def computed_visibility(self): - return self.visibility or messages.Visibility.DOMAIN - @property def ident(self): return str(self.key.id()) @@ -200,15 +196,15 @@ def to_message(self): message.owner = self.owner.to_message() message.description = self.description message.avatar_url = self.avatar_url - message.visibility = self.visibility message.git_url = self.git_url message.buildbot_job_id = self.buildbot_job_id message.built = self.built + message.translation_branch = self.translation_branch return message def update(self, message): self.description = message.description - self.visibility = message.visibility + self.translation_branch = message.translation_branch if message.git_url != self.git_url: self._update_buildbot_job(message.git_url) self.git_url = message.git_url From ec7ee870bac046e077bc89ae58844eccb4b188e1 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Wed, 18 Nov 2015 14:32:24 -0800 Subject: [PATCH 30/46] Group memberships. --- app/buildbot/buildbot.py | 5 +++++ app/filesets/services.py | 11 +++++++---- app/groups/groups.py | 15 ++++++++++++++- app/groups/memberships.py | 21 +++++++++++++++------ app/groups/messages.py | 2 +- app/projects/messages.py | 17 ++++++++--------- app/projects/projects.py | 24 ++++++++++++++++++++++++ app/projects/services.py | 19 ++++++++++++++++--- appengine_config.py | 2 ++ config/jetway.yaml.example | 0 10 files changed, 92 insertions(+), 24 deletions(-) mode change 100755 => 100644 config/jetway.yaml.example diff --git a/app/buildbot/buildbot.py b/app/buildbot/buildbot.py index 2e4dfc4..a34649c 100644 --- a/app/buildbot/buildbot.py +++ b/app/buildbot/buildbot.py @@ -39,6 +39,7 @@ def create_job(self, git_url, remote): } try: resp = requests.post(BASE + '/jobs', json=data, auth=self.auth) + resp.raise_for_status() except Exception as e: raise ConnectionError(e) content = resp.json() @@ -49,6 +50,7 @@ def create_job(self, git_url, remote): def get_job(self, job_id): try: resp = requests.get(BASE + '/jobs/{}'.format(job_id), auth=self.auth) + resp.raise_for_status() except Exception as e: raise ConnectionError(e) content = resp.json() @@ -61,6 +63,7 @@ def get_contents(self, job_id, path=None, ref=None): try: request_path = BASE + '/git/repos/{}/contents{}'.format(job_id, path) resp = requests.get(request_path, auth=self.auth) + resp.raise_for_status() except Exception as e: raise ConnectionError(e) content = resp.json() @@ -72,6 +75,7 @@ def read_file(self, job_id, path, ref): try: request_path = BASE + '/git/repos/{}/raw/{}{}'.format(job_id, ref, path) resp = requests.get(request_path, auth=self.auth) + resp.raise_for_status() except Exception as e: raise ConnectionError(e) return resp.content @@ -91,6 +95,7 @@ def write_file(self, job_id, path, contents, message, ref, sha, committer, autho BASE + '/jobs/{}/contents/update'.format(job_id), json=data, auth=self.auth) + resp.raise_for_status() except Exception as e: raise ConnectionError(e) result = resp.json() diff --git a/app/filesets/services.py b/app/filesets/services.py index cea2bf4..8ef043b 100644 --- a/app/filesets/services.py +++ b/app/filesets/services.py @@ -166,7 +166,7 @@ def _get_me(self, request): if user is None: raise api.UnauthorizedError('You must be logged in to do this.') email = user.email() - return users.User.get_by_email(email) + return users.User.get_or_create_by_email(email) def _get_or_create_fileset(self, request, me): allow_fileset_by_commit = bool(request.fileset.commit) @@ -197,11 +197,14 @@ def finalize(self, request): messages.SignRequestsResponse) def sign_requests(self, request): me = self._get_me(request) - fileset = self._get_or_create_fileset(request, me) - policy = policies.ProjectPolicy(me, fileset.project) + project = self._get_project(request) + policy = policies.ProjectPolicy(me, project) if (not self._is_authorized_buildbot() and not policy.can_write()): - raise api.ForbiddenError('Forbidden.') + text = 'Forbidden. {} cannot write to {}.' + raise api.ForbiddenError(text.format(me.email, project.name)) + + fileset = self._get_or_create_fileset(request, me) signed_reqs = fileset.sign_requests(request.unsigned_requests) resp = messages.SignRequestsResponse() resp.fileset = fileset.to_message() diff --git a/app/groups/groups.py b/app/groups/groups.py index b1079b8..4106f49 100644 --- a/app/groups/groups.py +++ b/app/groups/groups.py @@ -27,7 +27,7 @@ def get(cls, ident): def create(cls, project=None): group = cls() if project: - group.project_key = project + group.project_key = project.key group.put() return group @@ -41,6 +41,19 @@ def validate(self): # TODO: Decide if we need this check. # raise memberships.MembershipConflictError('Must be at least one admin.') + def update_membership(self, membership_message): + new_mem = memberships.Membership.from_message(membership_message) + for i, mem in enumerate(self.memberships): + if new_mem.user_key and mem.user_key == new_mem.user_key: + mem.update(membership_message) + self.memberships[i] = mem + if new_mem.domain and mem.domain == new_mem.domain: + mem.update(membership_message) + self.memberships[i] = mem + self.validate() + self.put() + return self + def create_membership(self, membership_message): mem = memberships.Membership.from_message(membership_message) mem.check_conflict(self.memberships) diff --git a/app/groups/memberships.py b/app/groups/memberships.py index 5c26252..eb8337f 100644 --- a/app/groups/memberships.py +++ b/app/groups/memberships.py @@ -21,14 +21,20 @@ class Membership(ndb.Model): def check_conflict(self, other_memberships): for mem in other_memberships: if self.user_key and self.user_key == mem.user_key: - raise MembershipConflictError('{} is already a member.'.format(self.user.nickname)) + text = '{} is already a member.' + raise MembershipConflictError(text.format(self.user.nickname)) if self.domain and self.domain == mem.domain: - raise MembershipConflictError('{} is already a member.'.format(self.domain)) + text = '{} is already a member.' + raise MembershipConflictError(text.format(self.domain)) @classmethod def from_message(cls, message): mem = cls() - mem.role = message.role + mem.update(message) + return mem + + def update(self, message): + self.role = message.role if message.user: if message.user.email: user = users.User.get_or_create_by_email(message.user.email) @@ -36,10 +42,12 @@ def from_message(cls, message): user = users.User.get_by_ident(message.user.email) else: raise ValueError('User not found.') - mem.user_key = user.key + self.user_key = user.key if message.domain: - mem.domain = message.domain - return mem + self.domain = message.domain + if not self.role: + self.role = messages.Role.READ + return self @webapp2.cached_property def user(self): @@ -48,6 +56,7 @@ def user(self): def to_message(self): message = messages.MembershipMessage() + message.role = self.role if self.user_key: message.user = self.user.to_message() if self.domain: diff --git a/app/groups/messages.py b/app/groups/messages.py index 4c10775..09c4b4e 100644 --- a/app/groups/messages.py +++ b/app/groups/messages.py @@ -7,7 +7,7 @@ class Role(messages.Enum): ADMIN = 1 READ = 2 WRITE = 3 - WRITE_TRANSLATIONS = 4 + TRANSLATE = 4 class Kind(messages.Enum): diff --git a/app/projects/messages.py b/app/projects/messages.py index 8563fc1..164e6c1 100644 --- a/app/projects/messages.py +++ b/app/projects/messages.py @@ -13,6 +13,13 @@ class Order(messages.Enum): NAME = 0 +class BuildbotGitStatus(messages.Enum): + NONE = 1 + SYNCING = 2 + ERROR = 3 + CONNECTED = 4 + + class ProjectMessage(messages.Message): nickname = messages.StringField(1) ident = messages.StringField(2) @@ -24,6 +31,7 @@ class ProjectMessage(messages.Message): buildbot_job_id = messages.StringField(11) git_url = messages.StringField(12) translation_branch = messages.StringField(13) + buildbot_git_status = messages.EnumField(BuildbotGitStatus, 14) ### @@ -75,12 +83,3 @@ class DeleteProjectRequest(messages.Message): class DeleteProjectResponse(messages.Message): pass - - -class CanRequest(messages.Message): - project = messages.MessageField(ProjectMessage, 1) - permission = messages.EnumField(Permission, 2) - - -class CanResponse(messages.Message): - can = messages.BooleanField(1) diff --git a/app/projects/projects.py b/app/projects/projects.py index 5d69a07..20d53a8 100644 --- a/app/projects/projects.py +++ b/app/projects/projects.py @@ -3,6 +3,7 @@ from ..buildbot import messages as buildbot_messages from ..catalogs import catalogs from ..groups import groups +from ..groups import messages as group_messages from ..policies import policies from google.appengine.ext import ndb from google.appengine.ext.ndb import msgprop @@ -33,6 +34,10 @@ class ProjectDoesNotExistError(Error): pass +class GitIntegrationError(Error): + pass + + class Project(ndb.Model): created = ndb.DateTimeProperty(auto_now_add=True) nickname = ndb.StringProperty() @@ -44,6 +49,7 @@ class Project(ndb.Model): git_url = ndb.StringProperty() group_key = ndb.KeyProperty() translation_branch = ndb.StringProperty() + buildbot_git_status = msgprop.EnumProperty(messages.BuildbotGitStatus) @property def name(self): @@ -79,6 +85,7 @@ def create(cls, owner, nickname, created_by, description=None, git_url=None): git_url=git_url, description=description) project.put() + project._init_default_group() project._update_buildbot_job(project.git_url) return project @@ -119,6 +126,16 @@ def _update_buildbot_job(self, git_url): except buildbot.Error: logging.exception('Buildbot connection error.') + def _init_default_group(self): + group = groups.Group.create(self) + if appengine_config.DEFAULT_DOMAIN: + mem_message = group_messages.MembershipMessage( + domain=appengine_config.DEFAULT_DOMAIN) + group.create_membership(mem_message) + self.group_key = group.key + self.put() + return group + @classmethod def search(cls, owner=None, order=None): query = cls.query() @@ -257,7 +274,12 @@ def delete_named_fileset(self, name): def list_named_filesets(self): return named_filesets.NamedFileset.search(project=self) + def verify_repo_status(self): + if not self.buildbot_job_id: + raise GitIntegrationError('Git repository not initialized.') + def list_branches(self): + self.verify_repo_status() bot = buildbot.Buildbot() job = bot.get_job(self.buildbot_job_id)['job'] results = [] @@ -274,6 +296,7 @@ def list_branches(self): return results def list_catalogs(self): + self.verify_repo_status() bot = buildbot.Buildbot() items = bot.get_contents(self.buildbot_job_id, path='/translations/') catalog_objs = [] @@ -284,4 +307,5 @@ def list_catalogs(self): return catalog_objs def get_catalog(self, locale): + self.verify_repo_status() return catalogs.Catalog(project=self, locale=locale) diff --git a/app/projects/services.py b/app/projects/services.py index e62e0b1..21d79a5 100644 --- a/app/projects/services.py +++ b/app/projects/services.py @@ -155,7 +155,7 @@ def list_branches(self, request): self._get_policy(project).authorize_read() try: branches = project.list_branches() - except buildbot.Error as e: + except (buildbot.Error, projects.GitIntegrationError) as e: raise api.Error(str(e)) resp = service_messages.ListBranchesResponse() resp.branches = branches @@ -168,7 +168,7 @@ def list_catalogs(self, request): self._get_policy(project).authorize_read() try: catalogs = project.list_catalogs() - except buildbot.Error as e: + except (buildbot.Error, projects.GitIntegrationError) as e: raise api.Error(str(e)) resp = service_messages.ListCatalogsResponse() resp.catalogs = [catalog.to_message(included=[]) for catalog in catalogs] @@ -181,7 +181,7 @@ def get_catalog(self, request): self._get_policy(project).authorize_read() try: catalog = project.get_catalog(request.catalog.locale) - except buildbot.Error as e: + except (buildbot.Error, projects.GitIntegrationError) as e: raise api.Error(str(e)) resp = service_messages.CatalogResponse() resp.catalog = catalog.to_message() @@ -226,6 +226,19 @@ def get_group(self, request): resp.group = project.group.to_message() return resp + @remote.method(service_messages.MembershipRequest, + service_messages.GroupResponse) + def update_membership(self, request): + project = self._get_project(request) + self._get_policy(project).authorize_admin() + try: + project.group.update_membership(request.membership) + except groups.Error as e: + raise api.Error(str(e)) + resp = service_messages.GroupResponse() + resp.group = project.group.to_message() + return resp + @remote.method(service_messages.MembershipRequest, service_messages.GroupResponse) def create_membership(self, request): diff --git a/appengine_config.py b/appengine_config.py index 6066e16..02105b3 100644 --- a/appengine_config.py +++ b/appengine_config.py @@ -44,6 +44,8 @@ ALLOWED_USER_DOMAINS = jetway_config.get('options', {}).get('allowed_user_domains', None) DEFAULT_USER_DOMAINS = jetway_config.get('options', {}).get('default_user_domains', None) +DEFAULT_DOMAIN = 'google.com' + REQUIRE_HTTPS_FOR_PREVIEWS = jetway_config.get('require_https', {}).get('preview_domain', False) HTTPS_PROXY_ENABLED_FOR_PREVIEWS = jetway_config.get('require_https', {}).get('behind_proxy', False) diff --git a/config/jetway.yaml.example b/config/jetway.yaml.example old mode 100755 new mode 100644 From 5e43ab281765df4355e3c0d8cf9248634b2a14ce Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Wed, 18 Nov 2015 16:45:51 -0800 Subject: [PATCH 31/46] Add refs to translations service. --- app/projects/projects.py | 11 +++++++---- app/projects/service_messages.py | 1 + app/projects/services.py | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/projects/projects.py b/app/projects/projects.py index 20d53a8..0e014ca 100644 --- a/app/projects/projects.py +++ b/app/projects/projects.py @@ -295,10 +295,13 @@ def list_branches(self): results = sorted(results, key=lambda message: message.name) return results - def list_catalogs(self): + def list_catalogs(self, ref=None): self.verify_repo_status() bot = buildbot.Buildbot() - items = bot.get_contents(self.buildbot_job_id, path='/translations/') + items = bot.get_contents( + self.buildbot_job_id, + path='/translations/', + ref=ref) catalog_objs = [] for item in items: if item['type'] == 'dir': @@ -306,6 +309,6 @@ def list_catalogs(self): catalog_objs.append(catalog) return catalog_objs - def get_catalog(self, locale): + def get_catalog(self, locale, ref=None): self.verify_repo_status() - return catalogs.Catalog(project=self, locale=locale) + return catalogs.Catalog(project=self, locale=locale, ref=ref) diff --git a/app/projects/service_messages.py b/app/projects/service_messages.py index 82f70fe..36af4e5 100644 --- a/app/projects/service_messages.py +++ b/app/projects/service_messages.py @@ -113,6 +113,7 @@ class ListBranchesResponse(messages.Message): class ProjectRequest(messages.Message): project = messages.MessageField(ProjectMessage, 1) + branch = messages.StringField(2) class ListCatalogsResponse(messages.Message): diff --git a/app/projects/services.py b/app/projects/services.py index 21d79a5..b0fab02 100644 --- a/app/projects/services.py +++ b/app/projects/services.py @@ -167,7 +167,7 @@ def list_catalogs(self, request): project = self._get_project(request) self._get_policy(project).authorize_read() try: - catalogs = project.list_catalogs() + catalogs = project.list_catalogs(ref=request.branch) except (buildbot.Error, projects.GitIntegrationError) as e: raise api.Error(str(e)) resp = service_messages.ListCatalogsResponse() @@ -180,7 +180,9 @@ def get_catalog(self, request): project = self._get_project(request) self._get_policy(project).authorize_read() try: - catalog = project.get_catalog(request.catalog.locale) + catalog = project.get_catalog( + locale=request.catalog.locale, + ref=request.catalog.ref) except (buildbot.Error, projects.GitIntegrationError) as e: raise api.Error(str(e)) resp = service_messages.CatalogResponse() From 077412058634aa2a9f6458a9184dd3ff71082465 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Sun, 29 Nov 2015 13:44:26 -0800 Subject: [PATCH 32/46] Remove frontend deps. --- bower.json | 4 ---- gulpfile.js | 56 --------------------------------------------------- package.json | 16 --------------- scripts/setup | 7 ------- 4 files changed, 83 deletions(-) delete mode 100644 bower.json delete mode 100644 gulpfile.js delete mode 100644 package.json diff --git a/bower.json b/bower.json deleted file mode 100644 index f45c7dd..0000000 --- a/bower.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "webreview", - "private": true -} diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index 16e51c3..0000000 --- a/gulpfile.js +++ /dev/null @@ -1,56 +0,0 @@ -var autoprefixer = require('gulp-autoprefixer'); -var concat = require('gulp-concat'); -var es = require('event-stream'); -var gulp = require('gulp'); -var plumber = require('gulp-plumber'); -var sass = require('gulp-sass'); -var stylish = require('jshint-stylish'); -var uglify = require('gulp-uglify'); - - -var Path = { - CSS_OUT_DIR: './dist/css/', - CSS_SOURCES: './jetway/frontend/static/sass/*', - JS_OUT_DIR: './dist/js/', - JS_SOURCES: './jetway/frontend/static/js/*.js', -}; - - -gulp.task('sass', function() { - var appFiles = gulp.src('./jetway/frontend/static/sass/*.scss') - .pipe(plumber()) - .pipe(sass({ - outputStyle: 'compressed' - })) - var vendorFiles = gulp.src([ - './bower_components/bootstrap/dist/css/bootstrap.min.css', - ]) - return es.concat(vendorFiles, appFiles) - .pipe(concat('main.min.css')) - .pipe(autoprefixer()) - .pipe(gulp.dest(Path.CSS_OUT_DIR)); -}); - - -gulp.task('minify', function(){ - return gulp.src([ - './bower_components/angular/angular.min.js', - './bower_components/angular-bootstrap/ui-bootstrap.min.js', - './bower_components/angular-ui-router/release/angular-ui-router.min.js', - './bower_components/angular-xeditable/dist/js/xeditable.js', - './jetway/frontend/static/js/controllers.js', - Path.JS_SOURCES, - ]) - .pipe(concat('main.min.js')) - .pipe(gulp.dest(Path.JS_OUT_DIR)); -}); - - -gulp.task('watch', function() { - gulp.watch([Path.JS_SOURCES], ['minify']); - gulp.watch([Path.CSS_SOURCES], ['sass']); -}); - - -gulp.task('build', ['sass', 'minify']); -gulp.task('default', ['sass', 'minify', 'watch']); diff --git a/package.json b/package.json deleted file mode 100644 index 28153ef..0000000 --- a/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "jetway", - "private": true, - "devDependencies": { - "bower": "*", - "gulp": "*", - "event-stream": "3.3.1", - "gulp-autoprefixer": "^2.1.0", - "gulp-sass": "^1.3.3", - "gulp-concat": "*", - "gulp-plumber": "^1.0.0", - "gulp-uglify": "*", - "jshint-stylish": "*", - "run-sequence": "^1.0.2" - } -} diff --git a/scripts/setup b/scripts/setup index fb4a726..365e67e 100755 --- a/scripts/setup +++ b/scripts/setup @@ -15,16 +15,9 @@ gaenv -h 2>&1> /dev/null || { sudo pip install gaenv } -node --version 2>&1> /dev/null || { - echo "node not installed. Install from: http://nodejs.org/" - exit 1 -} - virtualenv env source env/bin/activate pip install --upgrade --allow-unverified PIL --allow-external PIL -r requirements.txt -npm install -./node_modules/.bin/bower install gaenv --lib lib --no-import deactivate From 359a7275746817079f9eafb7f605e3ea0537d246 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Sun, 29 Nov 2015 13:56:49 -0800 Subject: [PATCH 33/46] Add webreview-fe as a submodule. --- .gitmodules | 3 +++ webreview-fe | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 webreview-fe diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c111764 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "webreview-fe"] + path = webreview-fe + url = https://github.com/grow/webreview-fe diff --git a/webreview-fe b/webreview-fe new file mode 160000 index 0000000..ee6fa85 --- /dev/null +++ b/webreview-fe @@ -0,0 +1 @@ +Subproject commit ee6fa85f1651be940fc2edeca7161117bb17eb97 From f2abd33eb6a9aec482ef812534f7892ff45a7c14 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Sun, 29 Nov 2015 14:09:44 -0800 Subject: [PATCH 34/46] Serve webreview-fe app. --- app.yaml | 17 ++--------------- app/frontend/handlers.py | 4 +++- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/app.yaml b/app.yaml index 1824f5d..7253669 100644 --- a/app.yaml +++ b/app.yaml @@ -34,21 +34,8 @@ handlers: - url: /_ah/spi/.* script: app.main.endpoints_app -- url: /_app/[^/]*/config/(.*\.(svg|png|gif|jpg))$ - static_files: config/\1 - upload: config/.*\.(svg|png|gif|jpg)$ - -- url: /_app/[^/]*/static/css - static_dir: dist/css - -- url: /_app/[^/]*/static/js - static_dir: dist/js - -- url: /_app/[^/]*/static/html - static_dir: app/frontend/static/html - -- url: /_app/[^/]*/static/images - static_dir: app/frontend/static/images +- url: /assets + static_dir: webreview-fe/dist/assets - url: /.* script: app.main.app diff --git a/app/frontend/handlers.py b/app/frontend/handlers.py index 359e0ba..d3a0c01 100644 --- a/app/frontend/handlers.py +++ b/app/frontend/handlers.py @@ -7,7 +7,9 @@ import os import webapp2 -_path = os.path.join(os.path.dirname(__file__), 'templates') +_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), '..', '..', 'webreview-fe', 'dist')) _loader = jinja2.FileSystemLoader(_path) _env = jinja2.Environment(loader=_loader, autoescape=True, trim_blocks=True) From 9e2d304074085db183bddaeb13fc276f7df00009 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Tue, 1 Dec 2015 13:52:36 -0800 Subject: [PATCH 35/46] Improve ember app build integration. Fix avatars on versioned URLs. --- app.yaml | 4 +++- app/avatars/avatars.py | 5 +++-- app/frontend/handlers.py | 2 +- scripts/deploy | 1 + 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app.yaml b/app.yaml index 7253669..6f0428b 100644 --- a/app.yaml +++ b/app.yaml @@ -35,7 +35,7 @@ handlers: script: app.main.endpoints_app - url: /assets - static_dir: webreview-fe/dist/assets + static_dir: dist/assets - url: /.* script: app.main.app @@ -56,6 +56,8 @@ skip_files: - env - htmlconv - lib/Crypto +- lib/PIL - node_modules - testing - ^.*.example +- webreview-fe diff --git a/app/avatars/avatars.py b/app/avatars/avatars.py index 63d3a88..de53c5a 100644 --- a/app/avatars/avatars.py +++ b/app/avatars/avatars.py @@ -65,7 +65,8 @@ def create_upload_url(cls, entity): letter = entity.key.kind()[:1].lower() avatar_ident = '{}/{}'.format(letter, entity.ident) root = '{}/jetway/avatars/{}'.format(gcs_bucket, avatar_ident) - return blobstore.create_upload_url('/avatars/{}'.format(avatar_ident), gs_bucket_name=root) + return blobstore.create_upload_url( + '/avatars/{}'.format(avatar_ident), gs_bucket_name=root) def update(self, gs_object_name): if self.gs_basename: @@ -80,7 +81,7 @@ def create_url(cls, owner): return path num = owner.ident[0] scheme = os.getenv('wsgi.url_scheme') - hostname = os.getenv('DEFAULT_VERSION_HOSTNAME') + hostname = os.getenv('HTTP_HOST') sep = '.' if scheme == 'http' else '-dot-' return '//avatars-{}{}{}{}'.format(num, sep, hostname, path) diff --git a/app/frontend/handlers.py b/app/frontend/handlers.py index d3a0c01..1c52ac9 100644 --- a/app/frontend/handlers.py +++ b/app/frontend/handlers.py @@ -9,7 +9,7 @@ _path = os.path.abspath( os.path.join( - os.path.dirname(__file__), '..', '..', 'webreview-fe', 'dist')) + os.path.dirname(__file__), '..', '..', 'dist')) _loader = jinja2.FileSystemLoader(_path) _env = jinja2.Environment(loader=_loader, autoescape=True, trim_blocks=True) diff --git a/scripts/deploy b/scripts/deploy index e956e54..70ad374 100755 --- a/scripts/deploy +++ b/scripts/deploy @@ -1,5 +1,6 @@ #!/bin/bash +cd webreview-fe && ember build && cd .. ./scripts/test if [ -d env ]; then From 3a61f962b179287230dde0ce104b1dee9c85b5a0 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Thu, 10 Dec 2015 16:43:09 -0800 Subject: [PATCH 36/46] Add transfer owner method to projects. --- app/avatars/avatars.py | 2 ++ app/buildbot/buildbot.py | 18 ++++++++++++------ app/projects/messages.py | 5 +++++ app/projects/projects.py | 12 +++++++++--- app/projects/projects_test.py | 7 +++++++ app/projects/services.py | 11 +++++++++++ app/testing.py | 4 ++++ 7 files changed, 50 insertions(+), 9 deletions(-) diff --git a/app/avatars/avatars.py b/app/avatars/avatars.py index de53c5a..91f0565 100644 --- a/app/avatars/avatars.py +++ b/app/avatars/avatars.py @@ -83,6 +83,8 @@ def create_url(cls, owner): scheme = os.getenv('wsgi.url_scheme') hostname = os.getenv('HTTP_HOST') sep = '.' if scheme == 'http' else '-dot-' + if '-dot-' in hostname: + return '//{}{}'.format(hostname, path) return '//avatars-{}{}{}{}'.format(num, sep, hostname, path) @classmethod diff --git a/app/buildbot/buildbot.py b/app/buildbot/buildbot.py index a34649c..740d7b0 100644 --- a/app/buildbot/buildbot.py +++ b/app/buildbot/buildbot.py @@ -4,6 +4,8 @@ BASE = '{}/api'.format(appengine_config.BUILDBOT_URL) +VERIFY = False + class Error(Exception): pass @@ -38,7 +40,8 @@ def create_job(self, git_url, remote): 'env': self.env, } try: - resp = requests.post(BASE + '/jobs', json=data, auth=self.auth) + resp = requests.post(BASE + '/jobs', json=data, auth=self.auth, + verify=VERIFY) resp.raise_for_status() except Exception as e: raise ConnectionError(e) @@ -49,7 +52,8 @@ def create_job(self, git_url, remote): def get_job(self, job_id): try: - resp = requests.get(BASE + '/jobs/{}'.format(job_id), auth=self.auth) + resp = requests.get(BASE + '/jobs/{}'.format(job_id), auth=self.auth, + verify=VERIFY) resp.raise_for_status() except Exception as e: raise ConnectionError(e) @@ -62,7 +66,7 @@ def get_contents(self, job_id, path=None, ref=None): path = path or '/' try: request_path = BASE + '/git/repos/{}/contents{}'.format(job_id, path) - resp = requests.get(request_path, auth=self.auth) + resp = requests.get(request_path, auth=self.auth, verify=VERIFY) resp.raise_for_status() except Exception as e: raise ConnectionError(e) @@ -74,13 +78,14 @@ def get_contents(self, job_id, path=None, ref=None): def read_file(self, job_id, path, ref): try: request_path = BASE + '/git/repos/{}/raw/{}{}'.format(job_id, ref, path) - resp = requests.get(request_path, auth=self.auth) + resp = requests.get(request_path, auth=self.auth, verify=VERIFY) resp.raise_for_status() except Exception as e: raise ConnectionError(e) return resp.content - def write_file(self, job_id, path, contents, message, ref, sha, committer, author): + def write_file(self, job_id, path, contents, message, ref, sha, + committer, author): data = { 'branch': ref, 'path': path, @@ -94,7 +99,8 @@ def write_file(self, job_id, path, contents, message, ref, sha, committer, autho resp = requests.post( BASE + '/jobs/{}/contents/update'.format(job_id), json=data, - auth=self.auth) + auth=self.auth, + verify=VERIFY) resp.raise_for_status() except Exception as e: raise ConnectionError(e) diff --git a/app/projects/messages.py b/app/projects/messages.py index 164e6c1..e3c42c6 100644 --- a/app/projects/messages.py +++ b/app/projects/messages.py @@ -83,3 +83,8 @@ class DeleteProjectRequest(messages.Message): class DeleteProjectResponse(messages.Message): pass + + +class TransferOwnerRequest(messages.Message): + project = messages.MessageField(ProjectMessage, 1) + owner = messages.MessageField(owner_messages.OwnerMessage, 2) diff --git a/app/projects/projects.py b/app/projects/projects.py index 0e014ca..d8c11b9 100644 --- a/app/projects/projects.py +++ b/app/projects/projects.py @@ -110,6 +110,8 @@ def get(cls, owner=None, nickname=None): return project def _update_buildbot_job(self, git_url): + if self.git_url == git_url and self.buildbot_job_id: + return if not git_url: self.buildbot_job_id = None self.put() @@ -121,7 +123,8 @@ def _update_buildbot_job(self, git_url): git_url=git_url, remote=self.permalink) self.buildbot_job_id = str(resp['job_id']) - logging.info('Buildbot job ID update {} -> {}'.format(self, self.buildbot_job_id)) + text = 'Buildbot job ID update {} -> {}' + logging.info(text.format(self, self.buildbot_job_id)) self.put() except buildbot.Error: logging.exception('Buildbot connection error.') @@ -222,11 +225,14 @@ def to_message(self): def update(self, message): self.description = message.description self.translation_branch = message.translation_branch - if message.git_url != self.git_url: - self._update_buildbot_job(message.git_url) + self._update_buildbot_job(message.git_url) self.git_url = message.git_url self.put() + def transfer_owner(self, owner): + self.owner_key = owner.key + self.put() + def list_users_to_notify(self): return self.search_users(is_public=None) diff --git a/app/projects/projects_test.py b/app/projects/projects_test.py index 9688a30..9fde5c4 100644 --- a/app/projects/projects_test.py +++ b/app/projects/projects_test.py @@ -14,6 +14,13 @@ def test_named_filesets(self): self.assertItemsEqual([named_fileset], ents) self.project.delete_named_fileset('preview') + def test_transfer_owner(self): + owner = self.project.owner + self.assertEqual(owner, self.project.owner) + new_owner = self.create_owner('new-owner', 'new-owner@example.com') + self.project.transfer_owner(new_owner) + self.assertEqual(new_owner, self.project.owner) + if __name__ == '__main__': unittest.main() diff --git a/app/projects/services.py b/app/projects/services.py index b0fab02..ecb3541 100644 --- a/app/projects/services.py +++ b/app/projects/services.py @@ -266,3 +266,14 @@ def delete_membership(self, request): resp = service_messages.GroupResponse() resp.group = project.group.to_message() return resp + + @remote.method(service_messages.TransferOwnerRequest, + service_messages.GetProjectResponse) + def transfer_owner(self, request): + project = self._get_project(request) + self._get_policy(project).authorize_admin() + owner = owners.Owner.get(request.owner.nickname) + project.transfer_owner(owner) + resp = service_messages.GetProjectResponse() + resp.project = project.to_message() + return resp diff --git a/app/testing.py b/app/testing.py index 9ee268e..ea01505 100644 --- a/app/testing.py +++ b/app/testing.py @@ -34,3 +34,7 @@ def create_fileset(self): project = self.create_project() commit = fileset_messages.CommitMessage(branch='master', sha='1234567890') return project.create_fileset('master', commit=commit) + + def create_owner(self, nickname, email): + creator = users.User.create(nickname, email=email) + return owners.Owner.get(creator.nickname) From a7e28d938896efad4e07319d7bb03df644ff628e Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Fri, 27 May 2016 18:20:16 -0700 Subject: [PATCH 37/46] Work on project transfers. --- app/projects/projects.py | 2 +- app/projects/services.py | 2 +- app/users/users.py | 2 ++ webreview-fe | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/projects/projects.py b/app/projects/projects.py index d8c11b9..1faf4b2 100644 --- a/app/projects/projects.py +++ b/app/projects/projects.py @@ -245,9 +245,9 @@ def search_users(self, is_public=True): @classmethod def filter(cls, results, user, permission=messages.Permission.READ): - policy = policies.ProjectPolicy(user, self) filtered = [] for project in results: + policy = policies.ProjectPolicy(user, project) if policy.can_read(): filtered.append(project) return filtered diff --git a/app/projects/services.py b/app/projects/services.py index ecb3541..da0d1c8 100644 --- a/app/projects/services.py +++ b/app/projects/services.py @@ -269,7 +269,7 @@ def delete_membership(self, request): @remote.method(service_messages.TransferOwnerRequest, service_messages.GetProjectResponse) - def transfer_owner(self, request): + def transfer(self, request): project = self._get_project(request) self._get_policy(project).authorize_admin() owner = owners.Owner.get(request.owner.nickname) diff --git a/app/users/users.py b/app/users/users.py index dc03bb6..f592147 100644 --- a/app/users/users.py +++ b/app/users/users.py @@ -123,6 +123,8 @@ def get(cls, nickname): query = cls.query() query = query.filter(cls.nickname == nickname) user = query.get() + if user is None: + user = cls.get_by_email(nickname) if user is None: raise UserDoesNotExistError('User "{}" not found.'.format(nickname)) return user diff --git a/webreview-fe b/webreview-fe index ee6fa85..04f78c6 160000 --- a/webreview-fe +++ b/webreview-fe @@ -1 +1 @@ -Subproject commit ee6fa85f1651be940fc2edeca7161117bb17eb97 +Subproject commit 04f78c67715b40768decdc42b5ca84a0c6d0ff84 From 13f75607920b52ff982e53132d9994c1f9775a16 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Sat, 28 May 2016 00:21:11 -0700 Subject: [PATCH 38/46] Org groups. --- app/groups/groups.py | 9 +++-- app/groups/messages.py | 2 ++ app/orgs/messages.py | 2 ++ app/orgs/orgs.py | 36 +++++++++++++++++++- app/orgs/service_messages.py | 7 +++- app/orgs/services.py | 9 +++++ app/users/messages.py | 64 ----------------------------------- app/users/service_messages.py | 63 ++++++++++++++++++++++++++++++++++ app/users/services.py | 53 +++++++++++++++-------------- app/users/users.py | 13 +++++-- index.yaml | 10 ++++++ webreview-fe | 2 +- 12 files changed, 173 insertions(+), 97 deletions(-) create mode 100644 app/users/service_messages.py diff --git a/app/groups/groups.py b/app/groups/groups.py index 4106f49..70c8074 100644 --- a/app/groups/groups.py +++ b/app/groups/groups.py @@ -10,6 +10,7 @@ class Group(ndb.Model): memberships = ndb.StructuredProperty(memberships.Membership, repeated=True) project_key = ndb.KeyProperty() + org_key = ndb.KeyProperty() @property def ident(self): @@ -24,10 +25,12 @@ def get(cls, ident): return group @classmethod - def create(cls, project=None): + def create(cls, project=None, org=None): group = cls() if project: group.project_key = project.key + if org: + group.org_key = org.key group.put() return group @@ -86,8 +89,10 @@ def list_memberships(self, kind=None): def to_message(self): message = messages.GroupMessage() message.ident = self.ident - if self.project: + if hasattr(self, 'project') and self.project: message.project = self.project.to_message() + if hasattr(self, 'org') and self.org: + message.org = self.org.to_message() message.users = [mem.to_message() for mem in self.list_memberships(messages.Kind.USER)] message.domains = [mem.to_message() diff --git a/app/groups/messages.py b/app/groups/messages.py index 09c4b4e..0df2a7c 100644 --- a/app/groups/messages.py +++ b/app/groups/messages.py @@ -1,4 +1,5 @@ from ..projects import messages as project_messages +from ..orgs import messages as org_messages from ..users import messages as user_messages from protorpc import messages @@ -26,3 +27,4 @@ class GroupMessage(messages.Message): domains = messages.MessageField(MembershipMessage, 2, repeated=True) ident = messages.StringField(3) project = messages.MessageField(project_messages.ProjectMessage, 4) + org = messages.MessageField(org_messages.OrgMessage, 5) diff --git a/app/orgs/messages.py b/app/orgs/messages.py index 5a64c2c..844addf 100644 --- a/app/orgs/messages.py +++ b/app/orgs/messages.py @@ -1,5 +1,6 @@ from protorpc import messages from protorpc import message_types +from ..users import messages as user_messages class OrgMessage(messages.Message): @@ -11,3 +12,4 @@ class OrgMessage(messages.Message): updated = message_types.DateTimeField(6) avatar_url = messages.StringField(7) ident = messages.StringField(8) + owner = messages.MessageField(user_messages.UserMessage, 9) diff --git a/app/orgs/orgs.py b/app/orgs/orgs.py index b707ab0..5e7e634 100644 --- a/app/orgs/orgs.py +++ b/app/orgs/orgs.py @@ -30,6 +30,8 @@ class Org(ndb.Model): location = ndb.StringProperty() description = ndb.StringProperty() website_url = ndb.StringProperty() + owner_key = ndb.KeyProperty() + group_key = ndb.KeyProperty() def __repr__(self): return ''.format(self.nickname) @@ -42,7 +44,8 @@ def create(cls, nickname, created_by): except OrgDoesNotExistError: org = cls( nickname=nickname, - created_by_key=created_by.key) + created_by_key=created_by.key, + owner_key=created_by.key) org.put() return org @@ -72,6 +75,13 @@ def url(self): def ident(self): return str(self.key.id()) + @property + def owner(self): + if self.owner_key: + return self.owner_key.get() + if self.created_by_key: + return self.created_by_key.get() + def update(self, message): try: if Org.get(message.nickname) != self: @@ -92,6 +102,29 @@ def list(cls): def avatar_url(self): return avatars.Avatar.create_url(self) + @classmethod + def search(cls, owner=None): + query = cls.query() + if owner: + query = query.filter(cls.owner_key == owner.key) + return query.fetch() + + @property + def group(self): + def _create_group(): + group = groups.Group.create() + self.group_key = group.key + self.put() + group.org = self + return group + if not self.group_key: + return _create_group() + group = self.group_key.get() + if group is None: + return _create_group() + group.org = self + return group + def to_message(self): message = messages.OrgMessage() message.nickname = self.nickname @@ -101,4 +134,5 @@ def to_message(self): message.updated = self.updated message.avatar_url = self.avatar_url message.ident = self.ident + message.owner = self.owner.to_message() return message diff --git a/app/orgs/service_messages.py b/app/orgs/service_messages.py index ec8d68b..f29b741 100644 --- a/app/orgs/service_messages.py +++ b/app/orgs/service_messages.py @@ -1,6 +1,7 @@ -from protorpc import messages +from ..groups import messages as group_messages from app.orgs import messages as org_messages from app.users import messages as user_messages +from protorpc import messages class CreateOrgRequest(messages.Message): @@ -49,3 +50,7 @@ class SearchMembersRequest(messages.Message): class SearchMembersResponse(messages.Message): users = messages.MessageField(user_messages.UserMessage, 1, repeated=True) + + +class GroupResponse(messages.Message): + group = messages.MessageField(group_messages.GroupMessage, 1) diff --git a/app/orgs/services.py b/app/orgs/services.py index 65970e4..13db106 100644 --- a/app/orgs/services.py +++ b/app/orgs/services.py @@ -71,3 +71,12 @@ def search_members(self, request): message = service_messages.SearchMembersResponse() message.users = [user.to_message() for user in results] return message + + @remote.method(service_messages.GetOrgRequest, + service_messages.GroupResponse) + def get_group(self, request): + org = orgs.Org.get(request.org.nickname) +# self._get_policy(project).authorize_read() + resp = service_messages.GroupResponse() + resp.group = org.group.to_message() + return resp diff --git a/app/users/messages.py b/app/users/messages.py index 6d69502..17b1611 100644 --- a/app/users/messages.py +++ b/app/users/messages.py @@ -1,6 +1,4 @@ from protorpc import messages -from app.orgs import messages as org_messages -from app.projects import messages as project_messages class UserMessage(messages.Message): @@ -12,65 +10,3 @@ class UserMessage(messages.Message): description = messages.StringField(6) location = messages.StringField(7) name = messages.StringField(8) - - -### - - -class GetMeRequest(messages.Message): - pass - - -class GetMeResponse(messages.Message): - me = messages.MessageField(UserMessage, 1) # TODO: Deprecate "me". - user = messages.MessageField(UserMessage, 2) - - -class SignInRequest(messages.Message): - pass - - -class SignInResponse(messages.Message): - me = messages.MessageField(UserMessage, 1) - - -class SignOutRequest(messages.Message): - pass - - -class SignOutResponse(messages.Message): - pass - - -class UpdateMeRequest(messages.Message): - me = messages.MessageField(UserMessage, 1) # TODO: Deprecate "me". - user = messages.MessageField(UserMessage, 2) - - -class UpdateMeResponse(messages.Message): - me = messages.MessageField(UserMessage, 1) # TODO: Deprecate "me". - user = messages.MessageField(UserMessage, 2) - - -class SearchOrgsRequest(messages.Message): - user = messages.MessageField(UserMessage, 1) - - -class SearchOrgsResponse(messages.Message): - orgs = messages.MessageField(org_messages.OrgMessage, 1, repeated=True) - - -class SearchProjectsRequest(messages.Message): - user = messages.MessageField(UserMessage, 1) - - -class SearchProjectsResponse(messages.Message): - projects = messages.MessageField(project_messages.ProjectMessage, 1, repeated=True) - - -class SearchRequest(messages.Message): - project = messages.MessageField(project_messages.ProjectMessage, 1) - - -class SearchResponse(messages.Message): - users = messages.MessageField(UserMessage, 1, repeated=True) diff --git a/app/users/service_messages.py b/app/users/service_messages.py new file mode 100644 index 0000000..5f30956 --- /dev/null +++ b/app/users/service_messages.py @@ -0,0 +1,63 @@ +from .messages import UserMessage +from app.orgs import messages as org_messages +from app.projects import messages as project_messages +from protorpc import messages + + +class GetMeRequest(messages.Message): + pass + + +class GetMeResponse(messages.Message): + me = messages.MessageField(UserMessage, 1) # TODO: Deprecate "me". + user = messages.MessageField(UserMessage, 2) + + +class SignInRequest(messages.Message): + pass + + +class SignInResponse(messages.Message): + me = messages.MessageField(UserMessage, 1) + + +class SignOutRequest(messages.Message): + pass + + +class SignOutResponse(messages.Message): + pass + + +class UpdateMeRequest(messages.Message): + me = messages.MessageField(UserMessage, 1) # TODO: Deprecate "me". + user = messages.MessageField(UserMessage, 2) + + +class UpdateMeResponse(messages.Message): + me = messages.MessageField(UserMessage, 1) # TODO: Deprecate "me". + user = messages.MessageField(UserMessage, 2) + + +class SearchOrgsRequest(messages.Message): + user = messages.MessageField(UserMessage, 1) + + +class SearchOrgsResponse(messages.Message): + orgs = messages.MessageField(org_messages.OrgMessage, 1, repeated=True) + + +class SearchProjectsRequest(messages.Message): + user = messages.MessageField(UserMessage, 1) + + +class SearchProjectsResponse(messages.Message): + projects = messages.MessageField(project_messages.ProjectMessage, 1, repeated=True) + + +class SearchRequest(messages.Message): + project = messages.MessageField(project_messages.ProjectMessage, 1) + + +class SearchResponse(messages.Message): + users = messages.MessageField(UserMessage, 1, repeated=True) diff --git a/app/users/services.py b/app/users/services.py index 08e884e..28a2f17 100644 --- a/app/users/services.py +++ b/app/users/services.py @@ -1,64 +1,65 @@ +from . import service_messages from app import api -from app.users import messages from app.owners import owners from app.projects import projects -from app.users import users from app.projects import watcher_messages +from app.users import messages +from app.users import users from protorpc import remote class MeService(api.Service): - @remote.method(messages.GetMeRequest, - messages.GetMeResponse) + @remote.method(service_messages.GetMeRequest, + service_messages.GetMeResponse) @api.me_required def get(self, request): - resp = messages.GetMeResponse() + resp = service_messages.GetMeResponse() resp.me = self.me.to_me_message() resp.user = resp.me return resp - @remote.method(messages.SignInRequest, - messages.SignInResponse) + @remote.method(service_messages.SignInRequest, + service_messages.SignInResponse) def sign_in(self, request): - resp = messages.SignInResponse() + resp = service_messages.SignInResponse() return resp - @remote.method(messages.SignOutRequest, - messages.SignOutResponse) + @remote.method(service_messages.SignOutRequest, + service_messages.SignOutResponse) @api.me_required def sign_out(self, request): - resp = messages.SignOutResponse() + resp = service_messages.SignOutResponse() return resp - @remote.method(messages.UpdateMeRequest, - messages.UpdateMeResponse) + @remote.method(service_messages.UpdateMeRequest, + service_messages.UpdateMeResponse) @api.me_required def update(self, request): try: self.me.update(request.user) except users.UserExistsError as e: raise api.ConflictError(str(e)) - resp = messages.UpdateMeResponse() + resp = service_messages.UpdateMeResponse() resp.me = self.me.to_me_message() resp.user = resp.me return resp - @remote.method(messages.SearchProjectsRequest, - messages.SearchProjectsResponse) + @remote.method(service_messages.SearchProjectsRequest, + service_messages.SearchProjectsResponse) @api.me_required def search_projects(self, request): results = self.me.search_projects() - resp = messages.SearchProjectsResponse() + resp = service_messages.SearchProjectsResponse() resp.projects = [project.to_message() for project in results] return resp - @remote.method(messages.SearchOrgsRequest, - messages.SearchOrgsResponse) + @remote.method(service_messages.SearchOrgsRequest, + service_messages.SearchOrgsResponse) @api.me_required def search_orgs(self, request): results = self.me.search_orgs() - resp = messages.SearchOrgsResponse() + resp = service_messages.SearchOrgsResponse() resp.orgs = [org.to_message() for org in results] return resp @@ -84,20 +85,20 @@ def _get_project(self, request): projects.ProjectDoesNotExistError) as e: raise api.NotFoundError(str(e)) - @remote.method(messages.SearchOrgsRequest, - messages.SearchOrgsResponse) + @remote.method(service_messages.SearchOrgsRequest, + service_messages.SearchOrgsResponse) def search_orgs(self, request): user = users.User.get(request.user.nickname) results = user.search_orgs() - resp = messages.SearchOrgsResponse() + resp = service_messages.SearchOrgsResponse() resp.orgs = [org.to_message() for org in results] return resp - @remote.method(messages.SearchRequest, - messages.SearchResponse) + @remote.method(service_messages.SearchRequest, + service_messages.SearchResponse) def search(self, request): project = self._get_project(request) results = project.search_users() - resp = messages.SearchResponse() + resp = service_messages.SearchResponse() resp.users = [user.to_message() for user in results] return resp diff --git a/app/users/users.py b/app/users/users.py index f592147..fcacca9 100644 --- a/app/users/users.py +++ b/app/users/users.py @@ -204,12 +204,21 @@ def update(self, message): def search_teams(self): from app.groups import groups query = groups.Group.query() + query = query.filter(groups.Group.project_key != None) query = query.filter(groups.Group.memberships.user_key == self.key) return query.fetch() def search_orgs(self): - # TODO: Implement. - return [] + from app.groups import groups + query = groups.Group.query() + query = query.filter(groups.Group.org_key != None) + query = query.filter(groups.Group.memberships.user_key == self.key) + results = query.fetch() + org_keys = [result.org_key for result in results] + from app.orgs import orgs + org_ents = orgs.Org.search(owner=self) or [] + org_ents += ndb.get_multi(list(set(org_keys))) + return org_ents def search_projects(self): team_ents = self.search_teams() diff --git a/index.yaml b/index.yaml index 1910a5c..6b040aa 100644 --- a/index.yaml +++ b/index.yaml @@ -44,6 +44,16 @@ indexes: - name: modified direction: desc +- kind: Group + properties: + - name: memberships.user_key + - name: org_key + +- kind: Group + properties: + - name: memberships.user_key + - name: project_key + - kind: Project properties: - name: owner_key diff --git a/webreview-fe b/webreview-fe index 04f78c6..aeaedbf 160000 --- a/webreview-fe +++ b/webreview-fe @@ -1 +1 @@ -Subproject commit 04f78c67715b40768decdc42b5ca84a0c6d0ff84 +Subproject commit aeaedbfcfb4b9f8cfbed0341bdea0fb2638e9942 From f0bec97a6e8ccaf429f4d686e40b6d95309eae3d Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Sun, 29 May 2016 00:59:27 -0700 Subject: [PATCH 39/46] Org owners. --- app/files/files.py | 6 ++++++ app/orgs/orgs.py | 9 +++++++++ app/orgs/service_messages.py | 5 +++++ app/orgs/services.py | 29 ++++++++++++++++++++++++++++- app/policies/projects.py | 3 +++ webreview-fe | 2 +- 6 files changed, 52 insertions(+), 2 deletions(-) diff --git a/app/files/files.py b/app/files/files.py index 207a2e3..dfa2b65 100644 --- a/app/files/files.py +++ b/app/files/files.py @@ -2,6 +2,7 @@ import cloudstorage from datetime import datetime from google.appengine.ext import blobstore +from google.appengine.ext import ndb from app.files import messages from app.utils import gcs import os @@ -16,6 +17,11 @@ class FileNotFoundError(Error): pass +class FileMap(ndb.Model): + md5 = ndb.StringProperty() + path = ndb.StringProperty() + + class Signer(object): def __init__(self, root): diff --git a/app/orgs/orgs.py b/app/orgs/orgs.py index 5e7e634..d7a1f24 100644 --- a/app/orgs/orgs.py +++ b/app/orgs/orgs.py @@ -1,6 +1,7 @@ from . import memberships from . import messages from ..avatars import avatars +from app.groups import groups from google.appengine.ext import ndb import os @@ -56,6 +57,14 @@ def delete(self): team.delete() self.key.delete() + @classmethod + def get_by_ident(cls, ident): + key = ndb.Key('Org', int(ident)) + project = key.get() + if project is None: + raise OrgDoesNotExistError('Org {} does not exist.'.format(ident)) + return project + @classmethod def get(cls, nickname): query = cls.query(cls.nickname == nickname) diff --git a/app/orgs/service_messages.py b/app/orgs/service_messages.py index f29b741..41da566 100644 --- a/app/orgs/service_messages.py +++ b/app/orgs/service_messages.py @@ -54,3 +54,8 @@ class SearchMembersResponse(messages.Message): class GroupResponse(messages.Message): group = messages.MessageField(group_messages.GroupMessage, 1) + + +class MembershipRequest(messages.Message): + org = messages.MessageField(org_messages.OrgMessage, 1) + membership = messages.MessageField(group_messages.MembershipMessage, 2) diff --git a/app/orgs/services.py b/app/orgs/services.py index 13db106..0839ac8 100644 --- a/app/orgs/services.py +++ b/app/orgs/services.py @@ -1,8 +1,9 @@ -from protorpc import remote from app import api +from app.groups import groups from app.orgs import orgs from app.orgs import service_messages from app.users import users +from protorpc import remote import logging @@ -80,3 +81,29 @@ def get_group(self, request): resp = service_messages.GroupResponse() resp.group = org.group.to_message() return resp + + @remote.method(service_messages.MembershipRequest, + service_messages.GroupResponse) + def update_membership(self, request): + org = self._get_org(request) +# self._get_policy(org).authorize_admin() + try: + org.group.update_membership(request.membership) + except groups.Error as e: + raise api.Error(str(e)) + resp = service_messages.GroupResponse() + resp.group = org.group.to_message() + return resp + + @remote.method(service_messages.MembershipRequest, + service_messages.GroupResponse) + def create_membership(self, request): + org = self._get_org(request) +# self._get_policy(org).authorize_admin() + try: + org.group.create_membership(request.membership) + except groups.Error as e: + raise api.Error(str(e)) + resp = service_messages.GroupResponse() + resp.group = org.group.to_message() + return resp diff --git a/app/policies/projects.py b/app/policies/projects.py index 69cb31a..bbb6c35 100644 --- a/app/policies/projects.py +++ b/app/policies/projects.py @@ -41,6 +41,9 @@ def can_administer(self): def can_read(self): if self.is_owner: return True + if self.project.owner.org: + if self.user.key == self.project.owner.org.owner_key: + return True if self.mem is None: return False return self.mem.role in [messages.Role.ADMIN, messages.Role.READ, None] diff --git a/webreview-fe b/webreview-fe index aeaedbf..d2b9303 160000 --- a/webreview-fe +++ b/webreview-fe @@ -1 +1 @@ -Subproject commit aeaedbfcfb4b9f8cfbed0341bdea0fb2638e9942 +Subproject commit d2b9303af60b48b70456b6e4f0c96b5402df9b2b From 1d7b2401b7f7ec844272a1e94c90ec6fc571dd35 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Sun, 29 May 2016 01:10:49 -0700 Subject: [PATCH 40/46] Add org owner projects to search results. --- app/projects/projects.py | 5 ++++- app/users/users.py | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/projects/projects.py b/app/projects/projects.py index 1faf4b2..d5d8d83 100644 --- a/app/projects/projects.py +++ b/app/projects/projects.py @@ -147,7 +147,10 @@ def search(cls, owner=None, order=None): elif order == messages.Order.NAME: query = query.order(-cls.nickname) if owner: - query = query.filter(cls.owner_key == owner.key) + if isinstance(owner, list): + query = query.filter(cls.owner_key.IN([o.key for o in owner])) + else: + query = query.filter(cls.owner_key == owner.key) return query.fetch() def delete(self): diff --git a/app/users/users.py b/app/users/users.py index fcacca9..b931723 100644 --- a/app/users/users.py +++ b/app/users/users.py @@ -229,6 +229,8 @@ def search_projects(self): team_projects = ndb.get_multi(list(set(project_keys))) from app.projects import projects user_projects = projects.Project.search(owner=self) + org_ents = self.search_orgs() + user_projects += projects.Project.search(owner=org_ents) results = filter(None, team_projects) for project in user_projects: if project not in results: From b71f4449aa18292b3bdbd6ad82864cec679f7f28 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Tue, 21 Feb 2017 10:35:49 -0800 Subject: [PATCH 41/46] Work on buildbot. --- Dockerfile | 3 --- Makefile | 17 +++++++++++++++++ app.yaml | 2 +- app/frontend/handlers.py | 24 ------------------------ app/main.py | 1 - app/orgs/services.py | 5 ++++- app/server/utils.py | 2 +- app/users/users.py | 3 ++- config/webreview.yaml | 16 ++++++++++++++++ 9 files changed, 41 insertions(+), 32 deletions(-) delete mode 100755 Dockerfile create mode 100644 Makefile create mode 100755 config/webreview.yaml diff --git a/Dockerfile b/Dockerfile deleted file mode 100755 index 3b7a588..0000000 --- a/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM gcr.io/google_appengine/python-compat -ADD . * -ADD . /app diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7bbf7a1 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +version ?= auto +project ?= betawebreview + +deploy: + cd webreview-fe && ember build && cd .. + gcloud app deploy \ + -q \ + --verbosity=error \ + --project=$(project) \ + --version=$(version) \ + app.yaml + gcloud app deploy \ + -q \ + --verbosity=error \ + --project=$(project) \ + --version=$(version) \ + index.yaml diff --git a/app.yaml b/app.yaml index 6f0428b..b11ec53 100644 --- a/app.yaml +++ b/app.yaml @@ -3,7 +3,7 @@ runtime: python27 threadsafe: true env_variables: - JETWAY_CONFIG: config/jetway.grow-prod.yaml + JETWAY_CONFIG: config/webreview.yaml GAE_USE_SOCKETS_HTTPLIB: 'anyvalue' # https://github.com/shazow/urllib3/issues/618#issuecomment-101795512 libraries: diff --git a/app/frontend/handlers.py b/app/frontend/handlers.py index 1c52ac9..934da6d 100644 --- a/app/frontend/handlers.py +++ b/app/frontend/handlers.py @@ -59,27 +59,3 @@ def post(self, letter, ident): avatar.update(gs_object_name) except avatars.AvatarDoesNotExistError: avatar = avatars.Avatar.create(letter, ident, gs_object_name) - - -class GitRedirectHandler(webapp2.RequestHandler): - - def dispatch(self): - args, kwargs = self.request.route_args, self.request.route_kwargs - if kwargs: - args = () - try: - return self.respond(*args, **kwargs) - except Exception, e: - return self.handle_exception(e, self.app.debug) - - def respond(self): - if os.getenv('SERVER_SOFTWARE').startswith('Dev'): - port = int(os.getenv('SERVER_PORT')) + 1000 - host = '{}:{}'.format(os.getenv('SERVER_NAME'), port) - else: - host = 'git.growlaunches.com' - scheme = os.getenv('wsgi.url_scheme') - url = '{}://{}{}'.format(scheme, host, self.request.path) - if self.request.query_string: - url += '?' + self.request.query_string - self.redirect(url, permanent=True) diff --git a/app/main.py b/app/main.py index 170b478..20d5ed1 100644 --- a/app/main.py +++ b/app/main.py @@ -22,7 +22,6 @@ # ('/_jetway/sheets/(.*)', sheets_handlers.SheetsHandler), ('/avatars/(u|o|p)/(.*)', frontend_handlers.AvatarHandler), ('/me/signout', auth_handlers.SignOutHandler), - ('/[^/]*/[^/]*.git.*', frontend_handlers.GitRedirectHandler), ('.*', frontend_handlers.FrontendHandler), ], config=config.WEBAPP2_AUTH_CONFIG) diff --git a/app/orgs/services.py b/app/orgs/services.py index 0839ac8..20b428f 100644 --- a/app/orgs/services.py +++ b/app/orgs/services.py @@ -27,7 +27,10 @@ def create(self, request): service_messages.GetOrgRequest, service_messages.GetOrgResponse) def get(self, request): - org = orgs.Org.get(request.org.nickname) + try: + org = orgs.Org.get(request.org.nickname) + except Exception as e: + raise remote.ApplicationError(str(e)) message = service_messages.GetOrgResponse() message.org = org.to_message() return message diff --git a/app/server/utils.py b/app/server/utils.py index d9b0ced..f2c6fcd 100644 --- a/app/server/utils.py +++ b/app/server/utils.py @@ -12,7 +12,7 @@ def is_avatar_request(hostname): def is_preview_server(hostname, path=None): return (hostname.endswith(appengine_config.PREVIEW_HOSTNAME) and hostname != appengine_config.PREVIEW_HOSTNAME - and not re.match('^avatars\d-dot-', hostname)) + and not re.match('^avatars-\w-dot-', hostname)) def parse_hostname(hostname, path=None, multitenant=False): diff --git a/app/users/users.py b/app/users/users.py index b931723..fe2eefa 100644 --- a/app/users/users.py +++ b/app/users/users.py @@ -230,7 +230,8 @@ def search_projects(self): from app.projects import projects user_projects = projects.Project.search(owner=self) org_ents = self.search_orgs() - user_projects += projects.Project.search(owner=org_ents) + if org_ents: + user_projects += projects.Project.search(owner=org_ents) results = filter(None, team_projects) for project in user_projects: if project not in results: diff --git a/config/webreview.yaml b/config/webreview.yaml new file mode 100755 index 0000000..38d8d2f --- /dev/null +++ b/config/webreview.yaml @@ -0,0 +1,16 @@ +options: + title: Web Review + +app: + client_secrets_file: "client_secret_696801287973-6usfr61a10jr8k7o16pebmqvmajgq3dd.apps.googleusercontent.com.json" + service_account_key_file: betawebreview-349110b1b659.json + api_key: "xxx" + +urls: + hostname: + prod: betawebreview.appspot.com + dev: webreview.dev.example.com + +require_https: + preview_domain: yes + app_domain: yes From 13e9dca5a6a77f6bb6b5ac0d9830b3836644d27c Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Sun, 19 Mar 2017 10:51:02 -0700 Subject: [PATCH 42/46] Work on ember migration. --- Makefile | 22 ++++++++++++---------- app.yaml | 9 +++++++-- app/main.py | 4 ++-- scripts/deploy | 12 ++++++++---- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index 7bbf7a1..3409ca3 100644 --- a/Makefile +++ b/Makefile @@ -4,14 +4,16 @@ project ?= betawebreview deploy: cd webreview-fe && ember build && cd .. gcloud app deploy \ - -q \ - --verbosity=error \ - --project=$(project) \ - --version=$(version) \ - app.yaml + -q \ + --verbosity=error \ + --project=$(project) \ + --version=$(version) \ + --no-promote \ + app.yaml gcloud app deploy \ - -q \ - --verbosity=error \ - --project=$(project) \ - --version=$(version) \ - index.yaml + -q \ + --no-promote \ + --verbosity=error \ + --project=$(project) \ + --version=$(version) \ + index.yaml diff --git a/app.yaml b/app.yaml index b11ec53..3ae2793 100644 --- a/app.yaml +++ b/app.yaml @@ -2,9 +2,11 @@ api_version: 1 runtime: python27 threadsafe: true +instance_class: F4_1G + env_variables: - JETWAY_CONFIG: config/webreview.yaml GAE_USE_SOCKETS_HTTPLIB: 'anyvalue' # https://github.com/shazow/urllib3/issues/618#issuecomment-101795512 + JETWAY_CONFIG: config/jetway.googwebreview.yaml libraries: - name: lxml @@ -52,6 +54,10 @@ skip_files: - ^(.*/)?index\.yaml - ^(.*/)?index\.yml - ^(.*/)?run_tests.py +- ^.*.example +- ^.*bower_components.* +- ^.*node_modules.* +- ^.*tmp.* - bower_components - env - htmlconv @@ -59,5 +65,4 @@ skip_files: - lib/PIL - node_modules - testing -- ^.*.example - webreview-fe diff --git a/app/main.py b/app/main.py index 20d5ed1..0a861b1 100644 --- a/app/main.py +++ b/app/main.py @@ -17,9 +17,9 @@ import endpoints import webapp2 +UNSECURED_SUFFIXES = ('.ttf', '.woff', 'sw.js', 'manifest.json') frontend_app = webapp2.WSGIApplication([ -# ('/_jetway/sheets/(.*)', sheets_handlers.SheetsHandler), ('/avatars/(u|o|p)/(.*)', frontend_handlers.AvatarHandler), ('/me/signout', auth_handlers.SignOutHandler), ('.*', frontend_handlers.FrontendHandler), @@ -78,7 +78,7 @@ def middleware(environ, start_response): # TODO: Remove dirty hack to get around issues with Chrome Beta and # Firefox. Note that this exposes all ttf and woff files. # http://stackoverflow.com/questions/31140826 - if environ['PATH_INFO'].endswith(('.ttf', '.woff')): + if environ['PATH_INFO'].endswith(UNSECURED_SUFFIXES): return app(environ, start_response) if utils.is_avatar_request(environ['SERVER_NAME']): return app(environ, start_response) diff --git a/scripts/deploy b/scripts/deploy index 70ad374..51911e7 100755 --- a/scripts/deploy +++ b/scripts/deploy @@ -6,15 +6,19 @@ cd webreview-fe && ember build && cd .. if [ -d env ]; then source env/bin/activate if [ $2 ]; then - gcloud preview app deploy \ + gcloud app deploy \ + -q \ + --no-promote \ --project=$1 \ --version=$2 \ - app.grow-prod.yaml + app.yaml elif [ $1 ]; then - gcloud preview app deploy \ + gcloud app deploy \ + -q \ + --no-promote \ --project=$1 \ --version=auto \ - app.grow-prod.yaml + app.yaml else echo "Usage: ./scripts/deploy " exit 1 From d6859f596d747959cbb61a987e1db5bbb467b0da Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Sun, 19 Mar 2017 11:00:16 -0700 Subject: [PATCH 43/46] Clean up dead code. --- app/filesets/services.py | 9 --------- app/main.py | 1 - app/sheets/__init__.py | 0 app/sheets/handlers.py | 11 ----------- app/sheets/sheets.py | 42 ---------------------------------------- 5 files changed, 63 deletions(-) delete mode 100644 app/sheets/__init__.py delete mode 100644 app/sheets/handlers.py delete mode 100644 app/sheets/sheets.py diff --git a/app/filesets/services.py b/app/filesets/services.py index 8ef043b..2e2fffc 100644 --- a/app/filesets/services.py +++ b/app/filesets/services.py @@ -135,15 +135,6 @@ def get(self, request): resp.fileset = fileset.to_message() return resp -# @remote.method(messages.FinalizeFilesetRequest, -# messages.FinalizeFilesetResponse) -# def finalize(self, request): -# fileset = self._get_fileset(request) -# fileset.update(request.fileset) -# resp = messages.FinalizeFilesetResponse() -# resp.fileset = fileset.to_message() -# return resp - @remote.method(messages.GetPageSpeedResultRequest, messages.GetPageSpeedResultResponse) def get_pagespeed_result(self, request): diff --git a/app/main.py b/app/main.py index 0a861b1..590c8ae 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,6 @@ from .comments import services as comment_services from .filesets import services as fileset_services from .frontend import handlers as frontend_handlers -#from .sheets import handlers as sheets_handlers from .launches import services as launch_services from .orgs import services as org_services from .owners import services as owner_services diff --git a/app/sheets/__init__.py b/app/sheets/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/sheets/handlers.py b/app/sheets/handlers.py deleted file mode 100644 index fb6ab65..0000000 --- a/app/sheets/handlers.py +++ /dev/null @@ -1,11 +0,0 @@ -#import airlock -from . import sheets - - -#class SheetsHandler(airlock.Handler): -class SheetsHandler(object): - - def get(self, sheet_id): - resp = sheets.get_sheet(sheet_id, gid=self.request.GET.get('gid')) - self.response.headers['Content-Type'] = 'text/csv' - self.response.out.write(resp) diff --git a/app/sheets/sheets.py b/app/sheets/sheets.py deleted file mode 100644 index eb52963..0000000 --- a/app/sheets/sheets.py +++ /dev/null @@ -1,42 +0,0 @@ -from googleapiclient import discovery -from oauth2client import client -#from oauth2client import keyring_storage -from oauth2client import tools -import appengine_config -import httplib2 -import logging - - -CLIENT_ID = appengine_config.client_secrets['web']['client_id'] -CLIENT_SECRET = appengine_config.client_secrets['web']['client_secret'] -OAUTH_SCOPE = 'https://www.googleapis.com/auth/drive' -REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob' - - -def get_sheet(sheet_id, gid=None, ext=''): - credentials = _get_credentials() - http = httplib2.Http() - http = credentials.authorize(http) - service = discovery.build('drive', 'v2', http=http) - resp = service.files().get(fileId=sheet_id).execute() - for mimetype, url in resp['exportLinks'].iteritems(): - if not mimetype.endswith(ext[1:]): - continue - if gid: - url += '&gid={}'.format(gid) - resp, content = service._http.request(url) - if resp.status != 200: - logging.error('Error downloading Google Sheet: {}'.format(sheet_id)) - break - return resp - - -def _get_credentials(self, username='default'): - storage = keyring_storage.Storage('Jetway', username) - credentials = storage.get() - if credentials is None: - parser = tools.argparser - flags, _ = parser.parse_known_args() - flow = client.OAuth2WebServerFlow(CLIENT_ID, CLIENT_SECRET, OAUTH_SCOPE) - credentials = tools.run_flow(flow, storage, flags) - return credentials From d1544d49914f0676179b62a23c74cb0a4b52e97e Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Sun, 19 Mar 2017 14:46:28 -0700 Subject: [PATCH 44/46] Add Makefile. Work on users/projects/groups. --- Makefile | 26 ++++++++++++++++++++++++++ app/filesets/filesets.py | 4 ++-- app/filesets/services.py | 8 ++++---- app/groups/groups.py | 20 ++++++++++++-------- app/orgs/orgs.py | 2 +- app/projects/projects.py | 2 +- app/users/users.py | 23 ++++++++++++++--------- requirements.txt | 4 +++- 8 files changed, 63 insertions(+), 26 deletions(-) diff --git a/Makefile b/Makefile index 3409ca3..70c1667 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,32 @@ version ?= auto project ?= betawebreview +install: + virtualenv env + . env/bin/activate + ./env/bin/pip install \ + --upgrade \ + --allow-unverified dateutil \ + --allow-external dateutil \ + -r \ + requirements.txt + ./env/bin/gaenv --lib lib --no-import + +test: + . env/bin/activate + ./env/bin/nosetests \ + -v \ + --rednose \ + --nocapture \ + --with-gae \ + --gae-lib-root=$(HOME)/google_appengine \ + --with-coverage \ + --cover-erase \ + --cover-html \ + --cover-html-dir=htmlcov \ + --cover-package=app \ + app/ + deploy: cd webreview-fe && ember build && cd .. gcloud app deploy \ diff --git a/app/filesets/filesets.py b/app/filesets/filesets.py index ebdc4b4..69257ac 100644 --- a/app/filesets/filesets.py +++ b/app/filesets/filesets.py @@ -239,8 +239,8 @@ def to_message(self): message.finalized = self.finalized message.status = self.status message.subdomain = self.subdomain - if self.created_by_key: - message.created_by = self.created_by.to_message() +# if self.created_by_key: +# message.created_by = self.created_by.to_message() if self.log: message.log = self.log.to_message() if self.stats: diff --git a/app/filesets/services.py b/app/filesets/services.py index 2e2fffc..7886907 100644 --- a/app/filesets/services.py +++ b/app/filesets/services.py @@ -163,13 +163,13 @@ def _get_or_create_fileset(self, request, me): allow_fileset_by_commit = bool(request.fileset.commit) p = self._get_project(request) try: - if allow_fileset_by_commit: - return filesets.Fileset.get(project=p, commit=request.fileset.commit) - elif request.fileset.ident: + if request.fileset.ident: return filesets.Fileset.get_by_ident(request.fileset.ident) - else: + elif request.fileset.name: p = self._get_project(request) return p.get_fileset(request.fileset.name) + elif allow_fileset_by_commit: + return filesets.Fileset.get(project=p, commit=request.fileset.commit) except filesets.FilesetDoesNotExistError: return filesets.Fileset.create( p, name=request.fileset.name, commit=request.fileset.commit, created_by=me) diff --git a/app/groups/groups.py b/app/groups/groups.py index 70c8074..f59092b 100644 --- a/app/groups/groups.py +++ b/app/groups/groups.py @@ -9,8 +9,8 @@ class Group(ndb.Model): memberships = ndb.StructuredProperty(memberships.Membership, repeated=True) - project_key = ndb.KeyProperty() - org_key = ndb.KeyProperty() + project_keys = ndb.KeyProperty(repeated=True) + org_keys = ndb.KeyProperty(repeated=True) @property def ident(self): @@ -28,9 +28,13 @@ def get(cls, ident): def create(cls, project=None, org=None): group = cls() if project: - group.project_key = project.key + if group.project_keys: + group.project_keys.append(project.key) + group.project_keys = [project.key] if org: - group.org_key = org.key + if group.org_keys: + group.org_keys.append(org.key) + group.org_keys = [org.key] group.put() return group @@ -89,10 +93,10 @@ def list_memberships(self, kind=None): def to_message(self): message = messages.GroupMessage() message.ident = self.ident - if hasattr(self, 'project') and self.project: - message.project = self.project.to_message() - if hasattr(self, 'org') and self.org: - message.org = self.org.to_message() +# if hasattr(self, 'project') and self.project: +# message.project = self.project.to_message() +# if hasattr(self, 'org') and self.org: +# message.org = self.org.to_message() message.users = [mem.to_message() for mem in self.list_memberships(messages.Kind.USER)] message.domains = [mem.to_message() diff --git a/app/orgs/orgs.py b/app/orgs/orgs.py index d7a1f24..cf17a2d 100644 --- a/app/orgs/orgs.py +++ b/app/orgs/orgs.py @@ -121,7 +121,7 @@ def search(cls, owner=None): @property def group(self): def _create_group(): - group = groups.Group.create() + group = groups.Group.create(org=self) self.group_key = group.key self.put() group.org = self diff --git a/app/projects/projects.py b/app/projects/projects.py index d5d8d83..e3f3ac0 100644 --- a/app/projects/projects.py +++ b/app/projects/projects.py @@ -190,7 +190,7 @@ def owner(self): @property def group(self): def _create_group(): - group = groups.Group.create() + group = groups.Group.create(project=self) self.group_key = group.key self.put() group.project = self diff --git a/app/users/users.py b/app/users/users.py index fe2eefa..9983336 100644 --- a/app/users/users.py +++ b/app/users/users.py @@ -204,35 +204,40 @@ def update(self, message): def search_teams(self): from app.groups import groups query = groups.Group.query() - query = query.filter(groups.Group.project_key != None) + query = query.filter(groups.Group.project_keys != None) query = query.filter(groups.Group.memberships.user_key == self.key) return query.fetch() def search_orgs(self): from app.groups import groups query = groups.Group.query() - query = query.filter(groups.Group.org_key != None) + query = query.filter(groups.Group.org_keys != None) query = query.filter(groups.Group.memberships.user_key == self.key) results = query.fetch() - org_keys = [result.org_key for result in results] + org_keys = [] + for result in results: + for org_key in result.org_keys: + org_keys.append(org_key) from app.orgs import orgs org_ents = orgs.Org.search(owner=self) or [] org_ents += ndb.get_multi(list(set(org_keys))) + org_ents = sorted(org_ents, key=lambda org: org.nickname) return org_ents def search_projects(self): - team_ents = self.search_teams() + group_ents = self.search_teams() project_keys = [] - for team in team_ents: - if team.project_key: - project_keys.append(team.project_key) - team_projects = ndb.get_multi(list(set(project_keys))) + for group in group_ents: + if group.project_key: + for project_key in group.project_keys: + project_keys.append(project_key) + group_projects = ndb.get_multi(list(set(project_keys))) from app.projects import projects user_projects = projects.Project.search(owner=self) org_ents = self.search_orgs() if org_ents: user_projects += projects.Project.search(owner=org_ents) - results = filter(None, team_projects) + results = filter(None, group_projects) for project in user_projects: if project not in results: results.append(project) diff --git a/requirements.txt b/requirements.txt index e049942..612aa47 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,8 +26,10 @@ uritemplate docker-py # Testing. -NoseGAE==0.5.6 +NoseGAE==0.5.10 nose==1.3.1 rednose==0.4.1 coverage==3.7.1 gaenv==0.1.7 +mock==1.0.1 +WebTest==2.0.17 From 8171cae9d45bc5d4dd0028084156c68ce9f2ef49 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Sun, 19 Mar 2017 14:57:33 -0700 Subject: [PATCH 45/46] Update indexes. --- index.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/index.yaml b/index.yaml index 6b040aa..7530137 100644 --- a/index.yaml +++ b/index.yaml @@ -44,6 +44,16 @@ indexes: - name: modified direction: desc +- kind: Group + properties: + - name: memberships.user_key + - name: org_keys + +- kind: Group + properties: + - name: memberships.user_key + - name: project_keys + - kind: Group properties: - name: memberships.user_key From b4a8e83a1dca6b962cece64480862d3b084d07c1 Mon Sep 17 00:00:00 2001 From: Jeremy Weinstein Date: Sun, 19 Mar 2017 22:18:29 -0700 Subject: [PATCH 46/46] Work on groups. --- Makefile | 10 ++++++++++ app.yaml | 2 +- app/filesets/filesets.py | 2 +- app/owners/owners.py | 2 ++ app/projects/messages.py | 4 ++-- app/projects/service_messages.py | 5 +++++ app/projects/services.py | 12 ++++++++---- index.yaml | 8 ++++---- 8 files changed, 33 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 70c1667..c81ca12 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,15 @@ version ?= auto project ?= betawebreview +run-frontend: + cd webreview-fe && ember serve + +run-backend: + virtualenv env + . env/bin/activate + dev_appserver.py \ + --port=8088 . + install: virtualenv env . env/bin/activate @@ -28,6 +37,7 @@ test: app/ deploy: + $(MAKE) test cd webreview-fe && ember build && cd .. gcloud app deploy \ -q \ diff --git a/app.yaml b/app.yaml index 3ae2793..d65889f 100644 --- a/app.yaml +++ b/app.yaml @@ -36,7 +36,7 @@ handlers: - url: /_ah/spi/.* script: app.main.endpoints_app -- url: /assets +- url: /_webreview/assets static_dir: dist/assets - url: /.* diff --git a/app/filesets/filesets.py b/app/filesets/filesets.py index 69257ac..fb078f9 100644 --- a/app/filesets/filesets.py +++ b/app/filesets/filesets.py @@ -217,7 +217,7 @@ def delete(self): def finalize(self): self.finalized = True self.status = messages.FilesetStatus.SUCCESS - fileset_utils.send_finalized_email(self) +# fileset_utils.send_finalized_email(self) self.put() def update(self, message): diff --git a/app/owners/owners.py b/app/owners/owners.py index e4aec8d..d3808f5 100644 --- a/app/owners/owners.py +++ b/app/owners/owners.py @@ -105,4 +105,6 @@ def to_message(self): message.created = self._entity.created message.website_url = self._entity.website_url message.avatar_url = self.avatar_url + if self.kind == messages.OwnerMessage.Kind.USER: + message.email = self._entity.email return message diff --git a/app/projects/messages.py b/app/projects/messages.py index e3c42c6..331e973 100644 --- a/app/projects/messages.py +++ b/app/projects/messages.py @@ -1,6 +1,6 @@ -from protorpc import messages -from protorpc import message_types from app.owners import messages as owner_messages +from protorpc import message_types +from protorpc import messages class Permission(messages.Enum): diff --git a/app/projects/service_messages.py b/app/projects/service_messages.py index 36af4e5..cf1df7f 100644 --- a/app/projects/service_messages.py +++ b/app/projects/service_messages.py @@ -136,3 +136,8 @@ class GroupResponse(messages.Message): class MembershipRequest(messages.Message): project = messages.MessageField(ProjectMessage, 1) membership = messages.MessageField(group_messages.MembershipMessage, 2) + + +class GroupRequest(messages.Message): + project = messages.MessageField(project_messages.ProjectMessage, 1) + group = messages.MessageField(group_messages.GroupMessage, 2) diff --git a/app/projects/services.py b/app/projects/services.py index da0d1c8..9bbaf38 100644 --- a/app/projects/services.py +++ b/app/projects/services.py @@ -219,13 +219,17 @@ def update_translations(self, request): resp.catalog = catalog.to_message() return resp - @remote.method(service_messages.ProjectRequest, + @remote.method(service_messages.GroupRequest, service_messages.GroupResponse) def get_group(self, request): - project = self._get_project(request) - self._get_policy(project).authorize_read() + if request.group: + group = groups.Group.get(request.group.ident) + elif request.project: + project = self._get_project(request) + self._get_policy(project).authorize_read() + group = project.group resp = service_messages.GroupResponse() - resp.group = project.group.to_message() + resp.group = group.to_message() return resp @remote.method(service_messages.MembershipRequest, diff --git a/index.yaml b/index.yaml index 7530137..a326039 100644 --- a/index.yaml +++ b/index.yaml @@ -47,22 +47,22 @@ indexes: - kind: Group properties: - name: memberships.user_key - - name: org_keys + - name: org_key - kind: Group properties: - name: memberships.user_key - - name: project_keys + - name: org_keys - kind: Group properties: - name: memberships.user_key - - name: org_key + - name: project_key - kind: Group properties: - name: memberships.user_key - - name: project_key + - name: project_keys - kind: Project properties: