diff --git a/MEMO.txt b/MEMO.txt index 1b36d91..4639a8b 100644 --- a/MEMO.txt +++ b/MEMO.txt @@ -8,8 +8,21 @@ TODO: + Add empty index page that ask user to login. + Add empty main page which required login user. + Add bootstrap stack - - Add empty js test page - - Add empty app js to main page + + Add empty js test page + + Add empty app js to main page + +> Minimal Model and API: + + Define User Model + > Define UserController + + /r/ for get + + /r/me for get + - Define Card Model + > Define CardController + - /r//card for list (no pagination) + - /r//card/ for get/put/delete + +> A simple CRUD for the Card model +> Exercise Rounds > Someday: - Logout diff --git a/app/app.yaml b/app/app.yaml index 72ce67c..f6364c0 100644 --- a/app/app.yaml +++ b/app/app.yaml @@ -19,5 +19,3 @@ libraries: version: latest - name: jinja2 version: latest -#- name: MarkupSafe -# version: latest \ No newline at end of file diff --git a/app/py/blbr/__init__.py b/app/py/blbr/__init__.py index e90f373..4502f9f 100644 --- a/app/py/blbr/__init__.py +++ b/app/py/blbr/__init__.py @@ -1,8 +1,65 @@ +import functools +import webapp2 +import json from google.appengine.ext import db +from google.appengine.api import users +def require_login(**options): + if users.get_current_user(): + return None + redirect = options.get('redirect') + if redirect: + return webapp2.redirect(users.create_login_url(redirect)) + resp = webapp2.Response() + resp.status = '400 Bad Request' + return resp + +def login_required(func, **deco_kwargs): + @functools.wraps(func) + def decorated_view(*args, **kwargs): + return require_login(**deco_kwargs) or func(*args, **kwargs) + return decorated_view -class User(db.Model): +def user_to_serializable(user): + return { + "nickname": user.nickname(), + "email": user.email(), + "user_id": user.user_id() + } + + +class ModelSerizable(object): + + to_map = { + users.User: user_to_serializable + } + + @staticmethod + def _build_property_name(list, base): + if issubclass(base, ModelSerizable): + list += base.list_property_names() + return list + + @classmethod + def list_property_names(cls): + names = [k for k,v in cls.__dict__.items() if isinstance(v, db.Property)] + return reduce(cls._build_property_name, cls.__bases__, names) + + @classmethod + def _build_serializable(cls, value, dict): + names = cls.list_property_names() + for name in names: + value = getattr(value, name) + to = ModelSerizable.to_map.get(value.__class__) + dict[name] = to and to(value) or value + return dict + + def to_serializable(self): + return self._build_serializable(self, {"id": str(self.key()) }) + + +class User(db.Model, ModelSerizable): account = db.UserProperty(required=True) @classmethod @@ -11,4 +68,54 @@ def find_by_account(cls, account): @classmethod def ensure_by_account(cls, account): - return cls.find_by_account(account) or cls(account=account) + found = cls.find_by_account(account) + if found: + return found + created = cls(account=account) + created.put() + return created + + +class UserMapper(object): + def get_account(self): + return users.get_current_user() + + def get(self, params): + if (1 != len(params)): + return users.create_login_url(self.url) + + account = self.get_account() + key = params[0] + if (key == "me"): + return User.ensure_by_account(account) + try: + found = User.get(db.Key(key)) + except db.BadKeyError: + return None + if not found or found.account != account: + return None + return found + + +class UserController(webapp2.RequestHandler): + url = '/r/([^/]+)' + + @property + def mapper(self): + if not hasattr(self, '_mapper'): + self._mapper = UserMapper() + return self._mapper + + @login_required + def get(self, *args): + found = self.mapper.get(args) + if not found: + self.response.status = 404 + return + self.response.headers['Content-Type'] = 'text/json' + json.dump(found.to_serializable(), self.response.out) + return self.response + + +def to_application(handler_classes): + return webapp2.WSGIApplication([(p.url, p) for p in handler_classes]) diff --git a/app/py/hello_test.py b/app/py/hello_test.py index ee6c670..69a09e7 100644 --- a/app/py/hello_test.py +++ b/app/py/hello_test.py @@ -1,5 +1,8 @@ import unittest +import os +import json +import webapp2 from google.appengine.api import memcache from google.appengine.api import users @@ -8,15 +11,21 @@ import blbr -class HelloTest(unittest.TestCase): - def test_hello(self): - pass +def round_serialization(obj): + return json.loads(json.dumps(obj)) +class WSGITestHelper(object): -class UserTest(unittest.TestCase): + def __init__(self, application): + self.application = application - def setUp(self): + def get(self, url): + req = webapp2.Request.blank(url) + return req.get_response(self.application) + +class TestBedHelper(object): + def __init__(self): # First, create an instance of the Testbed class. self.testbed = testbed.Testbed() # Then activate the testbed, which prepares the service stubs for use. @@ -25,19 +34,87 @@ def setUp(self): self.testbed.init_user_stub() self.testbed.init_datastore_v3_stub() self.testbed.init_memcache_stub() + self.user_email = "alice@example.com" + os.environ['USER_EMAIL'] = self.user_email - def tearDown(self): + def disable_current_user(self): + if os.environ.get('USER_EMAIL'): + del os.environ['USER_EMAIL'] + + def deactivate(self): + self.disable_current_user() self.testbed.deactivate() + + +class SerializableTest(unittest.TestCase): + def setUp(self): + self.helper = TestBedHelper() + def tearDown(self): + self.helper.deactivate() + def test_hello(self): - account = users.User("alice@example.com") - new_user = blbr.User(account=account) + email = "bob@example.com" + new_user = blbr.User(account=users.User(email)) new_user.put() + existing_user = blbr.User.find_by_account(new_user.account) + rounded = round_serialization(existing_user.to_serializable()) + self.assertIsNotNone(rounded['id']) + self.assertEquals(rounded['account']['email'], email) + + +class UserTest(unittest.TestCase, TestBedHelper): + def setUp(self): + self.helper = TestBedHelper() + self.web = WSGITestHelper(blbr.to_application([blbr.UserController])) + self.bob_email = "bob@example.com" - existing_user = blbr.User.find_by_account(account) + def tearDown(self): + self.helper.deactivate() + + def test_list_property_names(self): + names = blbr.User.list_property_names() + self.assertEquals(names, ['account']) + + def create_bob_user(self): + account = users.User(self.bob_email) + creating = blbr.User(account=account) + creating.put() + return creating + + def test_find_by_account(self): + new_user = self.create_bob_user() + existing_user = blbr.User.find_by_account(new_user.account) self.assertEquals(existing_user.account, new_user.account) - + def test_ensure(self): - account = users.User("alice@example.com") - u = blbr.User.ensure_by_account(account) + u = blbr.User.ensure_by_account(users.User("bob@example.com")) self.assertIsNotNone(u) + self.assertTrue(u.is_saved()) + + def test_web_get_me(self): + res = self.web.get('/r/me') + self.assertRegexpMatches(res.status, '200') + self.assertEquals(self.helper.user_email, json.loads(res.body)["account"]["email"]) + + def test_web_get_me(self): + alice = blbr.User.ensure_by_account(users.get_current_user()) + res = self.web.get('/r/%s' % str(alice.key())) + self.assertRegexpMatches(res.status, '200') + self.assertEquals(self.helper.user_email, json.loads(res.body)["account"]["email"]) + + def test_web_get_non_owner(self): + bob = self.create_bob_user() + res = self.web.get('/r/%s' % str(bob.key())) + self.assertRegexpMatches(res.status, '404') + + def test_web_get_notfound(self): + bob = self.create_bob_user() + res = self.web.get('/r/nonexistent') + self.assertRegexpMatches(res.status, '404') + + def test_web_unauth(self): + self.helper.disable_current_user() + res = self.web.get('/r/me') + self.assertRegexpMatches(res.status, '400') + diff --git a/app/py/theapp.py b/app/py/theapp.py index 20b3c29..c5a26a5 100644 --- a/app/py/theapp.py +++ b/app/py/theapp.py @@ -14,43 +14,41 @@ class TemplatePage(webapp2.RequestHandler): - LOGIN_REQUIRED = True + login_required = True def make_context(self): return {} def get(self): - user = users.get_current_user() - if not user and self.LOGIN_REQUIRED: - #raise Exception("Hello") - self.response = webapp2.redirect(users.create_login_url(self.URL)) - return self.response - - template = jinja_environment.get_template(self.TEMPLATE_NAME) + requiring = self.login_required and blbr.require_login(redirect=self.url) or None + if requiring: + return requiring + template = jinja_environment.get_template(self.template_name) self.response.headers['Content-Type'] = 'text/html' self.response.out.write(template.render(self.make_context())) - + return self.response class IndexPage(TemplatePage): - URL = "/" - TEMPLATE_NAME = 'index.html' - LOGIN_REQUIRED = False + url = "/" + template_name = 'index.html' + login_required = False def make_context(self): - return {'login_url': users.create_login_url(DashboardPage.URL)} + return {'login_url': users.create_login_url(DashboardPage.url)} class DashboardPage(TemplatePage): - URL = '/dashboard' - TEMPLATE_NAME = 'dashboard.html' + url = '/dashboard' + template_name = 'dashboard.html' class TestPage(TemplatePage): - URL = '/test' - LOGIN_REQUIRED = False - TEMPLATE_NAME = 'test.html' + url = '/test' + login_required = False + template_name = 'test.html' -page_classes = [IndexPage, DashboardPage, TestPage] +page_classes = [IndexPage, DashboardPage, TestPage, + blbr.UserController] -# The name |app| is given at 'app.cfg' file. -app = webapp2.WSGIApplication([(p.URL, p) for p in page_classes]) +# Don't change the name |app|. It is given in the 'app.cfg' file. +app = blbr.to_application(page_classes)