Commits

eduardo schettino committed 2f0f633

tutorial. split step 6 into 2. docs for step 6.

Comments (0)

Files changed (4)

doc/tutorial/tasks/tasks_7.py

+from google.appengine.ext import db
+import jinja2
+
+from avalanche.core import RequestHandler
+from avalanche.router import Route
+from avalanche.core import WSGIApplication
+from avalanche.snow import make_handler, JinjaRenderer, BaseHandler
+from avalanche.params import url_path_param, url_query_param, post_group_param
+
+
+############ Models
+
+class Task(db.Model):
+    name = db.StringProperty(required=True)
+    closed = db.BooleanProperty(default=False)
+    created = db.DateTimeProperty(auto_now_add=True)
+
+    @staticmethod
+    def id2task(task_id):
+        """@param task_id: (str)"""
+        return Task.get_by_id(int(task_id))
+
+
+############ Request Handlers
+
+class FlashMixinHandler(BaseHandler):
+    @url_query_param('flash')
+    def ctx_flash(self, flash):
+        return {'flash': flash}
+
+
+class TaskRequestHandler(FlashMixinHandler, BaseHandler):
+
+    MSG_TASK_NEW = 'New task successfully added'
+    MSG_TASK_NAME = 'Please enter a name for the task'
+    MSG_TASK_CLOSED = 'Task was successfully closed'
+
+    renderer = JinjaRenderer(jinja2.Environment(
+            loader=jinja2.FileSystemLoader('templates'),
+            undefined=jinja2.DebugUndefined,
+            autoescape=True,
+            ))
+
+    context_get = ['a_get', 'ctx_flash',]
+
+
+
+
+class ListTasks(TaskRequestHandler):
+    """list all open tasks"""
+    template = 'task_list.html'
+
+    @url_query_param('show_closed', 'closed', bool)
+    def a_get(self, show_closed):
+        if show_closed:
+            tasks = Task.all().order('created')
+        else:
+            tasks = Task.all().filter('closed = ', False).order('created')
+        return {'tasks': tasks,
+                'show_closed': show_closed,
+                }
+
+
+class NewTask(TaskRequestHandler):
+    """add a new task"""
+    template = 'task_new.html'
+
+    def a_get(self): # pragma: no cover
+        pass
+
+    @post_group_param('data', 'task')
+    def a_post(self, data):
+        name = data.get('name')
+        if name:
+            Task(name=name).put()
+            self.a_redirect('task-list', flash=self.MSG_TASK_NEW)
+        else:
+            self.a_redirect('task-new', flash=self.MSG_TASK_NAME)
+
+class CloseTask(TaskRequestHandler):
+    """mark a task as closed"""
+
+    @url_path_param('task', 'task_id', Task.id2task)
+    def a_post(self, task):
+        task.closed = True
+        task.put()
+        self.a_redirect('task-list', flash=self.MSG_TASK_CLOSED)
+
+
+########### WSGI App
+
+def tasks_app():
+    route_raw = [('/', ListTasks, 'task-list'),
+                 ('/new', NewTask, 'task-new'),
+                 ('/close/<task_id>', CloseTask, 'task-close')
+                 ]
+    routes = []
+    for path, handler, name in route_raw:
+        handler_class = make_handler(RequestHandler, handler)
+        routes.append(Route(path, handler_class, name))
+    return WSGIApplication(routes, debug=True)
+
+app = tasks_app()

doc/tutorial/tasks/tests/test_tasks_6.py

 import sys, os
 sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/..'))
 
+from urlparse import urlparse
+import urllib
+
 from google.appengine.ext import testbed
+from webob import Request
+from avalanche.core import RequestHandler
+from avalanche.snow import make_handler
 
-from tasks_6 import Task, TaskRequestHandler
+from tasks_6 import tasks_app, Task, TaskRequestHandler
 from tasks_6 import ListTasks, NewTask, CloseTask
 from tasks_6 import FlashMixinHandler
 
         my_testbed = testbed.Testbed()
         my_testbed.activate()
         my_testbed.init_datastore_v3_stub()
-        return my_testbed
 
-    def deactivate(my_testbed):
-        my_testbed.deactivate()
+        app = tasks_app()
+        app.testbed = my_testbed
+        return app
+
+    def deactivate(app):
+        app.testbed.deactivate()
 
     return request.cached_setup(
         setup=create,
         scope="function")
 
 
+def create_handler(app, handler_class, path='', POST=None, **req_kwargs):
+    """helper to setup request handler instances"""
+    request = Request.blank(path, POST=POST, **req_kwargs)
+    handler_class = make_handler(RequestHandler, handler_class)
+    return handler_class(app, request)
+
+
 
 ###############################################
 
 
 
 class TestFlahsHandler(object):
-    def test_ctx_flash(self):
-        handler = FlashMixinHandler()
+    def test_ctx_flash(self, app):
+        handler = create_handler(app, FlashMixinHandler)
         ctx = handler.ctx_flash(flash='flash message')
         assert ctx['flash'] == 'flash message'
 
         Task(name='task 2', closed=True).put()
         Task(name='task 3').put()
 
-        handler = ListTasks()
+        handler = create_handler(app, ListTasks)
         ctx = handler.a_get(show_closed=False)
         tasks = list(ctx['tasks'])
         assert len(tasks) == 2 # closed task is not included
         Task(name='task 2', closed=True).put()
         Task(name='task 3').put()
 
-        handler = ListTasks()
+        handler = create_handler(app, ListTasks)
         ctx = handler.a_get(show_closed=True)
         tasks = list(ctx['tasks'])
         assert len(tasks) == 3
 
 
 class TestNewTask(object):
-    def test_get(self):
+    def test_get(self, app):
         pass
 
     def test_post_save(self, app):
         post_data = {'name':'task test xxx'}
-        handler = NewTask()
+        handler = create_handler(app, NewTask)
         handler.a_post(post_data)
 
         # task is saved in DB
 
     def test_post_error(self, app):
         post_data = {'wrong_name': 'task test xxx'}
-        handler = NewTask()
+        handler = create_handler(app, NewTask)
         handler.a_post(data=post_data)
 
         # task is not saved in DB
         task_before.put()
         assert not task_before.closed
 
-        handler = CloseTask()
+        handler = create_handler(app, CloseTask)
         handler.a_post(task_before)
 
         # task is closed

doc/tutorial/tasks/tests/test_tasks_7.py

+import sys, os
+sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/..'))
+
+from google.appengine.ext import testbed
+
+from tasks_7 import Task, TaskRequestHandler
+from tasks_7 import ListTasks, NewTask, CloseTask
+from tasks_7 import FlashMixinHandler
+
+# create test app
+def pytest_funcarg__app(request):
+    def create():
+        my_testbed = testbed.Testbed()
+        my_testbed.activate()
+        my_testbed.init_datastore_v3_stub()
+        return my_testbed
+
+    def deactivate(my_testbed):
+        my_testbed.deactivate()
+
+    return request.cached_setup(
+        setup=create,
+        teardown=deactivate,
+        scope="function")
+
+
+
+###############################################
+
+
+class TestTaskModel(object):
+    def test_id2task(self, app):
+        task_key = Task(name='my first task').put()
+        got = Task.id2task(str(task_key.id()))
+        assert task_key == got.key()
+
+
+class TestFlahsHandler(object):
+    def test_ctx_flash(self):
+        handler = FlashMixinHandler()
+        ctx = handler.ctx_flash(flash='flash message')
+        assert ctx['flash'] == 'flash message'
+
+
+class TestListTasks(object):
+    def test_get_display_not_closed(self, app):
+        Task(name='my first task').put()
+        Task(name='task 2', closed=True).put()
+        Task(name='task 3').put()
+
+        handler = ListTasks()
+        ctx = handler.a_get(show_closed=False)
+        tasks = list(ctx['tasks'])
+        assert len(tasks) == 2 # closed task is not included
+        assert tasks[0].name == "my first task"
+        assert tasks[1].name == "task 3"
+        assert ctx['show_closed'] == False
+
+    def test_get_display_all(self, app):
+        Task(name='my first task').put()
+        Task(name='task 2', closed=True).put()
+        Task(name='task 3').put()
+
+        handler = ListTasks()
+        ctx = handler.a_get(show_closed=True)
+        tasks = list(ctx['tasks'])
+        assert len(tasks) == 3
+        assert tasks[0].name == "my first task"
+        assert tasks[1].name == "task 2"
+        assert tasks[2].name == "task 3"
+        assert ctx['show_closed'] == True
+
+
+class TestNewTask(object):
+    def test_get(self):
+        pass
+
+    def test_post_save(self, app):
+        post_data = {'name':'task test xxx'}
+        handler = NewTask()
+        handler.a_post(post_data)
+
+        # task is saved in DB
+        saved = Task.all().get()
+        assert saved
+        assert 'task test xxx' == saved.name
+
+        # page is redirected to list page
+        assert 'task-list' == handler.redirect_info[0]
+        assert {'flash':TaskRequestHandler.MSG_TASK_NEW} == handler.redirect_info[1]
+
+
+    def test_post_error(self, app):
+        post_data = {'wrong_name': 'task test xxx'}
+        handler = NewTask()
+        handler.a_post(data=post_data)
+
+        # task is not saved in DB
+        saved = Task.all().get()
+        assert not saved
+
+        # page is redirected to list page
+        assert 'task-new' == handler.redirect_info[0]
+        assert {'flash':TaskRequestHandler.MSG_TASK_NAME} == handler.redirect_info[1]
+
+
+class TestCloseTask(object):
+    def test_post(self, app):
+        task_before = Task(name="task abc")
+        task_before.put()
+        assert not task_before.closed
+
+        handler = CloseTask()
+        handler.a_post(task_before)
+
+        # task is closed
+        task_after = Task.get_by_id(task_before.key().id())
+        assert task_after.closed
+
+        # page is redirected to list page
+        assert 'task-list' == handler.redirect_info[0]
+        assert {'flash':TaskRequestHandler.MSG_TASK_CLOSED} == handler.redirect_info[1]

doc/understanding.rst

 step 6
 ========
 
-This final step will not introduce any new use-case or avalanche concept.
-But there is one last thing that should be refactored.
-All flash messages are being directly saved by the handlers, this would make it
-hard to re-use the handlers in another application that uses a different
-session storage.
+This final step will not introduce any new use-case.
 
-We will modify the POST methods to save the flash messages in the context.
-And add context-builder in FlashMixinHandler to actually add the messages to
-the session.
+The tests for HTTP 'redirect' look like quite cumbersome. ``avalanche.BaseHandler``
+has a method ``a_redirect``. This method can be used instead of
+``core.RequestHandler.redirect_to``, it will save the redirect info into the
+attribute ``redirect_info``.
+
 
 ``context_param`` is used to take a value from the context.
 
 
 .. code-block:: diff
 
+     class NewTask(TaskRequestHandler):
+
+         @post_group_param('data', 'task')
+         def a_post(self, data):
+             name = data.get('name')
+             if name:
+                 Task(name=name).put()
+  -              self.redirect_to('task-list', flash=self.MSG_TASK_NEW)
+  +              self.a_redirect('task-list', flash=self.MSG_TASK_NEW)
+             else:
+  -              self.redirect_to('task-new', flash=self.MSG_TASK_NAME)
+  +              self.a_redirect('task-new', flash=self.MSG_TASK_NAME)
 
 
 .. code-block:: diff
 
+    class CloseTask(TaskRequestHandler):
+        """mark a task as closed"""
+
+        @url_path_param('task', 'task_id', Task.id2task)
+        def a_post(self, task):
+            task.closed = True
+            task.put()
+   -        self.redirect_to('task-list', flash=self.MSG_TASK_CLOSED)
+   +        self.a_redirect('task-list', flash=self.MSG_TASK_CLOSED)
+
+
 
 Complete source of `tasks.py step 6 <https://bitbucket.org/schettino72/avalanche/src/tip/doc/tutorial/tasks/tasks_6.py>`_
 
 step 6 - tests
 ----------------
 
-.. code-block:: diff
-
-
+So now we can test the values straight away without dealing with
+conversions and quoting of values.
 
 .. code-block:: diff
+    def test_post_save(self, app):
+        (...)
 
+        # page is redirected to list page
+  -     assert handler.response.status_int == 302
+  -     location = urlparse(handler.response.location)
+  -     assert location.path == handler.uri_for('task-list')
+  -     # flash message in page
+  -     query = "flash=" + urllib.quote_plus(TaskRequestHandler.MSG_TASK_NEW)
+  -     assert location.query == query
+
+  +     assert 'task-list' == handler.redirect_info[0]
+  +     assert {'flash':TaskRequestHandler.MSG_TASK_NEW} == handler.redirect_info[1]
 
 
 You can check the other changes from the complete source of `tests step 6 <https://bitbucket.org/schettino72/avalanche/src/tip/doc/tutorial/tasks/tests/test_tasks_6.py>`_
 
 
+step 7
+==========
+
+
+
 Other use cases
 =================
 
-This first release of Avalanche focus on getting re-usability of single
+Up to now Avalanche focus on getting re-usability of single
 RequestHandler's separetely.
 Next releases will focus on re-usability of several handlers at once.