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
-
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 @@
-
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
-
-
-
-
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 @@
-
-
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 @@
-
-
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 @@
-
-
-
- Watch
-
-
- Unwatch
-
-
- {{watchers.length || 0}}
-
-
-
-
-
-
-
-
-{{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
-
-
- Name
-
-
-
- Description
-
-
-
- Repository URL
-
-
-
- Save
-
-
-
-
-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 @@
-
-
-
-
-
-
-
-
-
- Read only
- Write
- Admin
-
-
-
- Add
-
-
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 @@
-
- Notify me of activity on this project
-
-
-
-
-
- {{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 @@
-
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
-
-
- Email
-
-
-
- Username
-
-
-
- Save
-
-
-
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.
-
Delete... (not yet available)
-
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 @@
-
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 @@
-
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.
-
-
-
-
-
- Send referral
-
- Add custom message
-
-
-
-
- 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