merged in my branch 'resources', which is not aptly named anymore since it no longer...
authorNick Retallack <nickretallack@gmil.com>
Wed, 7 Oct 2009 06:25:16 +0000 (23:25 -0700)
committerNick Retallack <nickretallack@gmil.com>
Wed, 7 Oct 2009 06:25:16 +0000 (23:25 -0700)
21 files changed:
floof/config/routing.py
floof/controllers/account.py
floof/controllers/art.py
floof/controllers/gallery.py [new file with mode: 0644]
floof/controllers/search.py
floof/controllers/tag.py
floof/lib/helpers.py
floof/lib/search.py
floof/model/art.py
floof/model/search.py
floof/model/users.py
floof/public/layout.css
floof/templates/account/register.mako
floof/templates/art/new.mako
floof/templates/art/show.mako
floof/templates/base.mako
floof/templates/index.mako
floof/templates/macros.mako [new file with mode: 0644]
floof/templates/users/view.mako
floof/tests/functional/test_gallery.py [new file with mode: 0644]
floof/todo.txt

index 6ec64e4..c9b4975 100644 (file)
@@ -12,6 +12,7 @@ def make_map():
     map = Mapper(directory=config['pylons.paths']['controllers'],
                  always_scan=config['debug'], explicit=True)
     map.minimization = False
+
     # explicit = True disables a broken feature called "route memory",
     # where it adds everything matched in the current request as default variables
     # for the next one.  This is wrong because it doesn't invalidate things lower down in
@@ -20,34 +21,64 @@ def make_map():
 
     require_POST = dict(conditions={'method': ['POST']})
 
+    # get rid of trailing slashes
+    map.redirect('/*(url)/', '/{url}',
+                 _redirect_code='301 Moved Permanently')
+
+
     # The ErrorController route (handles 404/500 error pages); it should
     # likely stay at the top, ensuring it can always be resolved
     map.connect('/error/{action}', controller='error')
     map.connect('/error/{action}/{id}', controller='error')
 
-    map.connect('/', controller='main', action='index')
+    map.connect('home', '/', controller='main', action='index')
 
-    # User stuff
-    map.connect('/account/login', controller='account', action='login')
-    map.connect('/account/login_begin', controller='account', action='login_begin', **require_POST)
-    map.connect('/account/login_finish', controller='account', action='login_finish')
-    map.connect('/account/logout', controller='account', action='logout', **require_POST)
-    map.connect('/account/register', controller='account', action='register')
-    map.connect('/account/register_finish', controller='account', action='register_finish', **require_POST)
+    # Account stuff
+    with map.submapper(controller="account") as sub:
+        sub.connect('login',            '/account/login',           action='login')
+        sub.connect('login_begin',      '/account/login_begin',     action='login_begin', **require_POST)
+        sub.connect('login_finish',     '/account/login_finish',    action='login_finish')
+        sub.connect('logout',           '/account/logout',          action='logout', **require_POST)
+        sub.connect('register',         '/account/register',        action='register')
+        sub.connect('register_finish',  '/account/register_finish', action='register_finish', **require_POST)
 
+    # with map.submapper()
     map.connect('/users', controller='users', action='list')
-    map.connect('/users/{name}', controller='users', action='view')
+    map.connect('user_page', '/users/{name}', controller='users', action='view')
+
+
+    with map.submapper(controller="art") as sub:
+        sub.connect('new_art',      '/art/new',         action="new")
+        sub.connect('create_art',   '/art/create',      action="create")
+        sub.connect('rate_art',     '/art/{id}/rate',   action="rate")
+        sub.connect('show_art',     '/art/{id}',        action="show")
+        
+    with map.submapper(controller='tag') as sub:
+        sub.connect('delete_tag', '/art/{art_id}/tag/{id}')
+        sub.connect('create_tag', '/art/{art_id}/tag')
 
-    # Art stuff
-    map.connect('/art/new', controller='art', action='new')
-    map.connect('/art/upload', controller='art', action='upload')
-    map.connect('show_art', '/art/{id}', controller='art', action='show')
-    map.connect('/art/{id}/tag', controller='art', action='tag')
+    map.resource('tag','tags', controller="tag",
+        parent_resource=dict(member_name='art', collection_name='art'))
+        # Yeah, parent resources are specified kinda dumb-ly.  Would be better if you could pass in the
+        # real parent resource instead of mocking it up with a silly dict.  We should file a feature request.
+        
+    # I think resources is the right way to go for most things.  It ensures all of our actions have the right
+    # methods on them, at least.  It does require the use of silly _method="delete" post parameters though.
+    
+    # One sticking point though is, it'll happily allow you to add any formatting string you want, like art/1.json
+    # I wonder if there's a way to place requirements on that, or disable it until we actually have formats.
+    # It just serves the same action as usual but with a format argument in the context.
+            
+    # map.connect('/art/new', controller='art', action='new')
+    # map.connect('/art/upload', controller='art', action='upload')
+    # map.connect('show_art', '/art/{id}', controller='art', action='show')
+    # map.connect('/art/{id}/tag', controller='art', action='tag')
 
-    map.connect('/tag/{id}/delete', controller='tag', action='delete')
+    map.connect('/tag/{id}/delete', controller='tag', action='delete')
 
     map.connect('search', '/search', controller='search', action='index')
-    map.connect('/search/list', controller='search', action='list')
+    # map.connect( '/search/{query}', controller='search', action='index')
+    map.connect('saved_searches', '/search/list', controller='search', action='list')
 
 
     # default routing is back so we can test stuff.
index e4f39f3..7dabaa6 100644 (file)
@@ -5,8 +5,8 @@ from openid.extensions.sreg import SRegRequest, SRegResponse
 from openid.store.filestore import FileOpenIDStore
 from sqlalchemy.orm.exc import NoResultFound
 
-from pylons import request, response, session, tmpl_context as c, url
-from pylons.controllers.util import abort, redirect_to
+from pylons import request, response, session, tmpl_context as c, url, h
+from pylons.controllers.util import abort, redirect
 from routes import url_for, request_config
 
 from floof.lib.base import BaseController, render
@@ -35,7 +35,7 @@ class AccountController(BaseController):
         return_url = url_for(host=host, controller='account', action='login_finish')
         new_url = auth_request.redirectURL(return_to=return_url,
                                            realm=protocol + '://' + host)
-        redirect_to(new_url)
+        redirect(new_url)
 
     def login_finish(self):
         """Step two of logging in; the OpenID provider redirects back here."""
@@ -63,14 +63,14 @@ class AccountController(BaseController):
                 session['register:nickname'] = sreg_res['nickname']
 
             session.save()
-            redirect_to(url.current(action='register'))
+            redirect(url('register'))
 
         # Remember who's logged in, and we're good to go
         session['user_id'] = user.id
         session.save()
 
         # XXX send me where I came from
-        redirect_to('/')
+        redirect('/')
 
     def logout(self):
         """Log user out."""
@@ -81,7 +81,7 @@ class AccountController(BaseController):
 
         # XXX success message
         # XXX send me where I came from
-        redirect_to('/')
+        redirect('/')
 
     def register(self):
         """Logging in with an unrecognized identity URL redirects here."""
@@ -116,4 +116,4 @@ class AccountController(BaseController):
 
         # XXX how do we do success messages in some useful way?
         # XXX send me where I came from
-        redirect_to('/')
+        redirect('/')
index a0aecd7..5186daa 100644 (file)
@@ -1,14 +1,17 @@
 import logging
 
 from pylons import request, response, session, tmpl_context as c, h
-from pylons.controllers.util import abort, redirect_to
-
+from pylons.controllers.util import abort, redirect
+from pylons import url
 from floof.lib.base import BaseController, render
 
 log = logging.getLogger(__name__)
 
 import elixir
-from floof.model.art import Art
+from floof.model.art import Art, Rating
+
+from sqlalchemy.exceptions import IntegrityError
+
 
 class ArtController(BaseController):
     def __before__(self, id=None):
@@ -26,10 +29,20 @@ class ArtController(BaseController):
         return render("/art/new.mako")
 
     # TODO: login required
-    def upload(self):
-        Art(uploaded_by=c.user, **request.params)
-        elixir.session.commit()
-        redirect_to(controller="main", action="index")
+    def create(self):
+        c.art = Art(uploader=c.user, **request.params)
+
+        try:
+            elixir.session.commit()
+            redirect(url('show_art', id=c.art.id))
+        except IntegrityError:
+            # hurr, there must be a better way to do this but I am lazy right now
+            hash = c.art.hash
+            elixir.session.rollback()
+            duplicate_art = Art.get_by(hash=hash)
+            h.flash("We already have that one.")
+            redirect(url('show_art', id=duplicate_art.id))
+
 
     def show(self, id):
         # c.art = h.get_object_or_404(Art, id=id)
@@ -37,17 +50,17 @@ class ArtController(BaseController):
             c.your_score = c.art.user_score(c.user)
         return render("/art/show.mako")
         
-    # TODO: login required
-    # also, require post
-    def tag(self, id):
-        # c.art = h.get_object_or_404(Art, id=id)
-        c.art.add_tags(request.params.get("tags",""), c.user)
-        elixir.session.commit()
-        redirect_to('show_art', id=c.art.id)
-    
+
     # TODO: login required
     def rate(self, id):
         # c.art = h.get_object_or_404(Art, id=id)
-        c.art.rate(request.params["score"], c.user)
+        score = request.params.get("score")
+        if score and score.isnumeric():
+            score = int(score)
+        else:
+            score = Rating.reverse_options.get(score)
+        
+        c.art.rate(score, c.user)
         elixir.session.commit()
-        redirect_to('show_art', id=c.art.id)
+            
+        redirect(url('show_art', id=c.art.id))
diff --git a/floof/controllers/gallery.py b/floof/controllers/gallery.py
new file mode 100644 (file)
index 0000000..e923e30
--- /dev/null
@@ -0,0 +1,17 @@
+import logging
+
+from pylons import request, response, session, tmpl_context as c, h
+from pylons.controllers.util import abort, redirect
+from pylons import url
+
+from floof.lib.base import BaseController, render
+
+log = logging.getLogger(__name__)
+import elixir
+from floof.model.search import GalleryWidget
+class GalleryController(BaseController):
+
+    def delete(self, id):
+        c.gallery = h.get_object_or_404(GalleryWidget, id=id)
+        elixir.session.delete(tag)
+        elixir.session.commit()
index 3b6faef..57ff4d2 100644 (file)
@@ -1,14 +1,16 @@
 import logging
 
 from pylons import request, response, session, tmpl_context as c, h
-from pylons.controllers.util import abort, redirect_to
+from pylons.controllers.util import abort, redirect
+from pylons import url
 
 from floof.lib.base import BaseController, render
+from floof.lib.search import do_search
 
 log = logging.getLogger(__name__)
 
 from floof.model.art import Art, Tag, TagText
-from floof.model.search import SavedSearch
+from floof.model.search import SavedSearch, GalleryWidget
 import elixir
 
 class SearchController(BaseController):
@@ -18,16 +20,7 @@ class SearchController(BaseController):
             return self.save()
         
         c.query = request.params.get('query', '')
-        tags = c.query.split()
-        
-        tagtexts = TagText.query.filter(TagText.text.in_(tags))
-        tagtext_ids = [_.id for _ in tagtexts]
-
-        # Fetch art that has all the tags
-        c.artwork = Art.query.join(Tag) \
-                       .filter(Tag.tagtext_id.in_(tagtext_ids)) \
-                       .all()
-
+        c.artwork = do_search(c.query)
         return render('/index.mako')
         
     # TODO: login required
@@ -35,7 +28,7 @@ class SearchController(BaseController):
         c.query = request.params.get('query', '')
         saved_search = SavedSearch(author=c.user, string=c.query)
         elixir.session.commit()
-        redirect_to(action="list")
+        redirect(url('saved_searches'))
         # TODO: do something better than this.
         
     
@@ -47,9 +40,9 @@ class SearchController(BaseController):
     # TODO: login required
     def display(self, id):
         c.search = h.get_object_or_404(SavedSearch, id=id)
-        # TODO: create a gallery widget
-        
-        redirect_to(controller="users", action="view", name=c.user.name)
+        c.gallery = GalleryWidget(search=c.search, page=c.user.primary_page)
+        elixir.session.commit()
+        redirect(url(controller="users", action="view", name=c.user.name))
         
         
         
\ No newline at end of file
index 3b69ed6..874e605 100644 (file)
@@ -1,20 +1,29 @@
 import logging
 
-from pylons import request, response, session, tmpl_context as c
-from pylons.controllers.util import abort, redirect_to
+from pylons import request, response, session, tmpl_context as c, h
+from pylons.controllers.util import abort, redirect
 
 from floof.lib.base import BaseController, render
+from pylons import url
 
 log = logging.getLogger(__name__)
 
 import elixir
-from floof.model.art import Tag
+from floof.model.art import Art, Tag
 
 class TagController(BaseController):
 
-    def delete(self, id):
-        tag = Tag.get(id)
-        if tag:
-            elixir.session.delete(tag)
-            elixir.session.commit()
-        redirect_to(request.referrer)
\ No newline at end of file
+    # TODO: login required
+    def delete(self, art_id, id):
+        tag = h.get_object_or_404(Tag, id=id)
+        elixir.session.delete(tag)
+        elixir.session.commit()
+        redirect(url('show_art', id=art_id))
+        
+    # TODO: login required
+    def create(self, art_id):
+        c.art = h.get_object_or_404(Art, id=art_id)
+        c.art.add_tags(request.params.get("tags",""), c.user)
+        elixir.session.commit()
+        redirect(url('show_art', id=c.art.id))
+
index 28e9176..c8a1251 100644 (file)
@@ -8,6 +8,7 @@ available to Controllers. This module is available to both as 'h'.
 # from webhelpers.html.tags import checkbox, password
 from webhelpers import *
 from routes import url_for, redirect_to
+from pylons import url
 
 # Scaffolding helper imports
 from webhelpers.html.tags import *
index 3acb248..f7ce65b 100644 (file)
@@ -1,3 +1,23 @@
+from floof.model.art import Art, Tag, TagText
+
+def do_search(query):
+    tags = query.split()
+
+    tagtexts = TagText.query.filter(TagText.text.in_(tags))
+    tagtext_ids = [_.id for _ in tagtexts]
+
+    # Fetch art that has all the tags
+    artwork = Art.query.join(Tag) \
+                   .filter(Tag.tagtext_id.in_(tagtext_ids)) \
+                   .all()
+    return artwork
+
+
+
+
+
+
+# unfinished stuff
 def parse(query):
     words = query.split()
 
@@ -18,16 +38,6 @@ def parse(query):
                 # TODO: Find stuff that has this rating
                 # Rating.query.filter(Rating.s)
 
-
-
-
     tagtexts = TagText.query.filter(TagText.text.in_(tags))
     tagtext_ids = map(lambda x:x.id, tagtexts)
 
-    # TODO: this is wrong.  Please fix it so it returns art that has all the tags.
-    art_tag_pairs = elixir.session.query(Art,Tag).filter(Art.id == Tag.art_id).\
-        filter(Tag.tagtext_id.in_(tagtext_ids)).all()
-
-    # just the art please.
-    c.artwork = map(lambda x: x[0], art_tag_pairs)
-    return render('/index.mako')
index c3fba3a..b709610 100644 (file)
@@ -16,9 +16,9 @@ from floof.lib.dbhelpers import find_or_create, update_or_create
 class Art(Entity):
     title = Field(Unicode(120))
     original_filename = Field(Unicode(120))
-    hash = Field(String)
+    hash = Field(String, unique=True, required=True)
 
-    uploader = ManyToOne('User')
+    uploader = ManyToOne('User', required=True)
     tags = OneToMany('Tag')
 
     # def __init__(self, **kwargs):
@@ -33,6 +33,7 @@ class Art(Entity):
 
     def set_file(self, file):
         self.hash = save_file("art", file)
+        self.original_filename = file.filename
 
     file = property(get_path, set_file)
 
@@ -109,6 +110,9 @@ class Rating(Entity):
     art = ManyToOne('Art', ondelete='cascade')
     rater = ManyToOne('User', ondelete='cascade')
     score = Field(Integer)
+    
+    # @score.setter
+    # def score(self, value):    
         
     options = {-1:"sucks", 0:"undecided", 1:"good", 2:"great"}
     default = 0
index 011468c..be2bb9e 100644 (file)
@@ -1,30 +1,41 @@
 from elixir import *
-from users import User
+# from users import User
+
+from floof.lib.search import do_search
 
 class SavedSearch(Entity):
     string = Field(Unicode) # I tried calling this query, but it broke elixir
-    author = ManyToOne(User)
+    author = ManyToOne('User')
     
     def __unicode__(self):
         return self.string
+        
+    @property
+    def results(self):
+        return do_search(self.string)
+
 
 
 class GalleryWidget(Entity):
+    page = ManyToOne('UserPage')
     search = ManyToOne(SavedSearch)
-    displayer = ManyToOne(User) # determines whose page should it should show up on
-                                # Could be no-ones, if it's just a template.
+
+    # NOTE: no longer needed now that we have pages, I guess.
+    # displayer = ManyToOne('User') # determines whose page should it should show up on
+    #                             # Could be no-ones, if it's just a template.
     
     # Needs some fields for position on your page
 
     @property
-    def query(self):
-        return self.search.query
+    def string(self):
+        return self.search
     
-    @query.setter
-    def query(self, value):
+    @string.setter
+    def string(self, value):
         # TODO: should we delete the possibly orphaned saved search?
-        if not self.displayer:
-            # TODO: may have to refactor this into an init if the key ordering is inconvenienc
-            raise "Oh no!  This gallery needs a displayer to set on the saved search."
+        if not self.displayer:
+            # TODO: may have to refactor this into an init if the key ordering is inconvenienc
+            raise "Oh no!  This gallery needs a displayer to set on the saved search."
         
-        self.search = SavedSearch(author=self.displayer, query=value)
\ No newline at end of file
+        self.search = SavedSearch(author=getattr(self,"author",None), string=value)
+        
\ No newline at end of file
index 5c9783e..86c31d8 100644 (file)
@@ -6,17 +6,51 @@
 
 # from elixir import Entity, Field, Unicode, belongs_to, has_many
 from elixir import *
+from search import GalleryWidget
 
 class User(Entity):
     name = Field(Unicode(20))
     uploads = OneToMany('Art')
     has_many('identity_urls', of_kind='IdentityURL')
     searches = OneToMany('SavedSearch')
+    # galleries = OneToMany('GalleryWidget')
+    pages = OneToMany('UserPage', inverse="owner")
+    primary_page = OneToOne('UserPage', inverse="owner")
+
     
     def __unicode__(self):
         return self.name
 
+    def __init__(self, **kwargs):
+        super(User, self).__init__(**kwargs)
+        
+        
+        
+        # TODO: have this clone a standard starter page
+        self.primary_page = UserPage(owner=self, title="default", visible=True)
+        
+        # a starter gallery, just for fun
+        gallery = GalleryWidget(owner=self, string="awesome")
+        self.primary_page.galleries.append(gallery)
+
+
 class IdentityURL(Entity):
     url = Field(Unicode(255))
     belongs_to('user', of_kind='User')
 
+
+
+class UserPage(Entity):
+    """A user can have multiple pages, though by default they only have one visible.
+    This is so that they can keep some nice themed pages lying around for special occasions.
+    Page templates that provide familiar interfaces will also be UserPage records.  Users will
+    see a panel full of them, and they can choose to clone those template pages to their own page list.
+    If more than one is set to visible, there would be tabs.
+    
+     """
+    
+    owner = ManyToOne('User', inverse="pages")
+    title = Field(String)
+    
+    visible = Field(Boolean)
+    galleries = OneToMany('GalleryWidget')
\ No newline at end of file
index 25737d0..a098b5e 100644 (file)
@@ -10,6 +10,9 @@ body { font-family: sans-serif; font-size: 12px; }
 
 .full {display:block;}
 
+
+.artwork-grid li {display:inline;}
+
 /*** Common bits and pieces ***/
 /* General form layout */
 a {color:blue; text-decoration:none; pointer:cursor;} /* Who needs visited links */
index 485012c..2de23a4 100644 (file)
@@ -2,7 +2,7 @@
 
 <p>Registering from <strong>${c.identity_url}</strong>.</p>
 
-${h.form(url.current(action='register_finish'), method='POST')}
+${h.form(url('register_finish'), method='POST')}
 <dl class="form">
     <dt>Username</dt>
     <dd>${h.text('username', value=c.username)}</dd>
index e2c00fb..3eda527 100644 (file)
@@ -3,7 +3,7 @@
 <h1>Add New Art</h1>
 <p>Now: Upload a file.  Later: Supply a link?  Not exclusive to uploading.</p>
 
-${h.form(h.url_for(controller='art', action='upload'), multipart=True)}
+${h.form(h.url('create_art'), multipart=True)}
 ${h.file('file')}
 ${h.submit(None, 'Upload!')}
 ${h.end_form()}
index 715b9d1..262f1cb 100644 (file)
@@ -5,24 +5,30 @@
 <h1>Viewing Art</h1>
 
 % if c.user:
-${h.form (h.url_for (controller='art', action='tag', id=c.art.id), multipart=True)}
+${h.form (h.url("art_tags", art_id=c.art.id))}
 Add Some Tags: ${h.text('tags')}
 ${h.submit('submit', 'Tag!')}
 ${h.end_form()}
 
 % for tag in c.art.tags:
-<a href="${url(controller='tag', action='delete', id=tag.id)}">x</a>
+${h.form(h.url("art_tag", art_id=c.art.id, id=tag.id), method="delete")}
+${h.submit('delete', 'X')}
 <a href="${url(controller='search', action='index', query=tag)}">${tag}</a>
+${h.end_form()}
 % endfor
 
 <h2>What do you think?</h2>
+${h.form (h.url("rate_art", id=c.art.id), method="put")}
 % for score,text in sorted(Rating.options.items()):
-<a href="${h.url_for(controller='art', action='rate', id=c.art.id)}?score=${score}" \
+
 % if c.your_score == score:
-class="selected" \
+${h.submit('score', text, class_="selected")}
+% else:
+${h.submit('score', text)}
 % endif
->${text}</a> 
+
 % endfor
+${h.end_form()}
 % endif
 
 <img class="full" src="${c.art.get_path()}">
index 654b8b3..73f069c 100644 (file)
@@ -11,7 +11,7 @@
 <a href="${h.url_for("/")}">Home</a>
 
 % if c.user:
-| <a href="${h.url_for(controller="art", action="new")}">Add Art</a>
+| <a href="${h.url("new_art")}">Add Art</a>
 | <a href="${h.url_for(controller="search", action="list")}">Your Searches</a>
 ## | <a href="${h.url_for("/users/"+c.user}">Your Page</a>
 % endif
@@ -32,7 +32,7 @@ ${h.end_form()}
     <div id="user">
         % if c.user:
         <form action="${url(controller='account', action='logout')}" method="POST">
-        <p>Logged in as ${c.user.name}.  ${h.submit(None, 'Log out')}</p>
+        <p>Logged in as <a href="${h.url('user_page', name=c.user.name)}">${c.user.name}</a>.  ${h.submit(None, 'Log out')}</p>
         </form>
         % else:
         <form action="${url(controller='account', action='login_begin')}" method="POST">
@@ -46,6 +46,17 @@ ${h.end_form()}
 
 
 </div>
+
+<% messages = h.flash.pop_messages() %>
+% if messages:
+<ul id="flash-messages">
+    % for message in messages:
+    <li>${message}</li>
+    % endfor
+</ul>
+% endif
+
+
 <div id="body">
 ${next.body()}
 </div>
index af95f7a..a774551 100644 (file)
@@ -1,10 +1,4 @@
 <%inherit file="base.mako" />
+<%namespace name="macros" file="/macros.mako" />
 
-
-<ul class="artwork-grid">
-    % for artwork in c.artwork:
-    <li><a href="${h.url_for(controller="art", action="show", id=artwork.id)}">
-        <img width="180" src="${artwork.get_path()}">
-    </a></li>
-    % endfor
-</ul>
+${macros.thumbs(c.artwork)}
diff --git a/floof/templates/macros.mako b/floof/templates/macros.mako
new file mode 100644 (file)
index 0000000..061b7b9
--- /dev/null
@@ -0,0 +1,11 @@
+<%def name="thumbs(art)">
+    <ul class="artwork-grid">
+        % for item in art:
+            <li>
+                <a href="${h.url("show_art", id=item.id)}">
+                    <img width="180" src="${item.get_path()}">
+                </a>
+            </li>
+        % endfor
+    </ul>
+</%def>
\ No newline at end of file
index 9f39bae..f9a9329 100644 (file)
@@ -1,3 +1,9 @@
 <%inherit file="/base.mako" />
+<%namespace name="macros" file="/macros.mako" />
 
 <p>This is the userpage for ${c.this_user.name}.</p>
+
+% for gallery in c.this_user.primary_page.galleries:
+<h2>${gallery.string}</h2>
+${macros.thumbs(gallery.search.results)}
+% endfor
\ No newline at end of file
diff --git a/floof/tests/functional/test_gallery.py b/floof/tests/functional/test_gallery.py
new file mode 100644 (file)
index 0000000..695513c
--- /dev/null
@@ -0,0 +1,7 @@
+from floof.tests import *
+
+class TestGalleryController(TestController):
+
+    def test_index(self):
+        response = self.app.get(url(controller='gallery', action='index'))
+        # Test response...
index f9a7cd0..85b3761 100644 (file)
@@ -1 +1,5 @@
--- uploading files
\ No newline at end of file
+- new art:
+    if hash exists, do not create another record.
+    
+- search syntax:
+    railroad it
\ No newline at end of file