From: Nick Retallack Date: Tue, 1 Dec 2009 09:18:21 +0000 (-0800) Subject: merged. Oh no, we have two different user relationship models. Mine's in relations... X-Git-Url: http://git.veekun.com/zzz-floof.git/commitdiff_plain/f263f51648eef9739caa92e19ec45b32d7a1e49e?hp=27cb94de7786bd9a9d0e03c9562876d4e21cda77 merged. Oh no, we have two different user relationship models. Mine's in relations.py, and is used for stuff like by/for/of, most of which isn't implemented yet --- diff --git a/floof/config/routing.py b/floof/config/routing.py index 44462f8..f6906f4 100644 --- a/floof/config/routing.py +++ b/floof/config/routing.py @@ -74,6 +74,10 @@ def make_map(): sub.connect('delete_tag', '/art/{art_id}/tag/{id}') sub.connect('create_tag', '/art/{art_id}/tag') + with map.submapper(controller='relation') as sub: + sub.connect('create_relation', '/art/{art_id}/relations/{kind}/create', action="create") + # TODO: conditions: kind = by|for|of|character? + 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 diff --git a/floof/controllers/art.py b/floof/controllers/art.py index b8406ed..41298c8 100644 --- a/floof/controllers/art.py +++ b/floof/controllers/art.py @@ -8,7 +8,8 @@ from floof.lib.base import BaseController, render log = logging.getLogger(__name__) import elixir -from floof.model.art import Art, Rating +from floof.model.users import User +from floof.model import Art, Rating, UserRelation from floof.model.comments import Discussion from floof.model.users import User, UserRelationship @@ -16,6 +17,52 @@ from sqlalchemy import func from sqlalchemy.exceptions import IntegrityError from sqlalchemy.orm.exc import NoResultFound +from wtforms.validators import ValidationError +from wtforms import * + + +class ArtUploadForm(Form): + by = TextField('Artists') + file = FileField('Upload') + url = TextField('Link') + + # TODO: make this general purpose + def validate_file(self, field): + 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): @@ -26,23 +73,38 @@ class ArtController(BaseController): def new(self): """ New Art! """ + c.form = ArtUploadForm() return render("/art/new.mako") # TODO: login required def create(self): - c.art = Art(uploader=c.user, **request.params) - c.art.discussion = Discussion(count=0) + c.form = ArtUploadForm(request.params) + if c.form.validate(): - 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)) + c.art = Art(uploader=c.user, **request.params) + c.art.discussion = Discussion(count=0) + + for artist in c.form.by.data: + UserRelation(user=artist, kind="by", creator=c.user, art=c.art) + + file = request.params['file'] + + 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)) + + else: + ## TODO: JavaScript should be added to the upload form so that it is + ## impossible to submit the form when it contains any invalid users, + ## so this never happens. Only autocompled usernames should be allowed. + return render("/art/new.mako") def show(self, id): diff --git a/floof/controllers/comments.py b/floof/controllers/comments.py index b096ba2..342b009 100644 --- a/floof/controllers/comments.py +++ b/floof/controllers/comments.py @@ -6,7 +6,7 @@ from pylons.controllers.util import abort, redirect, redirect_to from sqlalchemy import and_ from floof.lib.base import BaseController, render -from floof.model.art import Art +from floof.model import Art from floof.model.comments import Comment log = logging.getLogger(__name__) diff --git a/floof/controllers/relation.py b/floof/controllers/relation.py new file mode 100644 index 0000000..48e7a46 --- /dev/null +++ b/floof/controllers/relation.py @@ -0,0 +1,35 @@ +import logging + +from pylons import request, response, session, tmpl_context as c, h, url +from pylons.controllers.util import abort, redirect + +from floof.lib.base import BaseController, render + +log = logging.getLogger(__name__) + +from floof.model import Art, UserRelation +from floof.model.users import User +import elixir + +# TODO!!! Implement adding a related user the same way that it works +# on the "add new art" page + +class RelationController(BaseController): + def create(self, art_id, kind): + art = h.get_object_or_404(Art, id=art_id) + user = h.get_object_or_404(User, name=request.params['username']) + ## TODO: actually, this should act like a form validation. + + prior_relation = UserRelation.get_by(art=art, user=user) + if prior_relation: + abort(404) ## should be a validation error + + relation = UserRelation(user=user, kind=kind, art=art, creator=c.user) + elixir.session.commit() + redirect(url('show_art', id=art_id)) + + def index(self): + # Return a rendered template + #return render('/relation.mako') + # or, return a response + return 'Hello World' diff --git a/floof/controllers/search.py b/floof/controllers/search.py index 4e92a6d..f87ddae 100644 --- a/floof/controllers/search.py +++ b/floof/controllers/search.py @@ -9,8 +9,8 @@ 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, GalleryWidget +from floof.model import Art, Tag, TagText +from floof.model import SavedSearch, GalleryWidget import elixir class SearchController(BaseController): diff --git a/floof/controllers/tag.py b/floof/controllers/tag.py index 4f98994..e5ce85b 100644 --- a/floof/controllers/tag.py +++ b/floof/controllers/tag.py @@ -9,7 +9,7 @@ from pylons import url log = logging.getLogger(__name__) import elixir -from floof.model.art import Art, Tag +from floof.model import Art, Tag class TagController(BaseController): diff --git a/floof/lib/search.py b/floof/lib/search.py index f7ce65b..37d762d 100644 --- a/floof/lib/search.py +++ b/floof/lib/search.py @@ -1,4 +1,4 @@ -from floof.model.art import Art, Tag, TagText +from floof.model import Art, Tag, TagText def do_search(query): tags = query.split() diff --git a/floof/model/__init__.py b/floof/model/__init__.py index 1a81770..8368357 100644 --- a/floof/model/__init__.py +++ b/floof/model/__init__.py @@ -22,7 +22,14 @@ if elixir.options_defaults.get('autoload', False) \ # # import other entities here, e.g. # from floof.model.blog import BlogEntry, BlogComment -from floof.model import art, users, search +from floof.model.art import * +from floof.model.ratings import * +from floof.model.comments import * +from floof.model.search import * +from floof.model.tags import * +from floof.model.users import * +from floof.model.relations import * + # Finally, call elixir to set up the tables. # but not if using reflected tables diff --git a/floof/model/art.py b/floof/model/art.py index 0732684..bb68c9f 100644 --- a/floof/model/art.py +++ b/floof/model/art.py @@ -14,6 +14,10 @@ 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 + +# 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)) @@ -23,6 +27,9 @@ class Art(Entity): tags = OneToMany('Tag') discussion = ManyToOne('Discussion') + user_relations = OneToMany('UserRelation') + + def set_file(self, file): self.hash = save_file("art", file) self.original_filename = file.filename @@ -33,69 +40,5 @@ class Art(Entity): if self.hash: return get_path("art", self.hash) - - 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) - - - - - def rate(self, score, user): - return update_or_create(Rating, {"rater":user, "art":self}, {"score":score}) - - def user_score(self, user): - rating = Rating.get_by(rater=user, art=self) - if rating: - return rating.score - return Rating.default - def __unicode__(self): return self.get_path() - - -class Tag(Entity): - # look into how ondelete works. This just sets a database property. - art = ManyToOne('Art', ondelete='cascade') - tagger = ManyToOne('User', ondelete='cascade') - tagtext = ManyToOne('TagText') - - def __unicode__(self): - if not self.tagtext: - 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 Rating(Entity): - art = ManyToOne('Art', ondelete='cascade') - rater = ManyToOne('User', ondelete='cascade') - score = Field(Integer) - - options = {-1:"sucks", 0:"undecided", 1:"good", 2:"great"} - default = 0 - - -Rating.reverse_options = dict (zip(Rating.options.values(), Rating.options.keys())) diff --git a/floof/model/ratings.py b/floof/model/ratings.py new file mode 100644 index 0000000..6b5850a --- /dev/null +++ b/floof/model/ratings.py @@ -0,0 +1,26 @@ +from elixir import * +from art import Art +from floof.lib.dbhelpers import find_or_create, update_or_create + +class Rating(Entity): + art = ManyToOne('Art', ondelete='cascade') + rater = ManyToOne('User', ondelete='cascade') + score = Field(Integer) + + options = {-1:"sucks", 0:"undecided", 1:"good", 2:"great"} + default = 0 + +Rating.reverse_options = dict (zip(Rating.options.values(), Rating.options.keys())) + + +class RatingMixin(object): + def rate(self, score, user): + return update_or_create(Rating, {"rater":user, "art":self}, {"score":score}) + + def user_score(self, user): + rating = Rating.get_by(rater=user, art=self) + if rating: + return rating.score + return Rating.default + +Art.__bases__ += (RatingMixin,) \ No newline at end of file diff --git a/floof/model/relations.py b/floof/model/relations.py new file mode 100644 index 0000000..a6fe020 --- /dev/null +++ b/floof/model/relations.py @@ -0,0 +1,34 @@ +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 01d45a2..fb86f35 100644 --- a/floof/model/search.py +++ b/floof/model/search.py @@ -1,17 +1,20 @@ from elixir import * # 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') + fork = ManyToOne("SavedSearch") 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) diff --git a/floof/model/tags.py b/floof/model/tags.py new file mode 100644 index 0000000..1a41278 --- /dev/null +++ b/floof/model/tags.py @@ -0,0 +1,46 @@ +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. + art = ManyToOne('Art', ondelete='cascade') + tagger = ManyToOne('User', ondelete='cascade') + tagtext = ManyToOne('TagText') + + def __unicode__(self): + if not self.tagtext: + 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 af4bfba..7601cf5 100644 --- a/floof/model/users.py +++ b/floof/model/users.py @@ -22,6 +22,9 @@ class User(Entity): def __unicode__(self): return self.name + + def __str__(self): + return self.name def __init__(self, **kwargs): super(User, self).__init__(**kwargs) @@ -47,10 +50,9 @@ class UserPage(Entity): 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. - - """ - + If more than one is set to visible, there would be tabs. The primary page is indicated in the user model. + """ + owner = ManyToOne('User', inverse="pages") title = Field(String) @@ -75,3 +77,4 @@ class UserRelationship(Entity): user = ManyToOne('User') target_user = ManyToOne('User') type = Field(Integer) # UserRelationshipTypes above + diff --git a/floof/public/layout.css b/floof/public/layout.css index b175705..1fa629e 100644 --- a/floof/public/layout.css +++ b/floof/public/layout.css @@ -23,6 +23,9 @@ dl.form { margin: 1em 0; padding-left: 1em; border-left: 0.5em solid gray; } dl.form dt { padding-bottom: 0.25em; font-style: italic; } dl.form dd { margin-bottom: 0.5em; } +.errors {color:red;} + + /* Comments */ .comment {} .comment .header { background: #d8d8d8; } diff --git a/floof/templates/art/new.mako b/floof/templates/art/new.mako index 3eda527..4204700 100644 --- a/floof/templates/art/new.mako +++ b/floof/templates/art/new.mako @@ -3,7 +3,31 @@

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)} -${h.file('file')} + +${normal_field(c.form.by)} +${normal_field(c.form.file)} + + +##Artist: ${h.text('artist')} +##${h.file('file')} ${h.submit(None, 'Upload!')} ${h.end_form()} + + + +<%def name="normal_field(field)"> +
+${field.label()|n} +${field()|n} +%if field.errors: + +%endif +
+ diff --git a/floof/templates/art/show.mako b/floof/templates/art/show.mako index cf713dc..05d1e42 100644 --- a/floof/templates/art/show.mako +++ b/floof/templates/art/show.mako @@ -1,7 +1,7 @@ <%inherit file="/base.mako" /> <%namespace name="comments" file="/comments/lib.mako" /> -<%! from floof.model.art import Rating %> +<%! from floof.model import Rating %>

Viewing Art

@@ -32,6 +32,20 @@ ${h.submit('score', text)} ${h.end_form()} % endif +

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/tests/functional/test_relation.py b/floof/tests/functional/test_relation.py new file mode 100644 index 0000000..b651acf --- /dev/null +++ b/floof/tests/functional/test_relation.py @@ -0,0 +1,7 @@ +from floof.tests import * + +class TestRelationController(TestController): + + def test_index(self): + response = self.app.get(url(controller='relation', action='index')) + # Test response...