diff --git a/.gitignore b/.gitignore index 36de90c..b656579 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ todopyramid.sqlite +*.pyc +*.egg-info diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6de862a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: python +python: + - "2.7" + - "2.6" +# command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors +install: + - "pip install -r requirements.txt -e ." + - "pip install nose" +# command to run tests, e.g. python setup.py test +script: "python setup.py nosetests" \ No newline at end of file diff --git a/README.md b/README.md index b2320c2..68c38d5 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ # ToDo Pyramid App -This is the Pyramid app for the Python Web Shootout. +[![Build Status](https://travis-ci.org/saschagottfried/todopyramid.svg?branch=master)](https://travis-ci.org/saschagottfried/todopyramid) + +This is a refactored version of the Pyramid app for the Python Web Shootout originally crafted by SixFeetUp. + +Try it out here: -Try it out here: ## Install @@ -139,18 +142,18 @@ Then we need to pull in its dependencies (which includes Deform itself). Then up (todopyramid)$ pip freeze > requirements.txt ``` -Then add the static resources to the `__init__.py` - +Since we include deform_bootstrap_extra, it does all the static resources registration usually done manually in __init__.py ``` -# Adding the static resources from Deform -config.add_static_view('deform_static', 'deform:static', cache_max_age=3600) -config.add_static_view('deform_bootstrap_static', 'deform_bootstrap:static', cache_max_age=3600) +pyramid.includes = + pyramid_tm + pyramid_persona + deform_bootstrap_extra ``` Now we need to get our template structure in place. We'll add a `todopyramid/layouts.py` with the following (see the [Creating a Custom UX for Pyramid][customux] tutorial for more details): ``` -ifrom pyramid.renderers import get_renderer +from pyramid.renderers import get_renderer from pyramid.decorator import reify @@ -166,14 +169,18 @@ Add the `global_layout.pt` with at least the following (look at the source code ``` - + - - - + + + + + @@ -184,17 +191,10 @@ Add the `global_layout.pt` with at least the following (look at the source code - - - - - - - - - - - + + + + ``` @@ -207,9 +207,9 @@ from .layouts import Layouts class ToDoViews(Layouts): - def __init__(self, context, request): - self.context = context + def __init__(self, request): self.request = request + self.context = request.context @view_config(route_name='home', renderer='templates/home.pt') def home_view(request): @@ -220,7 +220,7 @@ class ToDoViews(Layouts): Now we can add a `todopyramid/templates/home.pt` to our app with the following ``` - +

Home

Welcome to the Pyramid version of the ToDo app.

@@ -248,6 +248,48 @@ Now that we have created the shell for our app, it is time to create some models We will create a `TodoItem` and `Tag` model to start out with. This will give us the basis for our todo list. + +### Model Relationships + +TBD - Add notes about model relationships that support features offered by todopyramid. + +### Explore SQLAlchemy model with IPython + +``` +$ bin/pshell production.ini +Adding asdict2() to Colander. +Python 2.7.2+ (default, Jul 20 2012, 22:12:53) +Type "copyright", "credits" or "license" for more information. + +IPython 0.13.1 -- An enhanced Interactive Python. +? -> Introduction and overview of IPython's features. +%quickref -> Quick reference. +help -> Python's own help system. +object? -> Details about 'object', use 'object??' for extra details. + +Environment: + app The WSGI application. + registry Active Pyramid registry. + request Active request object. + root Root of the default resource tree. + root_factory Default root factory used to create `root`. + +In [1]: from todopyramid.models import DBSession, TodoUser + +In [2]: user = DBSession.query(TodoUser).filter_by(first_name='Arthur').one() + +In [3]: user +Out[3]: + +In [4]: user.email +Out[4]: u'king.arthur@example.com' +``` + +### Sorting + +TodoPyramids TodoGrid can order a rendered list of TodoItems by task name & due date - ascending and descending. + + [install]: http://pyramid.readthedocs.org/en/latest/narr/install.html [deform]: http://docs.pylonsproject.org/projects/deform/en/latest/ [deform_bootstrap]: http://pypi.python.org/pypi/deform_bootstrap diff --git a/development.ini b/development.ini index a58aa06..e727448 100644 --- a/development.ini +++ b/development.ini @@ -14,11 +14,13 @@ pyramid.default_locale_name = en pyramid.includes = pyramid_debugtoolbar pyramid_tm + pyramid_persona + deform_bootstrap_extra sqlalchemy.url = sqlite:///%(here)s/todopyramid.sqlite persona.secret = s00per s3cr3t -persona.audiences = http://localhost:6543 +persona.audiences = todopyramid.localhost persona.siteName = ToDo Pyramid # Option to generate content for new users to try out @@ -34,7 +36,7 @@ todopyramid.generate_content = true [server:main] use = egg:waitress#main -host = 0.0.0.0 +host = 127.0.0.1 port = 6543 ### diff --git a/production.ini b/production.ini index 0d47e8e..c2bb11e 100644 --- a/production.ini +++ b/production.ini @@ -13,19 +13,21 @@ pyramid.debug_routematch = false pyramid.default_locale_name = en pyramid.includes = pyramid_tm + pyramid_persona + deform_bootstrap_extra sqlalchemy.url = sqlite:///%(here)s/todopyramid.sqlite persona.secret = s00per s3cr3t -persona.audiences = http://demo.todo.sixfeetup.com +persona.audiences = todopyramid.localhost persona.siteName = ToDo Pyramid # Option to generate content for new users to try out -todopyramid.generate_content = true +todopyramid.generate_content = false [server:main] use = egg:waitress#main -host = 0.0.0.0 +host = 127.0.0.1 port = 6543 ### diff --git a/requirements.txt b/requirements.txt index aaa61d9..0cffb66 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,20 +4,20 @@ MarkupSafe==0.15 PasteDeploy==1.5.0 PyBrowserID==0.9.1 Pygments==1.6 -SQLAlchemy==0.8.0b2 +SQLAlchemy==0.8.0 WebHelpers==1.3 WebOb==1.2.3 -bag==0.3.2 +bag==0.3.4 colander==1.0a2 deform==0.9.6 -deform-bootstrap==0.2.6 -deform-bootstrap-extra==0.2 +deform-bootstrap==0.2.8 +deform-bootstrap-extra==0.2.8 distribute==0.6.35 ipython==0.13.1 peppercorn==0.4 -pyramid==1.4 +pyramid==1.4.3 pyramid-debugtoolbar==1.0.4 -pyramid-deform==0.2a5 +pyramid-deform==0.2 pyramid-persona==1.3.1 pyramid-tm==0.7 pytz==2012j diff --git a/setup.py b/setup.py index 3cb5700..24adcaf 100644 --- a/setup.py +++ b/setup.py @@ -7,23 +7,24 @@ CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() requires = [ - 'pyramid', + 'pyramid==1.4.3', 'SQLAlchemy', - 'transaction', 'pyramid_tm', 'pyramid_debugtoolbar', 'zope.sqlalchemy', 'waitress', - 'deform_bootstrap', - 'deform_bootstrap_extra', + 'deform<=1.999', + 'deform_bootstrap==0.2.8', + 'deform_bootstrap_extra==0.2.8', 'pyramid_persona', 'WebHelpers', 'pytz', + 'requests==1.1.0', ] setup( name='todopyramid', - version='1.0', + version='1.1', description='todopyramid', long_description=README + '\n\n' + CHANGES, classifiers=[ diff --git a/todopyramid/__init__.py b/todopyramid/__init__.py index d99e9b2..56026c5 100644 --- a/todopyramid/__init__.py +++ b/todopyramid/__init__.py @@ -6,6 +6,11 @@ Base, ) +from .views import get_user + +def get_db_session(request): + """return thread-local DB session""" + return DBSession def main(global_config, **settings): """ This function returns a Pyramid WSGI application. @@ -17,29 +22,41 @@ def main(global_config, **settings): settings=settings, root_factory='todopyramid.models.RootFactory', ) - config.include('pyramid_persona') - config.include('deform_bootstrap_extra') + + includeme(config) + + # scan modules for config descriptors + config.scan() + return config.make_wsgi_app() + + +def includeme(config): + """we use this concept to include routes and configuration setup in test cases + + http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/testing.html#creating-integration-tests + """ config.add_static_view('static', 'static', cache_max_age=3600) - # Adding the static resources from Deform - config.add_static_view( - 'deform_static', 'deform:static', cache_max_age=3600 - ) - config.add_static_view( - 'deform_bootstrap_static', 'deform_bootstrap:static', - cache_max_age=3600 - ) - config.add_static_view( - 'deform_bootstrap_extra_static', 'deform_bootstrap_extra:static', - cache_max_age=3600 - ) + # Misc. views config.add_route('home', '/') config.add_route('about', '/about') # Users config.add_route('account', '/account') # Viewing todo lists - config.add_route('list', '/list') + config.add_route('todos', '/todos') config.add_route('tags', '/tags') - config.add_route('tag', '/tags/{tag_name}') - config.scan() - return config.make_wsgi_app() + config.add_route('taglist', '/tags/{tag_name}') + # AJAX + config.add_route('todo', '/todos/{todo_id}') + config.add_route('delete.task', '/delete.task/{todo_id}') + config.add_route('tags.autocomplete', '/tags.autocomplete') + + # make DB session a request attribute + # http://blog.safaribooksonline.com/2014/01/07/building-pyramid-applications/ + config.add_request_method(get_db_session, 'db', reify=True) + + # Making A User Object Available as a Request Attribute + # http://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/auth/user_object.html + config.add_request_method(get_user, 'user', reify=True) + + diff --git a/todopyramid/grid.py b/todopyramid/grid.py index 858ea57..9d13734 100644 --- a/todopyramid/grid.py +++ b/todopyramid/grid.py @@ -95,11 +95,13 @@ def __html__(self): def tags_td(self, col_num, i, item): """Generate the column for the tags. + + Apply special tag CSS for currently selected tag matched route '/tags/{tag_name}' """ tag_links = [] for tag in item.sorted_tags: - tag_url = '%s/tags/%s' % (self.request.application_url, tag.name) + tag_url = self.request.route_url('taglist', tag_name=tag.name) tag_class = 'label' if self.selected_tag and tag.name == self.selected_tag: tag_class += ' label-warning' @@ -112,13 +114,15 @@ def tags_td(self, col_num, i, item): def due_date_td(self, col_num, i, item): """Generate the column for the due date. + + Time-Zone Localization is done in the model """ if item.due_date is None: return HTML.td('') span_class = 'due-date badge' if item.past_due: span_class += ' badge-important' - due_date = localize_datetime(item.due_date, self.user_tz) + due_date = item.due_date span = HTML.tag( "span", c=HTML.literal(due_date.strftime('%Y-%m-%d %H:%M:%S')), diff --git a/todopyramid/layouts.py b/todopyramid/layouts.py index 87d7162..519b800 100644 --- a/todopyramid/layouts.py +++ b/todopyramid/layouts.py @@ -1,14 +1,69 @@ from pyramid.renderers import get_renderer from pyramid.decorator import reify + +#ToDoPyramid currently highlights navbar item 'todos' for multiple routes +#Original version implemented navbar highlighting by setting a section variable +menu_items = [ + {'route': 'home', 'title': 'Home', 'routes': ['home']}, + {'route': 'todos', 'title': 'List', 'routes': ['todos', 'taglist']}, + {'route': 'tags', 'title': 'Tags', 'routes': ['tags']}, + {'route': 'account', 'title': 'Account', 'routes': ['account']}, + {'route': 'about', 'title': 'About', 'routes': ['about']} +] class Layouts(object): """This is the main layout for our application. This currently just sets up the global layout template. See the views module and their associated templates to see how this gets used. """ + + site_menu = menu_items + + def __init__(self, request): + """Set some common variables needed for each view. + """ + self.request = request + @reify def global_template(self): - renderer = get_renderer("templates/global_layout.pt") + renderer = get_renderer("todopyramid:templates/global_layout.pt") return renderer.implementation().macros['layout'] + + + @reify + def navbar(self): + """return navbar menu items and help to find current navbar item + + site menu concept inspired by + http://docs.pylonsproject.org/projects/pyramid-tutorials/en/latest/humans/creatingux/step07/index.html + + concept can be extended by components registering routes and adding their items to navbar menu + TodoPyramid adds Home, Todos, Tags, Account, About to this registry + When another TodoPyramid add-on component is activated by configuration it could add their menu item into this registry as well + + catching the matched route name inspired by + http://stackoverflow.com/questions/13552992/get-current-route-instead-of-route-path-in-pyramid + """ + def is_active_item(request, item): + """if we have a match between menu route and matched route set a boolean variable 'current' to true, else to false""" + if not request.matched_route: + item['active'] = False + return item + activate_item = True if request.matched_route.name in item['routes'] else False + item['active'] = activate_item + return item + + def menu_url(request, item): + item['url'] = request.route_url(item['route']) + return item + + def process_item(request, item): + item = menu_url(request, item) + item = is_active_item(request, item) + return item + + return [process_item(self.request, item) for item in self.site_menu] + + diff --git a/todopyramid/models.py b/todopyramid/models.py index ebbdcdf..394485e 100644 --- a/todopyramid/models.py +++ b/todopyramid/models.py @@ -15,15 +15,13 @@ from sqlalchemy.orm import sessionmaker from zope.sqlalchemy import ZopeTransactionExtension +from .utils import localize_datetime +from .utils import universify_datetime + DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) Base = declarative_base() -todoitemtag_table = Table( - 'todoitemtag', - Base.metadata, - Column('tag_id', Integer, ForeignKey('tags.name')), - Column('todo_id', Integer, ForeignKey('todoitems.id')), -) + class RootFactory(object): @@ -36,16 +34,12 @@ class RootFactory(object): def __init__(self, request): pass - -class Tag(Base): - """The Tag model is a many to many relationship to the TodoItem. - """ - __tablename__ = 'tags' - name = Column(Text, primary_key=True) - todoitem_id = Column(Integer, ForeignKey('todoitems.id')) - - def __init__(self, name): - self.name = name +todoitemtag_table = Table( + 'todoitemtags', + Base.metadata, + Column('tag_name', Integer, ForeignKey('tags.name')), + Column('todo_id', Integer, ForeignKey('todoitems.id')), +) class TodoItem(Base): @@ -55,14 +49,17 @@ class TodoItem(Base): __tablename__ = 'todoitems' id = Column(Integer, primary_key=True) task = Column(Text, nullable=False) - due_date = Column(DateTime) - user = Column(Integer, ForeignKey('users.email'), nullable=False) - tags = relationship(Tag, secondary=todoitemtag_table, lazy='dynamic') + _due_date = Column('due_date', DateTime) + user = Column(Integer, ForeignKey('users.email')) + author = relationship('TodoUser') + + # # many to many TodoItem<->Tag + tags = relationship("Tag", secondary=todoitemtag_table, backref="todos") def __init__(self, user, task, tags=None, due_date=None): self.user = user self.task = task - self.due_date = due_date + self.due_date = due_date # date will be universified if tags is not None: self.apply_tags(tags) @@ -71,13 +68,40 @@ def apply_tags(self, tags): creates the associated tag object. We strip off whitespace and lowercase the tags to keep a normalized list. """ + #tags = [] for tag_name in tags: - tag = tag_name.strip().lower() - self.tags.append(DBSession.merge(Tag(tag))) + tag_name = self.sanitize_tag(tag_name) + tag = self._find_or_create_tag(tag_name) + self.tags.append(tag) + + #reset collection of tags + #self.tags = tags + + def sanitize_tag(self, tag_name): + """tag name input validation""" + tag = tag_name.strip().lower() + return tag + + def _find_or_create_tag(self, tag_name): + """ensure tag names are unique + + http://stackoverflow.com/questions/2310153/inserting-data-in-many-to-many-relationship-in-sqlalchemy + + why we need that - prevent multiple tags + http://stackoverflow.com/questions/13149829/many-to-many-in-sqlalchemy-preventing-sqlalchemy-from-inserting-into-a-table-if + """ + q = DBSession.query(Tag).filter_by(name=tag_name) + t = q.first() + if not(t): + t = Tag(tag_name) + return t @property def sorted_tags(self): """Return a list of sorted tags for this task. + + TODO: we can apply sorting using the relationship + TODO: order case-insensitive ??? """ return sorted(self.tags, key=lambda x: x.name) @@ -85,8 +109,50 @@ def sorted_tags(self): def past_due(self): """Determine if this task is past its due date. Notice that we compare to `utcnow` since dates are stored in UTC. + + TODO: write tests """ - return self.due_date and self.due_date < datetime.utcnow() + return self._due_date and self._due_date < datetime.utcnow() + + def universify_due_date(self, date): + """convert datetime to UTC for storage""" + if date is not None: + self._due_date = universify_datetime(date) + + def localize_due_date(self): + """create a timezone-aware object for a given datetime and timezone name + """ + if self._due_date is not None and hasattr(self.author, 'time_zone'): + due_dt = localize_datetime(self._due_date, self.author.time_zone) + return due_dt + return self._due_date + + due_date = property(localize_due_date, universify_due_date) + + def __repr__(self): + """return representation - helps in IPython""" + return "TodoItem(%r, %r, %r, %r)" % (self.user, self.task, self.tags, self.due_date) + + +class Tag(Base): + """The Tag model is a many to many relationship to the TodoItem. + + http://docs.sqlalchemy.org/en/rel_0_9/orm/tutorial.html#building-a-many-to-many-relationship + """ + __tablename__ = 'tags' + + #id = Column(Integer, primary_key=True) + #name = Column(Text, nullable=False, unique=True) + + name = Column(Text, primary_key=True) + + + def __init__(self, name): + self.name = name + + def __repr__(self): + """return representation - helps in IPython""" + return "Tag(%r)" % (self.name) class TodoUser(Base): @@ -99,6 +165,7 @@ class TodoUser(Base): first_name = Column(Text) last_name = Column(Text) time_zone = Column(Text) + todos = relationship(TodoItem) todo_list = relationship(TodoItem, lazy='dynamic') def __init__(self, email, first_name=None, last_name=None, @@ -108,16 +175,61 @@ def __init__(self, email, first_name=None, last_name=None, self.last_name = last_name self.time_zone = time_zone + + def todos_by_tag(self, tag, order): + """return user todos with given tag""" + tag_filter = TodoItem.tags.any(name=tag) + qry = self.todo_list.filter(tag_filter) + + if order: + qry.order_by(order) + + return qry.all() + + @property def user_tags(self): """Find all tags a user has created + + BUG: does not find user created tags that actually have no related todos + + returns KeyedTuples with key 'tag_name' + TODO: refactor to return collection of Tag model - consider lazy + + explore code samples - we also have user/author model and a many-to-many relationship between todo and tag + http://docs.sqlalchemy.org/en/rel_0_9/orm/tutorial.html#building-a-many-to-many-relationship """ - qry = self.todo_list.session.query(todoitemtag_table.columns['tag_id']) + qry = self.todo_list.session.query(todoitemtag_table.columns['tag_name']) qry = qry.join(TodoItem).filter_by(user=self.email) - qry = qry.group_by('tag_id') - qry = qry.order_by('tag_id') + qry = qry.group_by('tag_name') + qry = qry.order_by('tag_name') return qry.all() + def user_tags_autocomplete(self, term): + """given a term return a unique collection (set) of user tags that start with it + + BUG: returns only tags currently applied to user todos, therefore missing tags related to deleted user todos + + + In [19]: for todo in user.todos: + for tag in todo.tags: + if tag.name.startswith('ber'): + ....: print tag.name + ....: + berlin + berlin + berlin + berlin + """ + matching_tags = set() + for todo in self.todos: + for tag in todo.tags: + if tag.name.startswith(term): + matching_tags.add(tag) + + return matching_tags + + @property def profile_complete(self): """A check to see if the user has completed their profile. If @@ -125,3 +237,39 @@ def profile_complete(self): settings. """ return self.first_name and self.last_name + + def delete_todo(self, todo_id): + """given a todo ID we delete it is contained in user todos + + delete from a collection + http://docs.sqlalchemy.org/en/latest/orm/session.html#deleting-from-collections + http://stackoverflow.com/questions/10378468/deleting-an-object-from-collection-in-sqlalchemy""" + todo_item = self.todo_list.filter( + TodoItem.id == todo_id) + + todo_item.delete() + + def create_todo(self, task, tags=None, due_date=None): + """may be we prefer using this method from authenticated views + this way we always create a user TodoItem instead of allowing view code to modify SQLAlchemy TodoItem collection + """ + #check common pitfall - mutable as default argument + if tags==None: + tags = [] + + todo = TodoItem(self.email, task, tags, due_date) + self.todos.append(todo) + + def edit_todo(self, todo_id, task, tags=None, due_date=None): + todo = self.todo_list.filter_by(id=todo_id).one() + todo.task = task + todo.apply_tags(tags) + todo.due_date = due_date + + def update_prefs(self, first_name, last_name, time_zone=u'US/Eastern'): + """update account preferences""" + self.first_name = first_name + self.last_name = last_name + self.time_zone = time_zone + + diff --git a/todopyramid/schema.py b/todopyramid/schema.py index 0da654b..d088516 100644 --- a/todopyramid/schema.py +++ b/todopyramid/schema.py @@ -38,6 +38,8 @@ def deferred_datetime_node(node, kw): class TodoSchema(MappingSchema): """This is the form schema used for list_view and tag_view. This is the basis for the add and edit form for tasks. + + TODO: schema.TodoSchema.name != models.TodoItem.task """ id = SchemaNode( Integer(), @@ -53,7 +55,7 @@ class TodoSchema(MappingSchema): description=( "Enter a comma after each tag to add it. Backspace to delete." ), - missing=[], + missing=None, ) due_date = SchemaNode( deferred_datetime_node, diff --git a/todopyramid/scripts/initializedb.py b/todopyramid/scripts/initializedb.py index e258a1e..fb242b6 100644 --- a/todopyramid/scripts/initializedb.py +++ b/todopyramid/scripts/initializedb.py @@ -18,6 +18,7 @@ Base, ) +from ..utils import localize_datetime def usage(argv): cmd = os.path.basename(argv[0]) @@ -26,42 +27,67 @@ def usage(argv): sys.exit(1) -def create_dummy_content(user_id): - """Create some tasks by default to show off the site +def create_dummy_user(): + """create our dummy user + + we handle transaction not here - design decision """ + + user = TodoUser( + email=u'king.arthur@example.com', + first_name=u'Arthur', + last_name=u'Pendragon', + ) + DBSession.add(user) + user_id = user.email + return user_id + +def create_dummy_content(user_id): + """Create some tasks for this user with by default to show off the site + + either called during application startup or during content creation while a new user registers + we do not handle transaction here - design decision + + TODO: bulk adding of new content + """ + + user = DBSession.query(TodoUser).filter(TodoUser.email == user_id).first() + time_zone = user.time_zone + + #this user creates several todo items with localized times task = TodoItem( user=user_id, task=u'Find a shrubbery', tags=[u'quest', u'ni', u'knight'], - due_date=datetime.utcnow() + timedelta(days=60), + due_date=localize_datetime((datetime.utcnow() + timedelta(days=60)), time_zone), ) DBSession.add(task) task = TodoItem( user=user_id, task=u'Search for the holy grail', tags=[u'quest'], - due_date=datetime.utcnow() - timedelta(days=1), + due_date=localize_datetime((datetime.utcnow() + timedelta(days=1)), time_zone), ) DBSession.add(task) task = TodoItem( user=user_id, task=u'Recruit Knights of the Round Table', tags=[u'quest', u'knight', u'discuss'], - due_date=datetime.utcnow() + timedelta(minutes=45), + due_date=localize_datetime((datetime.utcnow() + timedelta(minutes=45)), time_zone), ) DBSession.add(task) task = TodoItem( user=user_id, task=u'Build a Trojan Rabbit', tags=[u'quest', u'rabbit'], - due_date=datetime.utcnow() + timedelta(days=1), + due_date=localize_datetime((datetime.utcnow() + timedelta(days=1)), time_zone), ) DBSession.add(task) task = TodoItem( user=user_id, task=u'Talk to Tim the Enchanter', tags=[u'quest', u'discuss'], - due_date=datetime.utcnow() + timedelta(days=90), + due_date=localize_datetime((datetime.utcnow() + timedelta(days=90)), time_zone), ) DBSession.add(task) task = TodoItem( @@ -90,10 +116,7 @@ def main(argv=sys.argv): DBSession.configure(bind=engine) Base.metadata.create_all(engine) with transaction.manager: - user = TodoUser( - email=u'king.arthur@example.com', - first_name=u'Arthur', - last_name=u'Pendragon', - ) - DBSession.add(user) - create_dummy_content(u'king.arthur@example.com') + user_id = create_dummy_user() + create_dummy_content(user_id) + + diff --git a/todopyramid/static/todo_list.js b/todopyramid/static/todo_list.js index f0472b9..da21ea0 100644 --- a/todopyramid/static/todo_list.js +++ b/todopyramid/static/todo_list.js @@ -12,11 +12,10 @@ $(function() { e.preventDefault(); var todo_id = $(this).closest('ul').attr('id'); $.getJSON( - '/edit.task', - {'id': todo_id}, + '/todos/' + todo_id, function(json) { if (json) { - edit_form = $('#task-form'); + var edit_form = $('#task-form'); // Set the title to Edit edit_form.find('h3').text('Edit Task'); $.each(json, function(k, v) { @@ -47,7 +46,7 @@ $(function() { }); }); - // Compete a todo task when the link is clicked + // Complete a todo task when the link is clicked $("a.todo-complete").click(function(e) { e.preventDefault(); var todo_id = $(this).closest('ul').attr('id'); @@ -57,8 +56,7 @@ $(function() { bootbox.confirm(confirm_text, function(complete_item) { if (complete_item) { $.getJSON( - '/delete.task', - {'id': todo_id}, + '/delete.task/'+ todo_id, function(json) { if (json) { // Delete the row diff --git a/todopyramid/templates/global_layout.pt b/todopyramid/templates/global_layout.pt index fe61a7b..b186fee 100644 --- a/todopyramid/templates/global_layout.pt +++ b/todopyramid/templates/global_layout.pt @@ -13,7 +13,7 @@ + href="${css_path}"/> + + + + + - - - - - - - - - - - - -