From: Nick Retallack Date: Mon, 7 Dec 2009 02:05:00 +0000 (-0800) Subject: merged suff, commented out some of my own X-Git-Url: http://git.veekun.com/zzz-floof.git/commitdiff_plain/490b45cdc311a09db0266a3f974289e3979bf2ea?hp=c472ed30b5d4600a8ead8846ddbcbf3bb1b027d6 merged suff, commented out some of my own --- diff --git a/floof/config/routing.py b/floof/config/routing.py index f6906f4..7a0227a 100644 --- a/floof/config/routing.py +++ b/floof/config/routing.py @@ -22,8 +22,8 @@ def make_map(): require_POST = dict(conditions={'method': ['POST']}) # get rid of trailing slashes - map.redirect('/*(url)/', '/{url}', - _redirect_code='301 Moved Permanently') + # map.redirect('/*(url)/', '/{url}', + # _redirect_code='301 Moved Permanently') # The ErrorController route (handles 404/500 error pages); it should diff --git a/floof/controllers/account.py b/floof/controllers/account.py index 2c94736..2073d44 100644 --- a/floof/controllers/account.py +++ b/floof/controllers/account.py @@ -97,6 +97,7 @@ class AccountController(BaseController): c.identity_url = session['register:identity_url'] c.nickname = session.get('register:nickname', None) + # XXX hey, uh. warn if this name is taken already. return render('/account/register.mako') @@ -106,23 +107,27 @@ class AccountController(BaseController): identity_url = session['register:identity_url'] username = request.params.get('username', None) - # XXX how do we return errors in some useful way? - if not username: - return 'Please enter a username.' + h.flash(u'Please enter a username.') + return self.register() if User.query.filter_by(name=username).count(): - return 'That username is taken.' + h.flash(u'This username is already taken.') + return self.register() + + if not User.is_valid_name(username): + h.flash(u'This username is not valid.') + return self.register() # Create db records - user = User(name=username) + user = User(name=username, display_name=username) user.identity_urls.append(IdentityURL(url=identity_url)) elixir.session.commit() # Log in session['user_id'] = user.id session.save() + h.flash(u'You are now logged in.') - # XXX how do we do success messages in some useful way? # XXX send me where I came from redirect('/') diff --git a/floof/controllers/art.py b/floof/controllers/art.py index 9a2f1ba..0b89d98 100644 --- a/floof/controllers/art.py +++ b/floof/controllers/art.py @@ -1,15 +1,15 @@ import logging -from pylons import config, request, response, session, tmpl_context as c, h +from pylons import config, request, response, session, tmpl_context as c from pylons.controllers.util import abort, redirect from pylons import url from floof.lib.base import BaseController, render log = logging.getLogger(__name__) -from floof.lib import file_storage as storage -from floof.model.users import User -from floof.model import Art, Rating, UserRelation +from floof.lib import file_storage as storage, helpers as h +from floof.model import Art, Rating, ArtUser +from floof.model.art import ArtUserType from floof.model.comments import Discussion from floof.model.users import User, UserRelationship @@ -18,7 +18,6 @@ import elixir import os.path import PIL import PIL.Image -from sqlalchemy import func from sqlalchemy.exceptions import IntegrityError from sqlalchemy.orm.exc import NoResultFound from wtforms.validators import ValidationError @@ -36,38 +35,6 @@ class ArtUploadForm(Form): if field.data == u'': raise ValidationError('File is required') - # Also make this into a general User List field validator - """ PLEASE NOTE! I just realized that I need to have a __str__ method on User - to get it to write the usernames back in the form when it redisplays them, since - this validator turns them into user objects instead. This fact actually sounds dangerous - to me in the future, since it means I proably shouldn't be changing the data input - by the user right here in the validator, or the user will see the post-mangled data instead - of what they actually typed. Hm. - - One solution to this could be to only look up the users after normal validation is over, - and then manually add validation errors to the form if that fails. But I think that kind of - sucks. Perhaps the ideology in Formish, where they keep Validation and Conversion as - separate tasks, is a better way of doing it? That way there is less risk of changing the user's - input -- you do that at the conversiot stage -- yet it is still encapsulated in the form workflow. - Hm. But that means I'd have to query for the users in the validation step and throw them away, - or something equally stupid. Guess there's no perfect solution here, but I thought it was - worth discussing. - - Btw, this is meant to be used by a field with multi user autocompletion on it (like on stackoverflow tags), - so the user should never actually submit anything invalid unless they disable javascript and force it. - """ - def validate_by(self, field): - if not field.data: - raise ValidationError("Needs at least one creator") - user_names = field.data.split() - users = [] - # TODO: Could totally do a filter__in here instead of picking them out individually - for user_name in user_names: - user = User.get_by(name=user_name) - if not user: - raise ValidationError("Couldn't find user %s" % user_name) - users.append(user) - field.data = users class ArtController(BaseController): def __before__(self, id=None): @@ -128,11 +95,17 @@ class ArtController(BaseController): ) c.art.discussion = Discussion(count=0) - if c.form.by_me and c.user not in c.form.by.data: - UserRelation(user=c.user, creator=c.user, kind="by", art=c.art) - - for artist in c.form.by.data: - UserRelation(user=artist, creator=c.user, kind="by", art=c.art) + # <<<<<<< HEAD + # if c.form.by_me and c.user not in c.form.by.data: + # UserRelation(user=c.user, creator=c.user, kind="by", art=c.art) + # + # for artist in c.form.by.data: + # UserRelation(user=artist, creator=c.user, kind="by", art=c.art) + # ======= + # For the moment, cheerfully assume that people are uploading their own + # art + ArtUser(art=c.art, user=c.user, type=ArtUserType.BY) + # >>>>>>> origin/master try: @@ -174,8 +147,7 @@ class ArtController(BaseController): def watchstream(self, name): """Watchstream for a certain user.""" try: - c.watching_user = User.query.filter(func.lower(User.name) == name) \ - .one() + c.watching_user = User.get_by(name=name) except NoResultFound: abort(404) diff --git a/floof/controllers/gallery.py b/floof/controllers/gallery.py index e923e30..4a4ea5b 100644 --- a/floof/controllers/gallery.py +++ b/floof/controllers/gallery.py @@ -1,9 +1,10 @@ import logging -from pylons import request, response, session, tmpl_context as c, h +from pylons import request, response, session, tmpl_context as c from pylons.controllers.util import abort, redirect from pylons import url +from floof.lib import helpers as h from floof.lib.base import BaseController, render log = logging.getLogger(__name__) diff --git a/floof/controllers/relation.py b/floof/controllers/relation.py index 48e7a46..719fe2b 100644 --- a/floof/controllers/relation.py +++ b/floof/controllers/relation.py @@ -1,8 +1,9 @@ import logging -from pylons import request, response, session, tmpl_context as c, h, url +from pylons import request, response, session, tmpl_context as c, url from pylons.controllers.util import abort, redirect +from floof.lib import helpers as h from floof.lib.base import BaseController, render log = logging.getLogger(__name__) diff --git a/floof/controllers/search.py b/floof/controllers/search.py index f87ddae..8617966 100644 --- a/floof/controllers/search.py +++ b/floof/controllers/search.py @@ -1,11 +1,12 @@ import logging -from pylons import request, response, session, tmpl_context as c, h +from pylons import request, response, session, tmpl_context as c from pylons.controllers.util import abort, redirect from pylons import url +from floof.lib import helpers as h from floof.lib.base import BaseController, render -from floof.lib.search import do_search +from floof.lib.tags import parse log = logging.getLogger(__name__) @@ -20,7 +21,7 @@ class SearchController(BaseController): return self.save() c.query = request.params.get('query', '') - c.artwork = do_search(c.query) + c.artwork = parse(c.query).all() return render('/index.mako') # TODO: login required diff --git a/floof/controllers/tag.py b/floof/controllers/tag.py index e5ce85b..5d13789 100644 --- a/floof/controllers/tag.py +++ b/floof/controllers/tag.py @@ -1,16 +1,18 @@ import logging +import re -from pylons import request, response, session, tmpl_context as c, h +from pylons import request, response, session, tmpl_context as c, url from pylons.controllers.util import abort, redirect +import elixir +from floof.model import Art, ArtUser, ArtUserType, Tag, TagText, User +from floof.lib import helpers as h from floof.lib.base import BaseController, render -from pylons import url +from floof.lib.tags import add_tags +from floof.lib.dbhelpers import find_or_create log = logging.getLogger(__name__) -import elixir -from floof.model import Art, Tag - class TagController(BaseController): # TODO: login required @@ -23,7 +25,9 @@ class TagController(BaseController): # 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)) + tag_string = request.params.get('tags', '') + add_tags(c.art, tag_string, c.user) + + # Add or remove tags + redirect(url('show_art', id=c.art.id)) diff --git a/floof/controllers/user_settings.py b/floof/controllers/user_settings.py index 8d8adf0..de46e37 100644 --- a/floof/controllers/user_settings.py +++ b/floof/controllers/user_settings.py @@ -3,7 +3,6 @@ import logging import elixir from pylons import request, response, session, tmpl_context as c from pylons.controllers.util import abort, redirect_to -from sqlalchemy import func from sqlalchemy.orm.exc import NoResultFound import floof.lib.helpers as h @@ -22,7 +21,7 @@ class UserSettingsController(BaseController): `type`, and `add_remove` as parameters. """ try: - user = User.query.filter(func.lower(User.name) == name).one() + user = User.get_by(name=name) except NoResultFound: abort(404) diff --git a/floof/controllers/users.py b/floof/controllers/users.py index 1a79c15..7c24c0f 100644 --- a/floof/controllers/users.py +++ b/floof/controllers/users.py @@ -2,7 +2,6 @@ import logging from pylons import request, response, session, tmpl_context as c from pylons.controllers.util import abort, redirect_to -from sqlalchemy import func from sqlalchemy.orm.exc import NoResultFound from floof.lib.base import BaseController, render @@ -24,8 +23,7 @@ class UsersController(BaseController): """Userpage.""" try: - c.this_user = User.query.filter(func.lower(User.name) == name) \ - .one() + c.this_user = User.get_by(name=name) except NoResultFound: abort(404) diff --git a/floof/lib/search.py b/floof/lib/search.py deleted file mode 100644 index 37d762d..0000000 --- a/floof/lib/search.py +++ /dev/null @@ -1,43 +0,0 @@ -from floof.model 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() - - tags = [] - for word in words: - components = word.split(':') - if len(components) == 1: - # tags are plain. - tags.append(word) - elif components[0] == "rating": - if components[1].isnumeric(): - score = int(components[1]) - else: - score = Rating.reverse_options.get(components[1]) - - if -1 <= score <= 3: - pass - # 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) - diff --git a/floof/lib/tags.py b/floof/lib/tags.py new file mode 100644 index 0000000..6186048 --- /dev/null +++ b/floof/lib/tags.py @@ -0,0 +1,128 @@ +from floof.model import Art, ArtUser, ArtUserType, Tag, TagText, User + +from dbhelpers import find_or_create +import re +def parse(search_string): + """Parses a search query, and returns a query object on Art. + + Queries can contain: + - Regular tags: foo + - User relations: by:kalu, of:eevee, for:ootachi + + Later: + - Negative versions of anything above: -by:eevee, -dongs + """ + + # XXX doesn't do negative querying yet. + # XXX could use some sane limits. + + # We'll be building this as we go. + q = Art.query + + terms = search_string.split() + for tag in terms: + if ':' in tag: + # This is a special tag; at the moment, by/for/of to indicate + # related users + prefix, tag = tag.split(':', 1) + + # XXX what to do if this fails? abort? return empty query? + target_user = User.get_by(name=tag) + + if prefix == 'by': + rel = ArtUserType.BY + elif prefix == 'for': + rel = ArtUserType.FOR + elif prefix == 'of': + rel = ArtUserType.OF + else: + # Bogus tag. Not sure what to do here, so for the moment, + # ignore it + continue + + # Inner join to the ArtUser table + q = q.join(ArtUser, aliased=True) \ + .filter(ArtUser.user == target_user) \ + .filter(ArtUser.type == rel) + + else: + # Regular ol' tag + q = q.join(Tag, TagText, aliased=True) \ + .filter(TagText.text == tag) + + return q + +def add_tags(art, tag_string, user): + """Takes a string that looks like a tag query, and effectively modifies the + art's tags to match it. + """ + + # XXX what to do with invalid tags? just return them and let caller fix? + bad_tags = [] + for tag_text in tag_string.split(): + original_tag_text = tag_text + tag_text = tag_text.lower() + + # Adding or removing a tag? + if tag_text[0] == '-': + add = False + tag_text = tag_text[1:] + else: + # Allow "+foo" to mean "add foo" + if tag_text[0] == '+': + tag_text = tag_text[1:] + add = True + + # Check for special namespaces + prefix = None + if ':' in tag_text: + prefix, tag_text = tag_text.split(':', 1) + if prefix not in ['by', 'for', 'of']: + # This is bogus. Skip it. + bad_tags.append(original_tag_text) + continue + + if prefix == 'by': + # XXX this needs supporting. silently ignore for now + continue + + # Must be 3-50 alphanumeric characters + if not re.match('^[a-z0-9]{3,50}$', tag_text): + bad_tags.append(original_tag_text) + continue + + # Do work! + if prefix: + target_user = User.get_by(name=tag_text) + + # Special tag; at the moment, just a relationship + if prefix == 'by': + rel = ArtUserType.BY + elif prefix == 'for': + rel = ArtUserType.FOR + elif prefix == 'of': + rel = ArtUserType.OF + + user_assoc_data = dict(art=art, user=target_user, type=rel) + if add: + find_or_create(ArtUser, **user_assoc_data) + + else: + # XXX this will die for nonassociations + user_assoc = ArtUser.get_by(art=art, **user_assoc_data) + user_assoc.delete() + + else: + # Regular tag + if add: + tag = find_or_create(TagText, text=tag_text) + find_or_create(Tag, art=art, tagger=user, tagtext=tag) + + else: + tag = TagText.get_by(text=tag_text) + if tag: + # XXX this will die + tag_assoc = Tag.get_by(art=art, tagger=user, tagtext=tag) + tag_assoc.delete() + + elixir.session.commit() diff --git a/floof/model/art.py b/floof/model/art.py index 8989e8f..0452e32 100644 --- a/floof/model/art.py +++ b/floof/model/art.py @@ -4,7 +4,6 @@ # Copyright (c) 2009 Scribblr # -# from elixir import Entity, Field, Integer, Unicode from elixir import * import elixir @@ -12,12 +11,9 @@ from pylons import config from floof.lib.file_storage import get_path, save_file from floof.lib.dbhelpers import find_or_create, update_or_create -import floof.model.comments +from floof.model.users import User -# Note: Art is the most important class. To keep its size down, and to better organize the source code, -# other modules will mix into it automatically by adding to its __bases__. - class Art(Entity): title = Field(Unicode(120)) original_filename = Field(Unicode(120)) @@ -28,8 +24,35 @@ class Art(Entity): tags = OneToMany('Tag') discussion = ManyToOne('Discussion') - user_relations = OneToMany('UserRelation') + art_users = OneToMany('ArtUser') @property def file_path(self): return get_path("art", self.hash) + + # Associated users + # XXX ok these could stand to do the filtering sql-side + @property + def artists(self): + return (au.user for au in self.art_users if au.type == ArtUserType.BY) + + @property + def recipients(self): + return (au.user for au in self.art_users if au.type == ArtUserType.FOR) + + @property + def participants(self): + return (au.user for au in self.art_users if au.type == ArtUserType.OF) + +class ArtUserType(object): + BY = 1 + FOR = 2 + OF = 3 + +class ArtUser(Entity): + art = ManyToOne('Art', required=True) + user = ManyToOne('User', required=True) + type = Field(Integer, required=True) # ArtUserType + + # TODO: admin log ought to remember who confirmed the relation. + # (tag history will remember who proposed it) diff --git a/floof/model/relations.py b/floof/model/relations.py deleted file mode 100644 index a6fe020..0000000 --- a/floof/model/relations.py +++ /dev/null @@ -1,34 +0,0 @@ -from elixir import * -from art import Art - - -class UserRelation(Entity): - user = ManyToOne("User") - art = ManyToOne("Art") - kind = Field(String) # by for of - creator = ManyToOne("User") - confirmed_by_related_user = Field(Boolean) - - # it is useful to record which authority figure on a given artwork - # confirmed the validity of this relation. - confirmed_by_authority = ManyToOne("User") - - def __init__(self, **kwargs): - super(UserRelation, self).__init__(**kwargs) - assert self.user and self.art and self.kind and self.creator - - if self.creator == self.user: - self.confirmed_by_related_user = True - # TODO: implement authorities - # if self.creator in self.art.authorities - # self.confirmed_by_authority = self.creator - - def __unicode__(self): - return "%s: %s" % (self.kind, self.related_user) - - -class RelationMixin(object): - def add_relation(creator, kind, user): - return UserRelation(art=self, creator=creator, kind=kind, user=user) - -Art.__bases__ += (RelationMixin,) diff --git a/floof/model/search.py b/floof/model/search.py index fb86f35..37f2d5f 100644 --- a/floof/model/search.py +++ b/floof/model/search.py @@ -10,13 +10,6 @@ class SavedSearch(Entity): def __unicode__(self): return self.string - @property - def results(self): - # This caused some cyclic dependencies when I tried importing it - # at the module level. I wonder why that is... - from floof.lib.search import do_search - return do_search(self.string) - class GalleryWidget(Entity): diff --git a/floof/model/tags.py b/floof/model/tags.py index 1a41278..b0a7072 100644 --- a/floof/model/tags.py +++ b/floof/model/tags.py @@ -1,7 +1,5 @@ from elixir import * from art import Art -from floof.lib.dbhelpers import find_or_create, update_or_create - class Tag(Entity): # look into how ondelete works. This just sets a database property. @@ -14,33 +12,9 @@ class Tag(Entity): return "(broken)" return unicode(self.tagtext) - class TagText(Entity): text = Field(Unicode(50)) # gotta enforce this somehow tags = OneToMany('Tag') def __unicode__(self): return self.text - - -class TagMixin(object): - def add_tags(self, tags, user): - for text in tags.split(): - if text[0] == '-': - # Nega-tags - tagtext = TagText.get_by(text=text[1:]) - if tagtext: - tag = Tag.get_by(art=self, tagger=user, tagtext=tagtext) - if tag: - elixir.session.delete(tag) - - else: - if len(text) > 50: - raise "Long Tag!" # can we handle this more gracefully? - # sqlite seems happy to store strings much longer than the supplied limit... - - # elixir should really have its own find_or_create. - tagtext = find_or_create(TagText, text=text) - tag = find_or_create(Tag, art=self, tagger=user, tagtext=tagtext) - -Art.__bases__ += (TagMixin, ) diff --git a/floof/model/users.py b/floof/model/users.py index 7601cf5..a6f3a6d 100644 --- a/floof/model/users.py +++ b/floof/model/users.py @@ -4,12 +4,15 @@ # Copyright (c) 2009 Scribblr # -# from elixir import Entity, Field, Unicode, belongs_to, has_many +import re + from elixir import * + from search import GalleryWidget class User(Entity): - name = Field(Unicode(20)) + name = Field(Unicode(20), unique=True) + display_name = Field(Unicode(20)) uploads = OneToMany('Art') has_many('identity_urls', of_kind='IdentityURL') searches = OneToMany('SavedSearch') @@ -19,10 +22,22 @@ class User(Entity): relationships = OneToMany('UserRelationship', inverse='user') target_of_relationships = OneToMany('UserRelationship', inverse='target_user') + @classmethod + def is_valid_name(cls, name): + """Returns True iff the name is a valid username. + + Only lowercase letters, numbers, and hyphens are allowed. + + Names must also be at least one character long, but no more than 20, + and cannot start or end with a hyphen. + """ + return re.match('^[-a-z0-9]{1,20}$', name) \ + and name[0] != '-' and name[-1] != '-' + def __unicode__(self): return self.name - + def __str__(self): return self.name diff --git a/floof/public/layout.css b/floof/public/layout.css index bc7056e..7a7308b 100644 --- a/floof/public/layout.css +++ b/floof/public/layout.css @@ -16,6 +16,7 @@ body { font-family: sans-serif; font-size: 12px; } /*** Common bits and pieces ***/ h1 { margin: 0.5em 0 0.25em; font-size: 2em; border-bottom: 1px solid #404040; text-shadow: #a0a0a0 1px 1px 1px; } +h2 { margin: 0.5em 0 0.25em; font-size: 1.5em; border-bottom: 1px dotted #606060; text-shadow: #a0a0a0 1px 1px 1px; } a { color: #647cc4; font-weight: bold; text-decoration: none; pointer: cursor; } a:visited { color: #48598e; } diff --git a/floof/templates/art/new.mako b/floof/templates/art/new.mako index 684c004..0adc916 100644 --- a/floof/templates/art/new.mako +++ b/floof/templates/art/new.mako @@ -1,22 +1,20 @@ <%inherit file="/base.mako" />

Add New Art

-

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

- -## Todo: write some macros to make outputting form fields easier. - ${h.form(h.url('create_art'), multipart=True)} -
- ${normal_field(c.form.by)} - ${checkbox_field(c.form.by_me)} -
- -
${normal_field(c.form.file)}
- +##<<<<<<< HEAD +##
+## ${normal_field(c.form.by)} +## ${checkbox_field(c.form.by_me)} +##
+## +##
${normal_field(c.form.file)}
+##======= +## Todo: write some macros to make outputting form fields easier. +${normal_field(c.form.file)} +##>>>>>>> origin/master -##Artist: ${h.text('artist')} -##${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 a38806d..9356447 100644 --- a/floof/templates/art/show.mako +++ b/floof/templates/art/show.mako @@ -34,17 +34,15 @@ ${h.end_form()}

Relations

-

Add Relations

-${h.form (h.url("create_relation", kind="by", art_id=c.art.id))} -By: ${h.text('username')} -${h.submit('add','Add')} -${h.end_form()} - ${comments.comment_block(c.art.discussion.comments)} diff --git a/floof/templates/users/view.mako b/floof/templates/users/view.mako index 5b7e12d..eb24b2d 100644 --- a/floof/templates/users/view.mako +++ b/floof/templates/users/view.mako @@ -21,7 +21,8 @@ ${h.form(url(controller='user_settings', action='rel_toggle', name=c.user.name.l % endif % endif +<%! from floof.lib.search import parse %> % for gallery in c.this_user.primary_page.galleries:

${gallery.string}

-${macros.thumbs(gallery.search.results)} +${macros.thumbs(parse(gallery.tags.string))} % endfor diff --git a/floof/websetup.py b/floof/websetup.py index 815832d..a4e935f 100644 --- a/floof/websetup.py +++ b/floof/websetup.py @@ -23,7 +23,7 @@ def setup_app(command, conf, vars): # Users from floof.model.users import IdentityURL, User identity_url = IdentityURL(url=u'http://eevee.livejournal.com/') - user = User(name=u'Eevee') + user = User(name=u'eevee') user.identity_urls.append(identity_url) model.Session.commit()