From: Nick Retallack Date: Wed, 7 Oct 2009 06:25:16 +0000 (-0700) Subject: merged in my branch 'resources', which is not aptly named anymore since it no longer... X-Git-Url: http://git.veekun.com/zzz-floof.git/commitdiff_plain/366dedf71b3cdd775251c7cea4b1519b44b37164?hp=bedce37c00ee72650787b590e0e549bf6971f2e3 merged in my branch 'resources', which is not aptly named anymore since it no longer uses resources at all. Just named routes. It has new features like galleries though so it's cool. --- diff --git a/floof/config/routing.py b/floof/config/routing.py index 6ec64e4..c9b4975 100644 --- a/floof/config/routing.py +++ b/floof/config/routing.py @@ -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. diff --git a/floof/controllers/account.py b/floof/controllers/account.py index e4f39f3..7dabaa6 100644 --- a/floof/controllers/account.py +++ b/floof/controllers/account.py @@ -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('/') diff --git a/floof/controllers/art.py b/floof/controllers/art.py index a0aecd7..5186daa 100644 --- a/floof/controllers/art.py +++ b/floof/controllers/art.py @@ -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 index 0000000..e923e30 --- /dev/null +++ b/floof/controllers/gallery.py @@ -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() diff --git a/floof/controllers/search.py b/floof/controllers/search.py index 3b6faef..57ff4d2 100644 --- a/floof/controllers/search.py +++ b/floof/controllers/search.py @@ -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 diff --git a/floof/controllers/tag.py b/floof/controllers/tag.py index 3b69ed6..874e605 100644 --- a/floof/controllers/tag.py +++ b/floof/controllers/tag.py @@ -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)) + diff --git a/floof/lib/helpers.py b/floof/lib/helpers.py index 28e9176..c8a1251 100644 --- a/floof/lib/helpers.py +++ b/floof/lib/helpers.py @@ -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 * diff --git a/floof/lib/search.py b/floof/lib/search.py index 3acb248..f7ce65b 100644 --- a/floof/lib/search.py +++ b/floof/lib/search.py @@ -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') diff --git a/floof/model/art.py b/floof/model/art.py index c3fba3a..b709610 100644 --- a/floof/model/art.py +++ b/floof/model/art.py @@ -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 diff --git a/floof/model/search.py b/floof/model/search.py index 011468c..be2bb9e 100644 --- a/floof/model/search.py +++ b/floof/model/search.py @@ -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 diff --git a/floof/model/users.py b/floof/model/users.py index 5c9783e..86c31d8 100644 --- a/floof/model/users.py +++ b/floof/model/users.py @@ -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 diff --git a/floof/public/layout.css b/floof/public/layout.css index 25737d0..a098b5e 100644 --- a/floof/public/layout.css +++ b/floof/public/layout.css @@ -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 */ diff --git a/floof/templates/account/register.mako b/floof/templates/account/register.mako index 485012c..2de23a4 100644 --- a/floof/templates/account/register.mako +++ b/floof/templates/account/register.mako @@ -2,7 +2,7 @@

Registering from ${c.identity_url}.

-${h.form(url.current(action='register_finish'), method='POST')} +${h.form(url('register_finish'), method='POST')}
Username
${h.text('username', value=c.username)}
diff --git a/floof/templates/art/new.mako b/floof/templates/art/new.mako index e2c00fb..3eda527 100644 --- a/floof/templates/art/new.mako +++ b/floof/templates/art/new.mako @@ -3,7 +3,7 @@

Add New Art

Now: Upload a file. Later: Supply a link? Not exclusive to uploading.

-${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()} diff --git a/floof/templates/art/show.mako b/floof/templates/art/show.mako index 715b9d1..262f1cb 100644 --- a/floof/templates/art/show.mako +++ b/floof/templates/art/show.mako @@ -5,24 +5,30 @@

Viewing Art

% 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: -x +${h.form(h.url("art_tag", art_id=c.art.id, id=tag.id), method="delete")} +${h.submit('delete', 'X')} ${tag} +${h.end_form()} % endfor

What do you think?

+${h.form (h.url("rate_art", id=c.art.id), method="put")} % for score,text in sorted(Rating.options.items()): -${text} + % endfor +${h.end_form()} % endif diff --git a/floof/templates/base.mako b/floof/templates/base.mako index 654b8b3..73f069c 100644 --- a/floof/templates/base.mako +++ b/floof/templates/base.mako @@ -11,7 +11,7 @@ Home % if c.user: -| Add Art +| Add Art | Your Searches ## | Your Page % endif @@ -32,7 +32,7 @@ ${h.end_form()}
% if c.user:
-

Logged in as ${c.user.name}. ${h.submit(None, 'Log out')}

+

Logged in as ${c.user.name}. ${h.submit(None, 'Log out')}

% else:
@@ -46,6 +46,17 @@ ${h.end_form()}
+ +<% messages = h.flash.pop_messages() %> +% if messages: + +% endif + +
${next.body()}
diff --git a/floof/templates/index.mako b/floof/templates/index.mako index af95f7a..a774551 100644 --- a/floof/templates/index.mako +++ b/floof/templates/index.mako @@ -1,10 +1,4 @@ <%inherit file="base.mako" /> +<%namespace name="macros" file="/macros.mako" /> - - +${macros.thumbs(c.artwork)} diff --git a/floof/templates/macros.mako b/floof/templates/macros.mako new file mode 100644 index 0000000..061b7b9 --- /dev/null +++ b/floof/templates/macros.mako @@ -0,0 +1,11 @@ +<%def name="thumbs(art)"> + + \ No newline at end of file diff --git a/floof/templates/users/view.mako b/floof/templates/users/view.mako index 9f39bae..f9a9329 100644 --- a/floof/templates/users/view.mako +++ b/floof/templates/users/view.mako @@ -1,3 +1,9 @@ <%inherit file="/base.mako" /> +<%namespace name="macros" file="/macros.mako" />

This is the userpage for ${c.this_user.name}.

+ +% for gallery in c.this_user.primary_page.galleries: +

${gallery.string}

+${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 index 0000000..695513c --- /dev/null +++ b/floof/tests/functional/test_gallery.py @@ -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... diff --git a/floof/todo.txt b/floof/todo.txt index f9a7cd0..85b3761 100644 --- a/floof/todo.txt +++ b/floof/todo.txt @@ -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