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/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..c81ca12 --- /dev/null +++ b/Makefile @@ -0,0 +1,55 @@ +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 + ./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: + $(MAKE) test + cd webreview-fe && ember build && cd .. + gcloud app deploy \ + -q \ + --verbosity=error \ + --project=$(project) \ + --version=$(version) \ + --no-promote \ + app.yaml + gcloud app deploy \ + -q \ + --no-promote \ + --verbosity=error \ + --project=$(project) \ + --version=$(version) \ + index.yaml 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..d65889f 100644 --- a/app.yaml +++ b/app.yaml @@ -2,7 +2,15 @@ api_version: 1 runtime: python27 threadsafe: true +instance_class: F4_1G + +env_variables: + GAE_USE_SOCKETS_HTTPLIB: 'anyvalue' # https://github.com/shazow/urllib3/issues/618#issuecomment-101795512 + JETWAY_CONFIG: config/jetway.googwebreview.yaml + libraries: +- name: lxml + version: latest - name: endpoints version: latest - name: ssl @@ -26,22 +34,13 @@ handlers: secure: always - url: /_ah/spi/.* - script: jetway.main.endpoints_app - -- url: /_jetway/[^/]*/static/css - static_dir: dist/css + script: app.main.endpoints_app -- url: /_jetway/[^/]*/static/js - static_dir: dist/js - -- url: /_jetway/[^/]*/static/html - static_dir: jetway/frontend/static/html - -- url: /_jetway/[^/]*/static/images - static_dir: jetway/frontend/static/images +- url: /_webreview/assets + static_dir: dist/assets - url: /.* - script: jetway.main.app + script: app.main.app skip_files: - ^(.*/)?#.*# @@ -55,10 +54,15 @@ skip_files: - ^(.*/)?index\.yaml - ^(.*/)?index\.yml - ^(.*/)?run_tests.py +- ^.*.example +- ^.*bower_components.* +- ^.*node_modules.* +- ^.*tmp.* - bower_components - env - htmlconv - lib/Crypto +- lib/PIL - node_modules - testing -- ^.*.example +- webreview-fe 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 92% rename from jetway/api_errors.py rename to app/api_errors.py index 3c94c25..2f40b7b 100644 --- a/jetway/api_errors.py +++ b/app/api_errors.py @@ -2,7 +2,7 @@ import httplib -class Error(Exception): +class Error(remote.ApplicationError): pass 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 89% rename from jetway/avatars/avatars.py rename to app/avatars/avatars.py index 47613f3..91f0565 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 @@ -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,9 +81,11 @@ 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) + if '-dot-' in hostname: + return '//{}{}'.format(hostname, path) + return '//avatars-{}{}{}{}'.format(num, sep, hostname, path) @classmethod def generate(cls, ident): 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/builds/__init__.py b/app/buildbot/__init__.py similarity index 100% rename from jetway/builds/__init__.py rename to app/buildbot/__init__.py diff --git a/app/buildbot/buildbot.py b/app/buildbot/buildbot.py new file mode 100644 index 0000000..740d7b0 --- /dev/null +++ b/app/buildbot/buildbot.py @@ -0,0 +1,110 @@ +from requests import auth +import appengine_config +import requests + +BASE = '{}/api'.format(appengine_config.BUILDBOT_URL) + +VERIFY = False + + +class Error(Exception): + pass + + +class ConnectionError(Error): + pass + + +class IntegrationError(Error): + 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, + verify=VERIFY) + resp.raise_for_status() + except Exception as e: + raise ConnectionError(e) + content = resp.json() + if 'error' in content: + raise IntegrationError(content['error']) + return content + + def get_job(self, job_id): + try: + resp = requests.get(BASE + '/jobs/{}'.format(job_id), auth=self.auth, + verify=VERIFY) + resp.raise_for_status() + except Exception as 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): + path = path or '/' + try: + request_path = BASE + '/git/repos/{}/contents{}'.format(job_id, path) + resp = requests.get(request_path, auth=self.auth, verify=VERIFY) + resp.raise_for_status() + except Exception as e: + 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, 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): + 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, + auth=self.auth, + verify=VERIFY) + resp.raise_for_status() + except Exception as e: + raise ConnectionError(e) + result = resp.json() + if 'error' in result: + raise IntegrationError(result['error']) + return result diff --git a/app/buildbot/messages.py b/app/buildbot/messages.py new file mode 100644 index 0000000..00f4e5e --- /dev/null +++ b/app/buildbot/messages.py @@ -0,0 +1,11 @@ +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) + ident = messages.StringField(3) diff --git a/app/catalogs/__init__.py b/app/catalogs/__init__.py new file mode 100644 index 0000000..20ea213 --- /dev/null +++ b/app/catalogs/__init__.py @@ -0,0 +1 @@ +from . import babel_compatibility_patch diff --git a/app/catalogs/babel_compatibility_patch.py b/app/catalogs/babel_compatibility_patch.py new file mode 100644 index 0000000..315739f --- /dev/null +++ b/app/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/app/catalogs/catalogs.py b/app/catalogs/catalogs.py new file mode 100644 index 0000000..cd1ec25 --- /dev/null +++ b/app/catalogs/catalogs.py @@ -0,0 +1,116 @@ +from . import messages +from ..buildbot import buildbot +from ..translations import translations +from babel.messages import catalog +from babel.messages import pofile +import babel +import cStringIO +import io +import webapp2 + + +class Error(Exception): + pass + + +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 + self.sha = None + self.content = None + self.load() + + @property + def path(self): + return '/translations/{}/LC_MESSAGES/messages.po'.format(self.locale) + + @property + 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'] + + @webapp2.cached_property + def babel_catalog(self): + fp = io.BytesIO() + fp.write(self.content) + 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 list(self.babel_catalog)[1:]: + translation = translations.Translation( + catalog=self, + msgid=message.id, + string=message.string) + translation_objs.append(translation) + return translation_objs + + 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.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, + commit_message=None): + label = '(webreview) 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 + 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.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, + author=author) diff --git a/app/catalogs/messages.py b/app/catalogs/messages.py new file mode 100644 index 0000000..2fd8472 --- /dev/null +++ b/app/catalogs/messages.py @@ -0,0 +1,18 @@ +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) + 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) + ident = messages.StringField(9) + sha = messages.StringField(10) + ref = messages.StringField(11) 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/files/__init__.py b/app/domains/__init__.py similarity index 100% rename from jetway/files/__init__.py rename to app/domains/__init__.py diff --git a/app/domains/domains.py b/app/domains/domains.py new file mode 100644 index 0000000..c3acb34 --- /dev/null +++ b/app/domains/domains.py @@ -0,0 +1,6 @@ +from app import models +from google.appengine.ext import ndb + + +class Domain(models.Model): + name = ndb.StringProperty() diff --git a/jetway/filesets/__init__.py b/app/files/__init__.py similarity index 100% rename from jetway/filesets/__init__.py rename to app/files/__init__.py diff --git a/jetway/files/files.py b/app/files/files.py similarity index 95% rename from jetway/files/files.py rename to app/files/files.py index d0f1da3..dfa2b65 100644 --- a/jetway/files/files.py +++ b/app/files/files.py @@ -2,8 +2,9 @@ import cloudstorage from datetime import datetime from google.appengine.ext import blobstore -from jetway.files import messages -from jetway.utils import gcs +from google.appengine.ext import ndb +from app.files import messages +from app.utils import gcs import os import time @@ -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/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/frontend/__init__.py b/app/filesets/__init__.py similarity index 100% rename from jetway/frontend/__init__.py rename to app/filesets/__init__.py diff --git a/jetway/filesets/filesets.py b/app/filesets/filesets.py similarity index 94% rename from jetway/filesets/filesets.py rename to app/filesets/filesets.py index aaa2f89..fb078f9 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 @@ -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 @@ -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): @@ -238,8 +238,9 @@ def to_message(self): message.commit = self.commit message.finalized = self.finalized message.status = self.status - if self.created_by_key: - message.created_by = self.created_by.to_message() + message.subdomain = self.subdomain +# 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: @@ -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/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 95% rename from jetway/filesets/messages.py rename to app/filesets/messages.py index 134c4f9..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 @@ -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/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 85% rename from jetway/filesets/services.py rename to app/filesets/services.py index 8105bb3..7886907 100644 --- a/jetway/filesets/services.py +++ b/app/filesets/services.py @@ -1,9 +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.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 @@ -110,12 +111,16 @@ 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 + 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() @@ -130,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): @@ -161,20 +157,19 @@ 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 = (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: - 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) @@ -193,10 +188,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) + project = self._get_project(request) + policy = policies.ProjectPolicy(me, project) if (not self._is_authorized_buildbot() - and not fileset.project.can(me, projects.Permission.WRITE)): - raise api.ForbiddenError('Forbidden.') + and not policy.can_write()): + 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/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/launches/__init__.py b/app/frontend/__init__.py similarity index 100% rename from jetway/launches/__init__.py rename to app/frontend/__init__.py diff --git a/jetway/frontend/handlers.py b/app/frontend/handlers.py similarity index 62% rename from jetway/frontend/handlers.py rename to app/frontend/handlers.py index 8a550b1..934da6d 100644 --- a/jetway/frontend/handlers.py +++ b/app/frontend/handlers.py @@ -1,13 +1,15 @@ 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 import webapp2 -_path = os.path.join(os.path.dirname(__file__), 'templates') +_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), '..', '..', 'dist')) _loader = jinja2.FileSystemLoader(_path) _env = jinja2.Environment(loader=_loader, autoescape=True, trim_blocks=True) @@ -57,31 +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 MeHandler(BaseHandler): - pass - - -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/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/logs/__init__.py b/app/groups/__init__.py similarity index 100% rename from jetway/logs/__init__.py rename to app/groups/__init__.py diff --git a/app/groups/groups.py b/app/groups/groups.py new file mode 100644 index 0000000..f59092b --- /dev/null +++ b/app/groups/groups.py @@ -0,0 +1,112 @@ +from . import messages +from . import memberships +from google.appengine.ext import ndb +from google.appengine.ext.ndb import msgprop + + +Error = memberships.Error + + +class Group(ndb.Model): + memberships = ndb.StructuredProperty(memberships.Membership, repeated=True) + project_keys = ndb.KeyProperty(repeated=True) + org_keys = ndb.KeyProperty(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 + + @classmethod + def create(cls, project=None, org=None): + group = cls() + if project: + if group.project_keys: + group.project_keys.append(project.key) + group.project_keys = [project.key] + if org: + if group.org_keys: + group.org_keys.append(org.key) + group.org_keys = [org.key] + group.put() + 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: + pass + # 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) + 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 +# 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() + for mem in self.list_memberships(messages.Kind.DOMAIN)] + return message + + def get_membership(self, user): + mems = self.list_memberships() + for mem in mems: + if mem.user_key == user.key: + return mem + if mem.domain == user.domain: + return mem diff --git a/app/groups/memberships.py b/app/groups/memberships.py new file mode 100644 index 0000000..eb8337f --- /dev/null +++ b/app/groups/memberships.py @@ -0,0 +1,64 @@ +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: + text = '{} is already a member.' + raise MembershipConflictError(text.format(self.user.nickname)) + if self.domain and self.domain == mem.domain: + text = '{} is already a member.' + raise MembershipConflictError(text.format(self.domain)) + + @classmethod + def from_message(cls, message): + mem = cls() + 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) + elif message.user.ident: + user = users.User.get_by_ident(message.user.email) + else: + raise ValueError('User not found.') + self.user_key = user.key + if message.domain: + self.domain = message.domain + if not self.role: + self.role = messages.Role.READ + return self + + @webapp2.cached_property + def user(self): + if self.user_key: + return self.user_key.get() + + def to_message(self): + message = messages.MembershipMessage() + message.role = self.role + if self.user_key: + message.user = self.user.to_message() + if self.domain: + message.domain = self.domain + return message diff --git a/app/groups/messages.py b/app/groups/messages.py new file mode 100644 index 0000000..0df2a7c --- /dev/null +++ b/app/groups/messages.py @@ -0,0 +1,30 @@ +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 + + +class Role(messages.Enum): + ADMIN = 1 + READ = 2 + WRITE = 3 + TRANSLATE = 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): + 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) + org = messages.MessageField(org_messages.OrgMessage, 5) diff --git a/jetway/memberships/__init__.py b/app/launches/__init__.py similarity index 100% rename from jetway/memberships/__init__.py rename to app/launches/__init__.py diff --git a/jetway/launches/launches.py b/app/launches/launches.py similarity index 87% rename from jetway/launches/launches.py rename to app/launches/launches.py index 2482a57..6ee8d63 100644 --- a/jetway/launches/launches.py +++ b/app/launches/launches.py @@ -1,8 +1,7 @@ from google.appengine.ext import ndb -from jetway.filesets import filesets -from jetway.projects import projects -from jetway.launches import messages -from jetway.teams import teams +from app.filesets import filesets +from app.projects import projects +from app.launches import messages class Error(Exception): @@ -122,24 +121,13 @@ 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 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/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/orgs/__init__.py b/app/logs/__init__.py similarity index 100% rename from jetway/orgs/__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 87% rename from jetway/main.py rename to app/main.py index e6ef797..590c8ae 100644 --- a/jetway/main.py +++ b/app/main.py @@ -5,25 +5,22 @@ 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 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 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), - ('/[^/]*/[^/]*.git.*', frontend_handlers.GitRedirectHandler), ('.*', frontend_handlers.FrontendHandler), ], config=config.WEBAPP2_AUTH_CONFIG) @@ -56,7 +53,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') @@ -78,6 +74,13 @@ 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(UNSECURED_SUFFIXES): + 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']) @@ -85,16 +88,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/app/main_test.py similarity index 80% rename from jetway/main_test.py rename to app/main_test.py index 717a219..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 @@ -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/app/models/models.py b/app/models/models.py new file mode 100644 index 0000000..04892f0 --- /dev/null +++ b/app/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/owners/__init__.py b/app/orgs/__init__.py similarity index 100% rename from jetway/owners/__init__.py rename to app/orgs/__init__.py diff --git a/jetway/memberships/memberships.py b/app/orgs/memberships.py similarity index 100% rename from jetway/memberships/memberships.py rename to app/orgs/memberships.py diff --git a/jetway/orgs/messages.py b/app/orgs/messages.py similarity index 79% rename from jetway/orgs/messages.py rename to app/orgs/messages.py index 5a64c2c..844addf 100644 --- a/jetway/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/jetway/orgs/orgs.py b/app/orgs/orgs.py similarity index 59% rename from jetway/orgs/orgs.py rename to app/orgs/orgs.py index c832d4b..cf17a2d 100644 --- a/jetway/orgs/orgs.py +++ b/app/orgs/orgs.py @@ -1,8 +1,8 @@ +from . import memberships +from . import messages +from ..avatars import avatars +from app.groups import groups 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 @@ -31,6 +31,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) @@ -43,22 +45,26 @@ 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() - 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 + from app.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() + @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) @@ -78,6 +84,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: @@ -94,39 +107,33 @@ 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) + @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(org=self) + 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 @@ -136,4 +143,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/jetway/orgs/service_messages.py b/app/orgs/service_messages.py similarity index 73% rename from jetway/orgs/service_messages.py rename to app/orgs/service_messages.py index 7c237c6..41da566 100644 --- a/jetway/orgs/service_messages.py +++ b/app/orgs/service_messages.py @@ -1,6 +1,7 @@ +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 -from jetway.orgs import messages as org_messages -from jetway.users import messages as user_messages class CreateOrgRequest(messages.Message): @@ -49,3 +50,12 @@ 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) + + +class MembershipRequest(messages.Message): + org = messages.MessageField(org_messages.OrgMessage, 1) + membership = messages.MessageField(group_messages.MembershipMessage, 2) diff --git a/jetway/orgs/services.py b/app/orgs/services.py similarity index 59% rename from jetway/orgs/services.py rename to app/orgs/services.py index e829c3f..20b428f 100644 --- a/jetway/orgs/services.py +++ b/app/orgs/services.py @@ -1,8 +1,9 @@ +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 -from jetway import api -from jetway.orgs import orgs -from jetway.orgs import service_messages -from jetway.users import users import logging @@ -26,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 @@ -71,3 +75,38 @@ 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 + + @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/jetway/projects/__init__.py b/app/owners/__init__.py similarity index 100% rename from jetway/projects/__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 81% rename from jetway/owners/owners.py rename to app/owners/owners.py index a32845e..d3808f5 100644 --- a/jetway/owners/owners.py +++ b/app/owners/owners.py @@ -1,10 +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.teams import teams -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): @@ -106,17 +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 - - 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/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 90% rename from jetway/owners/services.py rename to app/owners/services.py index ec4f3f2..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): @@ -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/server/__init__.py b/app/policies/__init__.py similarity index 100% rename from jetway/server/__init__.py rename to app/policies/__init__.py diff --git a/app/policies/policies.py b/app/policies/policies.py new file mode 100644 index 0000000..2e036f7 --- /dev/null +++ b/app/policies/policies.py @@ -0,0 +1 @@ +from .projects import * diff --git a/app/policies/projects.py b/app/policies/projects.py new file mode 100644 index 0000000..bbb6c35 --- /dev/null +++ b/app/policies/projects.py @@ -0,0 +1,56 @@ +from ..groups import groups +from ..groups import messages +from app import api_errors + + +class Error(Exception): + pass + + +class ForbiddenError(Error, api_errors.ForbiddenError): + pass + + +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): + 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] + + def can_write(self): + if self.is_owner: + return True + if self.mem is None: + return False + return self.mem.role in [messages.Role.ADMIN, messages.Role.WRITE] diff --git a/jetway/sheets/__init__.py b/app/projects/__init__.py similarity index 100% rename from jetway/sheets/__init__.py rename to app/projects/__init__.py diff --git a/app/projects/messages.py b/app/projects/messages.py new file mode 100644 index 0000000..331e973 --- /dev/null +++ b/app/projects/messages.py @@ -0,0 +1,90 @@ +from app.owners import messages as owner_messages +from protorpc import message_types +from protorpc import messages + + +class Permission(messages.Enum): + READ = 1 + WRITE = 2 + ADMINISTER = 3 + + +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) + owner = messages.MessageField(owner_messages.OwnerMessage, 3) + description = messages.StringField(4) + avatar_url = messages.StringField(6) + 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) + buildbot_git_status = messages.EnumField(BuildbotGitStatus, 14) + + +### + + +class CreateProjectRequest(messages.Message): + project = messages.MessageField(ProjectMessage, 1) + + +class CreateProjectResponse(messages.Message): + project = messages.MessageField(ProjectMessage, 1) + + +class DeleteProjectRequest(messages.Message): + project = messages.MessageField(ProjectMessage, 1) + + +class DeleteProjectResponse(messages.Message): + pass + + +class UpdateProjectRequest(messages.Message): + project = messages.MessageField(ProjectMessage, 1) + + +class UpdateProjectResponse(messages.Message): + project = messages.MessageField(ProjectMessage, 1) + + +class SearchProjectRequest(messages.Message): + project = messages.MessageField(ProjectMessage, 1) + + +class SearchProjectResponse(messages.Message): + projects = messages.MessageField(ProjectMessage, 1, repeated=True) + + +class GetProjectRequest(messages.Message): + project = messages.MessageField(ProjectMessage, 1) + + +class GetProjectResponse(messages.Message): + project = messages.MessageField(ProjectMessage, 1) + + +class DeleteProjectRequest(messages.Message): + project = messages.MessageField(ProjectMessage, 1) + + +class DeleteProjectResponse(messages.Message): + pass + + +class TransferOwnerRequest(messages.Message): + project = messages.MessageField(ProjectMessage, 1) + owner = messages.MessageField(owner_messages.OwnerMessage, 2) diff --git a/jetway/projects/projects.py b/app/projects/projects.py similarity index 53% rename from jetway/projects/projects.py rename to app/projects/projects.py index 7369acc..e3f3ac0 100644 --- a/jetway/projects/projects.py +++ b/app/projects/projects.py @@ -1,13 +1,21 @@ from . import watchers +from ..buildbot import buildbot +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 -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 jetway.teams import teams +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 +import logging import os @@ -26,17 +34,8 @@ 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 GitIntegrationError(Error): + pass class Project(ndb.Model): @@ -45,15 +44,21 @@ class Project(ndb.Model): owner_key = ndb.KeyProperty() created_by_key = ndb.KeyProperty() description = ndb.StringProperty() - cover = ndb.StructuredProperty(Cover) - visibility = msgprop.EnumProperty(messages.Visibility, - default=messages.Visibility.PRIVATE) built = ndb.DateTimeProperty() + buildbot_job_id = ndb.StringProperty() + git_url = ndb.StringProperty() + group_key = ndb.KeyProperty() + translation_branch = ndb.StringProperty() + buildbot_git_status = msgprop.EnumProperty(messages.BuildbotGitStatus) @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) @@ -66,7 +71,7 @@ def ident(self): return str(self.key.id()) @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.' @@ -77,11 +82,11 @@ 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() - teams.Team.create(owner, None, - created_by=created_by, project=project, - kind=teams.messages.Kind.PROJECT_OWNERS) + project._init_default_group() + project._update_buildbot_job(project.git_url) return project @classmethod @@ -104,6 +109,36 @@ def get(cls, owner=None, nickname=None): raise ProjectDoesNotExistError(text.format(nickname)) 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() + return + logging.info('Buildbot URL update {} -> {}'.format(self, git_url)) + bot = buildbot.Buildbot() + try: + resp = bot.create_job( + git_url=git_url, + remote=self.permalink) + self.buildbot_job_id = str(resp['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.') + + 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() @@ -112,24 +147,19 @@ 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): - from jetway.launches import launches - team_results = teams.Team.search(projects=[self], kind=teams.messages.Kind.DEFAULT) + from app.launches import launches 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: @@ -145,9 +175,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) @@ -161,13 +188,20 @@ 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) + def group(self): + def _create_group(): + group = groups.Group.create(project=self) + 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 def avatar_url(self): @@ -184,33 +218,31 @@ 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: - message.cover = self.cover.to_message() + 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 - if message.cover: - self.cover = Cover.from_message(message.cover) - self.visibility = message.visibility + self.translation_branch = message.translation_branch + self._update_buildbot_job(message.git_url) + self.git_url = message.git_url self.put() - def search_teams(self, users=None): - return teams.Team.search(projects=[self], users=None) + 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) 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 @@ -218,54 +250,11 @@ def search_users(self, is_public=True): def filter(cls, results, user, permission=messages.Permission.READ): filtered = [] for project in results: - if project.can(user, permission): + policy = policies.ProjectPolicy(user, project) + 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 - 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), - # 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() - if self.visibility == messages.Visibility.ORGANIZATION: - return bool(found_teams) - if self.visibility == messages.Visibility.PRIVATE: - # 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.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) @@ -293,3 +282,42 @@ 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 = [] + 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, ref=None): + self.verify_repo_status() + bot = buildbot.Buildbot() + items = bot.get_contents( + self.buildbot_job_id, + path='/translations/', + ref=ref) + catalog_objs = [] + for item in items: + if item['type'] == 'dir': + catalog = self.get_catalog(locale=item['name']) + catalog_objs.append(catalog) + return catalog_objs + + def get_catalog(self, locale, ref=None): + self.verify_repo_status() + return catalogs.Catalog(project=self, locale=locale, ref=ref) diff --git a/jetway/projects/projects_test.py b/app/projects/projects_test.py similarity index 60% rename from jetway/projects/projects_test.py rename to app/projects/projects_test.py index 8ccd096..9fde5c4 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 @@ -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/jetway/projects/service_messages.py b/app/projects/service_messages.py similarity index 65% rename from jetway/projects/service_messages.py rename to app/projects/service_messages.py index 69a17f4..cf1df7f 100644 --- a/jetway/projects/service_messages.py +++ b/app/projects/service_messages.py @@ -1,4 +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 @@ -98,3 +101,43 @@ 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) + + +class ProjectRequest(messages.Message): + project = messages.MessageField(ProjectMessage, 1) + branch = messages.StringField(2) + + +class ListCatalogsResponse(messages.Message): + catalogs = messages.MessageField(catalog_messages.CatalogMessage, 1, repeated=True) + + +class CatalogRequest(messages.Message): + project = messages.MessageField(ProjectMessage, 1) + catalog = messages.MessageField(catalog_messages.CatalogMessage, 2) + + +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) + + +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 new file mode 100644 index 0000000..9bbaf38 --- /dev/null +++ b/app/projects/services.py @@ -0,0 +1,283 @@ +from . import service_messages +from ..buildbot import buildbot +from ..policies import policies +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 + + +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: + return projects.Project.get_by_ident(request.project.ident) + owner = owners.Owner.get(request.project.owner.nickname) + return projects.Project.get(owner, request.project.nickname) + except (owners.OwnerDoesNotExistError, + projects.ProjectDoesNotExistError) as e: + raise api.NotFoundError(str(e)) + + @remote.method(service_messages.CreateProjectRequest, + service_messages.CreateProjectResponse) + def create(self, request): + try: + 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)) + resp = service_messages.CreateProjectResponse() + resp.project = project.to_message() + return resp + + @remote.method(service_messages.SearchProjectRequest, + service_messages.SearchProjectResponse) + def search(self, request): + if request.project and request.project.owner: + owner = owners.Owner.get(request.project.owner.nickname) + else: + owner = None + results = projects.Project.search(owner=owner, order=messages.Order.NAME) + results = projects.Project.filter(results, self.me) + results = sorted(results, key=lambda project: project.name) + resp = service_messages.SearchProjectResponse() + resp.projects = [project.to_message() for project in results] + return resp + + @remote.method(service_messages.UpdateProjectRequest, + service_messages.UpdateProjectResponse) + def update(self, request): + project = self._get_project(request) + self._get_policy(project).authorize_admin() + project.update(request.project) + resp = service_messages.UpdateProjectResponse() + resp.project = project.to_message() + return resp + + @remote.method(service_messages.GetProjectRequest, + service_messages.GetProjectResponse) + def get(self, request): + project = self._get_project(request) + self._get_policy(project).authorize_read() + resp = service_messages.GetProjectResponse() + resp.project = project.to_message() + return resp + + @remote.method(service_messages.DeleteProjectRequest, + service_messages.DeleteProjectResponse) + def delete(self, request): + project = self._get_project(request) + self._get_policy(project).authorize_admin() + project.delete() + resp = service_messages.DeleteProjectResponse() + return resp + + @remote.method(service_messages.GetProjectRequest, + service_messages.CreateWatcherResponse) + def watch(self, request): + project = self._get_project(request) + self._get_policy(project).authorize_read() + watcher = project.create_watcher(self.me) + resp = service_messages.CreateWatcherResponse() + resp.watcher = watcher.to_message() + return resp + + @remote.method(service_messages.GetProjectRequest, + service_messages.ListWatchersResponse) + def unwatch(self, request): + project = self._get_project(request) + self._get_policy(project).authorize_read() + project.delete_watcher(self.me) + watchers = project.list_watchers() + resp = service_messages.ListWatchersResponse() + resp.watchers = [watcher.to_message() for watcher in watchers] + return resp + + @remote.method(service_messages.ListWatchersRequest, + service_messages.ListWatchersResponse) + def list_watchers(self, request): + project = self._get_project(request) + 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) + resp.watchers = [watcher.to_message() for watcher in watchers] + return resp + + @remote.method(service_messages.ListNamedFilesetsRequest, + service_messages.ListNamedFilesetsResponse) + def list_named_filesets(self, request): + project = self._get_project(request) + self._get_policy(project).authorize_read() + named_filesets = project.list_named_filesets() + resp = service_messages.ListNamedFilesetsRequest() + resp.named_filesets = [named_fileset.to_message() + for named_fileset in named_filesets] + return resp + + @remote.method(service_messages.CreateNamedFilesetRequest, + service_messages.CreateNamedFilesetResponse) + def create_named_fileset(self, request): + project = self._get_project(request) + self._get_policy(project).authorize_write() + named_fileset = project.create_named_fileset( + request.named_fileset.name, request.named_fileset.branch) + resp = service_messages.CreateNamedFilesetResponse() + resp.named_fileset = named_fileset.to_message() + return resp + + @remote.method(service_messages.DeleteNamedFilesetRequest, + service_messages.DeleteNamedFilesetResponse) + def delete_named_fileset(self, request): + project = self._get_project(request) + self._get_policy(project).authorize_write() + 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) + self._get_policy(project).authorize_read() + try: + branches = project.list_branches() + except (buildbot.Error, projects.GitIntegrationError) as e: + raise api.Error(str(e)) + 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) + self._get_policy(project).authorize_read() + try: + catalogs = project.list_catalogs(ref=request.branch) + 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] + return resp + + @remote.method(service_messages.CatalogRequest, + service_messages.CatalogResponse) + def get_catalog(self, request): + project = self._get_project(request) + self._get_policy(project).authorize_read() + try: + 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() + 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) + self._get_policy(project).authorize_write() + try: + catalog = project.get_catalog(request.catalog.locale) + except buildbot.Error as e: + raise api.Error(str(e)) + 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 + + @remote.method(service_messages.GroupRequest, + service_messages.GroupResponse) + def get_group(self, request): + 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 = 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): + project = self._get_project(request) + self._get_policy(project).authorize_admin() + 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 + + @remote.method(service_messages.MembershipRequest, + service_messages.GroupResponse) + def delete_membership(self, request): + project = self._get_project(request) + self._get_policy(project).authorize_admin() + 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 + + @remote.method(service_messages.TransferOwnerRequest, + service_messages.GetProjectResponse) + def transfer(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/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/teams/__init__.py b/app/server/__init__.py similarity index 100% rename from jetway/teams/__init__.py rename to app/server/__init__.py diff --git a/jetway/server/handlers.py b/app/server/handlers.py similarity index 81% rename from jetway/server/handlers.py rename to app/server/handlers.py index 518f66a..b880488 100644 --- a/jetway/server/handlers.py +++ b/app/server/handlers.py @@ -1,10 +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.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 @@ -15,6 +16,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 +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 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) 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 69% rename from jetway/server/utils.py rename to app/server/utils.py index 0cc3f96..f2c6fcd 100644 --- a/jetway/server/utils.py +++ b/app/server/utils.py @@ -5,10 +5,14 @@ _HOSTNAME_RE = re.compile('^(?:(.*)--)?(.*)--([^\.]*)\.') +def is_avatar_request(hostname): + return re.match('^avatars-.-dot-', 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): @@ -26,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, @@ -38,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) diff --git a/jetway/server/utils_test.py b/app/server/utils_test.py similarity index 84% rename from jetway/server/utils_test.py rename to app/server/utils_test.py index 3bde0ac..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 @@ -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/testing.py b/app/testing.py similarity index 76% rename from jetway/testing.py rename to app/testing.py index 0eaac26..ea01505 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 @@ -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) diff --git a/jetway/users/__init__.py b/app/translations/__init__.py similarity index 100% rename from jetway/users/__init__.py rename to app/translations/__init__.py diff --git a/app/translations/messages.py b/app/translations/messages.py new file mode 100644 index 0000000..b425090 --- /dev/null +++ b/app/translations/messages.py @@ -0,0 +1,7 @@ +from protorpc import messages + + +class TranslationMessage(messages.Message): + msgid = messages.StringField(1) + string = messages.StringField(2) + ident = messages.StringField(3) diff --git a/app/translations/translations.py b/app/translations/translations.py new file mode 100644 index 0000000..ac4bc1d --- /dev/null +++ b/app/translations/translations.py @@ -0,0 +1,28 @@ +from . import messages + + +class Translation(object): + + def __init__(self, catalog, msgid, string, comments=None): + self.catalog = catalog + 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/utils/__init__.py b/app/users/__init__.py similarity index 100% rename from jetway/utils/__init__.py rename to app/users/__init__.py diff --git a/app/users/messages.py b/app/users/messages.py new file mode 100644 index 0000000..17b1611 --- /dev/null +++ b/app/users/messages.py @@ -0,0 +1,12 @@ +from protorpc import messages + + +class UserMessage(messages.Message): + ident = messages.StringField(1) + nickname = messages.StringField(2) + avatar_url = messages.StringField(3) + email = messages.StringField(4) + website_url = messages.StringField(5) + description = messages.StringField(6) + location = messages.StringField(7) + name = messages.StringField(8) diff --git a/jetway/users/messages.py b/app/users/service_messages.py similarity index 59% rename from jetway/users/messages.py rename to app/users/service_messages.py index a91b3ff..5f30956 100644 --- a/jetway/users/messages.py +++ b/app/users/service_messages.py @@ -1,19 +1,7 @@ +from .messages import UserMessage +from app.orgs import messages as org_messages +from app.projects import messages as project_messages from protorpc import messages -from jetway.orgs import messages as org_messages -from jetway.projects import messages as project_messages - - -class UserMessage(messages.Message): - ident = messages.StringField(1) - nickname = messages.StringField(2) - avatar_url = messages.StringField(3) - email = messages.StringField(4) - website_url = messages.StringField(5) - description = messages.StringField(6) - location = messages.StringField(7) - - -### class GetMeRequest(messages.Message): @@ -21,16 +9,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 +30,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/app/users/services.py similarity index 54% rename from jetway/users/services.py rename to app/users/services.py index f4fffdd..28a2f17 100644 --- a/jetway/users/services.py +++ b/app/users/services.py @@ -1,62 +1,65 @@ -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 . import service_messages +from app import api +from app.owners import owners +from app.projects import projects +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.me) + 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 @@ -70,17 +73,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): @@ -93,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/jetway/users/users.py b/app/users/users.py similarity index 71% rename from jetway/users/users.py rename to app/users/users.py index d1de4a8..9983336 100644 --- a/jetway/users/users.py +++ b/app/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 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 class Error(Exception): @@ -26,6 +25,11 @@ class UserExistsError(Error): 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. @@ -33,10 +37,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): @@ -82,7 +98,6 @@ def create_unique_username(cls, email): class User(BaseUser): - nickname = ndb.StringProperty() description = ndb.StringProperty() location = ndb.StringProperty() @@ -108,23 +123,27 @@ 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 @property def ident(self): - return str(self.key.id()) + return self.key.urlsafe() 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 + message.name = self.name return message def to_me_message(self): @@ -175,7 +194,7 @@ 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 self.location = message.location @@ -183,40 +202,44 @@ def update(self, message): self.put() def search_teams(self): - query = teams.Team.query() - query = query.filter(teams.Team.user_keys == self.key) + from app.groups import groups + query = groups.Group.query() + 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): - 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)) + from app.groups import groups + query = groups.Group.query() + query = query.filter(groups.Group.org_keys != None) + query = query.filter(groups.Group.memberships.user_key == self.key) + results = query.fetch() + 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: - project_keys += team.project_keys - team_projects = ndb.get_multi(list(set(project_keys))) - from jetway.projects import projects + 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) - results = filter(None, team_projects) + org_ents = self.search_orgs() + if org_ents: + user_projects += projects.Project.search(owner=org_ents) + results = filter(None, group_projects) for project in user_projects: if project not in results: 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/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 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/appengine_config.py b/appengine_config.py index f0c40a9..02105b3 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') @@ -22,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)) @@ -41,22 +44,30 @@ 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) + REQUIRE_HTTPS_FOR_APP = jetway_config.get('require_https', {}).get('app_domain', False) 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) - 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) + 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'): @@ -67,9 +78,13 @@ 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') -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/bower.json b/bower.json deleted file mode 100644 index 8bd3819..0000000 --- a/bower.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "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" - } -} diff --git a/config/jetway.yaml.example b/config/jetway.yaml.example old mode 100755 new mode 100644 index 6250e77..7e004a3 --- 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/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 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/index.yaml b/index.yaml index e13a19f..a326039 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 @@ -23,12 +31,39 @@ indexes: - name: modified direction: desc +- kind: Fileset + properties: + - name: name + - name: project_key + - name: modified + direction: desc + - kind: Fileset properties: - name: project_key - name: modified direction: desc +- kind: Group + properties: + - name: memberships.user_key + - name: org_key + +- kind: Group + properties: + - name: memberships.user_key + - name: org_keys + +- kind: Group + properties: + - name: memberships.user_key + - name: project_key + +- kind: Group + properties: + - name: memberships.user_key + - name: project_keys + - kind: Project properties: - name: owner_key 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) diff --git a/jetway/frontend/static/html/home.html b/jetway/frontend/static/html/home.html deleted file mode 100644 index ccff46c..0000000 --- a/jetway/frontend/static/html/home.html +++ /dev/null @@ -1,5 +0,0 @@ -

Sites

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

Create site

-
- -
-
- - -
-
/
-
- - -
-
- -
- - -
-
- -
- -
-
-
diff --git a/jetway/frontend/static/html/orgs.new.html b/jetway/frontend/static/html/orgs.new.html deleted file mode 100644 index 3473296..0000000 --- a/jetway/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/jetway/frontend/static/html/owner.html b/jetway/frontend/static/html/owner.html deleted file mode 100644 index 52738e2..0000000 --- a/jetway/frontend/static/html/owner.html +++ /dev/null @@ -1,5 +0,0 @@ -

- - {{owner.nickname}} -

-
diff --git a/jetway/frontend/static/html/project.builds.html b/jetway/frontend/static/html/project.builds.html deleted file mode 100644 index 802cd22..0000000 --- a/jetway/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/jetway/frontend/static/html/project.html b/jetway/frontend/static/html/project.html deleted file mode 100644 index f5b80ae..0000000 --- a/jetway/frontend/static/html/project.html +++ /dev/null @@ -1,37 +0,0 @@ -
-
- - - - -
-
- -

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

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

Connection

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

Delete

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

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

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

Settings

-
-
- - -
-
- - -
-
- -
-
-
diff --git a/jetway/frontend/static/html/settings.accounts.html b/jetway/frontend/static/html/settings.accounts.html deleted file mode 100644 index 85b6c33..0000000 --- a/jetway/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/jetway/frontend/static/html/settings.html b/jetway/frontend/static/html/settings.html deleted file mode 100644 index 04939f3..0000000 --- a/jetway/frontend/static/html/settings.html +++ /dev/null @@ -1,31 +0,0 @@ -
-

- - {{me.nickname}} -

-
-
- -
-
-
-
-
diff --git a/jetway/frontend/static/html/settings.index.html b/jetway/frontend/static/html/settings.index.html deleted file mode 100644 index 8bf2d1c..0000000 --- a/jetway/frontend/static/html/settings.index.html +++ /dev/null @@ -1,26 +0,0 @@ -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
Saving...
-
-
diff --git a/jetway/frontend/static/html/settings.referrals.html b/jetway/frontend/static/html/settings.referrals.html deleted file mode 100644 index f3e8b27..0000000 --- a/jetway/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/jetway/frontend/static/images/badge.png b/jetway/frontend/static/images/badge.png deleted file mode 100644 index 5196d8d..0000000 Binary files a/jetway/frontend/static/images/badge.png and /dev/null differ diff --git a/jetway/frontend/static/js/app.js b/jetway/frontend/static/js/app.js deleted file mode 100644 index fe2e7ab..0000000 --- a/jetway/frontend/static/js/app.js +++ /dev/null @@ -1,332 +0,0 @@ -var _prefix = '/_app/' + __config.ver; - -var app = angular.module('jetway', [ - 'ui.router', - 'ui.bootstrap', - 'jetwayFilters', - 'xeditable' -]) - -.config(function($stateProvider, $urlRouterProvider, $locationProvider, $tooltipProvider) { - $tooltipProvider.options({ - animation: false - }); - $locationProvider.html5Mode(true); - $stateProvider - .state('home', { - url: '/', - templateUrl: _prefix + '/static/html/home.html', - controller: HomeController - }) - .state('new', { - url: '/new?owner', - controller: NewController, - templateUrl: _prefix + '/static/html/new.html' - }) - .state('orgs', { - url: '/orgs', - template: '', - 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/jetway/frontend/static/js/controllers.js b/jetway/frontend/static/js/controllers.js deleted file mode 100644 index 8544524..0000000 --- a/jetway/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/jetway/frontend/static/js/filters.js b/jetway/frontend/static/js/filters.js deleted file mode 100644 index 147bfc9..0000000 --- a/jetway/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/jetway/frontend/static/js/rpc.js b/jetway/frontend/static/js/rpc.js deleted file mode 100644 index f922aa2..0000000 --- a/jetway/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/jetway/frontend/static/sass/_global.scss b/jetway/frontend/static/sass/_global.scss deleted file mode 100644 index ec00392..0000000 --- a/jetway/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/jetway/frontend/static/sass/main.scss b/jetway/frontend/static/sass/main.scss deleted file mode 100644 index 4fd53b8..0000000 --- a/jetway/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; -} 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/projects/messages.py b/jetway/projects/messages.py deleted file mode 100644 index c60ea0b..0000000 --- a/jetway/projects/messages.py +++ /dev/null @@ -1,37 +0,0 @@ -from protorpc import messages -from protorpc import message_types -from jetway.owners import messages as owner_messages - - -class Permission(messages.Enum): - READ = 1 - WRITE = 2 - ADMINISTER = 3 - - -class Visibility(messages.Enum): - PUBLIC = 1 - ORGANIZATION = 2 - PRIVATE = 3 - COVER = 4 - - -class CoverMessage(messages.Message): - content = messages.StringField(1) - - -class Order(messages.Enum): - NAME = 0 - - -class ProjectMessage(messages.Message): - nickname = messages.StringField(1) - 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) - name = messages.StringField(9) - built = message_types.DateTimeField(10) diff --git a/jetway/projects/services.py b/jetway/projects/services.py deleted file mode 100644 index 6b65459..0000000 --- a/jetway/projects/services.py +++ /dev/null @@ -1,156 +0,0 @@ -from . import service_messages -from protorpc import remote -from jetway import api -from jetway.owners import owners -from jetway.projects import projects -from jetway.projects import messages - - -class ProjectService(api.Service): - - def _get_project(self, request): - try: - if request.project.ident: - return projects.Project.get_by_ident(request.project.ident) - owner = owners.Owner.get(request.project.owner.nickname) - return projects.Project.get(owner, request.project.nickname) - except (owners.OwnerDoesNotExistError, - projects.ProjectDoesNotExistError) as e: - raise api.NotFoundError(str(e)) - - @remote.method(service_messages.CreateProjectRequest, - service_messages.CreateProjectResponse) - def create(self, request): - try: - owner = owners.Owner.get(request.project.owner.nickname) - project = projects.Project.create(owner, request.project.nickname, - description=request.project.description, - created_by=self.me) - except projects.ProjectExistsError as e: - raise api.ConflictError(str(e)) - resp = service_messages.CreateProjectResponse() - resp.project = project.to_message() - return resp - - @remote.method(service_messages.SearchProjectRequest, - service_messages.SearchProjectResponse) - def search(self, request): - if request.project and request.project.owner: - owner = owners.Owner.get(request.project.owner.nickname) - else: - owner = None - results = projects.Project.search(owner=owner, order=messages.Order.NAME) - results = projects.Project.filter(results, self.me) - results = sorted(results, key=lambda project: project.name) - resp = service_messages.SearchProjectResponse() - resp.projects = [project.to_message() for project in results] - return resp - - @remote.method(service_messages.UpdateProjectRequest, - service_messages.UpdateProjectResponse) - def update(self, request): - project = self._get_project(request) - if not project.can(self.me, projects.Permission.ADMINISTER): - raise api.ForbiddenError('Forbidden.') - project.update(request.project) - resp = service_messages.UpdateProjectResponse() - resp.project = project.to_message() - return resp - - @remote.method(service_messages.GetProjectRequest, - 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.') - resp = service_messages.GetProjectResponse() - resp.project = project.to_message() - return resp - - @remote.method(service_messages.DeleteProjectRequest, - 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.') - project.delete() - 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): - project = self._get_project(request) - if not project.can(self.me, projects.Permission.READ): - raise api.ForbiddenError('Forbidden.') - watcher = project.create_watcher(self.me) - resp = service_messages.CreateWatcherResponse() - resp.watcher = watcher.to_message() - return resp - - @remote.method(service_messages.GetProjectRequest, - 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.') - project.delete_watcher(self.me) - watchers = project.list_watchers() - resp = service_messages.ListWatchersResponse() - resp.watchers = [watcher.to_message() for watcher in watchers] - return resp - - @remote.method(service_messages.ListWatchersRequest, - 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.') - watchers = project.list_watchers() - resp = service_messages.ListWatchersResponse() - resp.watching = any(self.me == watcher.user for watcher in watchers) - resp.watchers = [watcher.to_message() for watcher in watchers] - return resp - - @remote.method(service_messages.ListNamedFilesetsRequest, - 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.') - named_filesets = project.list_named_filesets() - resp = service_messages.ListNamedFilesetsRequest() - resp.named_filesets = [named_fileset.to_message() - for named_fileset in named_filesets] - return resp - - @remote.method(service_messages.CreateNamedFilesetRequest, - 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.') - named_fileset = project.create_named_fileset( - request.named_fileset.name, request.named_fileset.branch) - resp = service_messages.CreateNamedFilesetResponse() - resp.named_fileset = named_fileset.to_message() - return resp - - @remote.method(service_messages.DeleteNamedFilesetRequest, - 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.') - project.delete_named_fileset(request.named_fileset.name) - resp = service_messages.DeleteNamedFilesetResponse() - return resp diff --git a/jetway/sheets/handlers.py b/jetway/sheets/handlers.py deleted file mode 100644 index fb6ab65..0000000 --- a/jetway/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/jetway/sheets/sheets.py b/jetway/sheets/sheets.py deleted file mode 100644 index eb52963..0000000 --- a/jetway/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 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 e2a02df..0000000 --- a/jetway/teams/services.py +++ /dev/null @@ -1,178 +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, - review_required=request.membership.review_required, - 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 fbd8449..0000000 --- a/jetway/teams/teams.py +++ /dev/null @@ -1,224 +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) - user_key = ndb.KeyProperty() - is_public = ndb.BooleanProperty(default=False) - review_required = ndb.BooleanProperty(default=False) - - @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 - message.review_required = self.review_required - 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 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, review_required=False, 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/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/requirements.txt b/requirements.txt index 5884e8e..612aa47 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,10 @@ +Babel==1.3 +pytz + GoogleAppEngineCloudStorageClient==1.9.5.0 google-api-python-client==1.3.1 httplib2 +requests protorpc-standalone==0.9.1 pycrypto==2.6 @@ -22,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 diff --git a/scripts/deploy b/scripts/deploy index c4ea7eb..51911e7 100755 --- a/scripts/deploy +++ b/scripts/deploy @@ -1,19 +1,24 @@ -#!/bin/bash -e +#!/bin/bash +cd webreview-fe && ember build && cd .. ./scripts/test 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 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 . 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 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.' diff --git a/webreview-fe b/webreview-fe new file mode 160000 index 0000000..d2b9303 --- /dev/null +++ b/webreview-fe @@ -0,0 +1 @@ +Subproject commit d2b9303af60b48b70456b6e4f0c96b5402df9b2b