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
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.
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
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."""
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."""
# 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."""
# XXX how do we do success messages in some useful way?
# XXX send me where I came from
- redirect_to('/')
+ redirect('/')
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):
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)
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))
--- /dev/null
+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()
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):
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
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.
# 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
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))
+
# 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 *
+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()
# 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')
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):
def set_file(self, file):
self.hash = save_file("art", file)
+ self.original_filename = file.filename
file = property(get_path, set_file)
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
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
# 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
.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 */
<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>
<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()}
<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()}">
<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
<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">
</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>
<%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)}
--- /dev/null
+<%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
<%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
--- /dev/null
+from floof.tests import *
+
+class TestGalleryController(TestController):
+
+ def test_index(self):
+ response = self.app.get(url(controller='gallery', action='index'))
+ # Test response...
--- 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