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=40a5c182712d31d44aeacddd16a457f5ab707de7 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/LICENSE b/LICENSE new file mode 100644 index 0000000..aa118a2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright © 2009 Alex Munroe (Eevee), Kalu, Ootachi, and Koinu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/floof/config/routing.py b/floof/config/routing.py index 32451e9..f6906f4 100644 --- a/floof/config/routing.py +++ b/floof/config/routing.py @@ -42,9 +42,13 @@ def make_map(): 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('user_page', '/users/{name}', controller='users', action='view') + # Specific user stuff + with map.submapper(controller='users') as sub: + sub.connect( '/users', action='list') + sub.connect('user_page', '/users/{name}', action='view') + with map.submapper(controller='user_settings') as sub: + sub.connect( '/users/{name}/settings/relationships/toggle', + action='rel_toggle', **require_POST) # Comments with map.submapper(controller='comments') as sub: @@ -55,12 +59,17 @@ def make_map(): sub.connect('/*owner_url/comments/{id}/reply', action='reply') sub.connect('/*owner_url/comments/{id}/reply_done', action='reply_done', **require_POST) + # Art 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") + # Some art pages pertain to a specific user, but still belong in the art + # controller + map.connect('/users/{name}/watchstream', controller='art', action='watchstream') + with map.submapper(controller='tag') as sub: sub.connect('delete_tag', '/art/{art_id}/tag/{id}') sub.connect('create_tag', '/art/{art_id}/tag') diff --git a/floof/controllers/art.py b/floof/controllers/art.py index be75ee7..41298c8 100644 --- a/floof/controllers/art.py +++ b/floof/controllers/art.py @@ -11,8 +11,11 @@ import elixir 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 +from sqlalchemy import func from sqlalchemy.exceptions import IntegrityError +from sqlalchemy.orm.exc import NoResultFound from wtforms.validators import ValidationError from wtforms import * @@ -124,3 +127,20 @@ class ArtController(BaseController): elixir.session.commit() redirect(url('show_art', id=c.art.id)) + + + def watchstream(self, name): + """Watchstream for a certain user.""" + try: + c.watching_user = User.query.filter(func.lower(User.name) == name) \ + .one() + except NoResultFound: + abort(404) + + # This user has watches which are users which have art + # XXX use artist, not uploader + c.artwork = Art.query.join(Art.uploader, + User.target_of_relationships) \ + .filter(UserRelationship.user_id == c.watching_user.id) + + return render('/index.mako') diff --git a/floof/controllers/user_settings.py b/floof/controllers/user_settings.py new file mode 100644 index 0000000..8d8adf0 --- /dev/null +++ b/floof/controllers/user_settings.py @@ -0,0 +1,79 @@ +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 +from floof.lib.base import BaseController, render +from floof.model.users import User, UserRelationship +from floof.model.forms import UserRelationshipToggleForm + +log = logging.getLogger(__name__) + +class UserSettingsController(BaseController): + + def rel_toggle(self, name): + """Adds or removes a single relationship with a single user. + + Expects to be called as a POST with `target_user_id`, + `type`, and `add_remove` as parameters. + """ + try: + user = User.query.filter(func.lower(User.name) == name).one() + except NoResultFound: + abort(404) + + schema = UserRelationshipToggleForm() + try: + form_result = schema.to_python(request.params) + except BaseException, e: + # The data for this form is generated entirely by the app. If + # there are errors, the user has been dicking around. + abort(400) + + # Grab any existing relationship row + rel = None + try: + rel = UserRelationship.query.filter_by( + user_id=user.id, + target_user_id=form_result['target_user'].id, + type=form_result['type'], + ).one() + except: + pass + + # XXX shouldn't include "watching"... + target_name = form_result['target_user'].name + if form_result['add_remove'] == u'add': + # Adding + if rel: + # Already exists! Nothing to do. + h.flash("You're already watching {name}..." + .format(name=target_name)) + else: + # Add it + UserRelationship( + user_id=user.id, + target_user_id=form_result['target_user'].id, + type=form_result['type'], + ) + h.flash("Now watching {name}." + .format(name=target_name)) + else: + # Removing + if rel: + # Toss it + rel.delete() + h.flash("No longer watching {name}. How cruel!." + .format(name=target_name)) + else: + # Already gone! Nothing to do. + h.flash("You're not watching {name}..." + .format(name=target_name)) + + elixir.session.commit() + + self.redirect_to_referrer() diff --git a/floof/controllers/users.py b/floof/controllers/users.py index 1e251f0..1a79c15 100644 --- a/floof/controllers/users.py +++ b/floof/controllers/users.py @@ -6,7 +6,7 @@ from sqlalchemy import func from sqlalchemy.orm.exc import NoResultFound from floof.lib.base import BaseController, render -from floof.model.users import User +from floof.model.users import User, UserRelationship log = logging.getLogger(__name__) @@ -29,4 +29,11 @@ class UsersController(BaseController): except NoResultFound: abort(404) + rels = UserRelationship.query.filter_by( + user_id=c.user.id, + target_user_id=c.this_user.id, + ).all() + + c.relationships = [_.type for _ in rels] + return render('/users/view.mako') diff --git a/floof/lib/base.py b/floof/lib/base.py index 169493c..b42d2be 100644 --- a/floof/lib/base.py +++ b/floof/lib/base.py @@ -3,8 +3,9 @@ Provides the BaseController class for subclassing. """ from pylons.controllers import WSGIController +from pylons.controllers.util import abort, redirect from pylons.templating import render_mako as render -from pylons import config, session, tmpl_context as c +from pylons import config, request, session, tmpl_context as c from routes import request_config from floof import model @@ -34,3 +35,11 @@ class BaseController(WSGIController): return WSGIController.__call__(self, environ, start_response) finally: model.Session.remove() + + + def redirect_to_referrer(self): + """Performs a redirect_to to wherever we came from. Used for stuff + like logging in. + """ + referrer = request.headers.get('REFERER', '/') + redirect(referrer, code=302) diff --git a/floof/model/comments.py b/floof/model/comments.py index 5e57c14..ef3a948 100644 --- a/floof/model/comments.py +++ b/floof/model/comments.py @@ -83,10 +83,10 @@ class Comment(Entity): # comment's left/right Comment.query.filter(Comment.discussion == self.discussion) \ .filter(Comment.left >= parent_right) \ - .update({ Comment.left: Comment.left + 2 }) + .update({ 'left': Comment.left + 2 }) Comment.query.filter(Comment.discussion == self.discussion) \ .filter(Comment.right >= parent_right) \ - .update({ Comment.right: Comment.right + 2 }) + .update({ 'right': Comment.right + 2 }) # Then stick the new comment in the right place self.left = parent_right diff --git a/floof/model/forms.py b/floof/model/forms.py new file mode 100644 index 0000000..c316c38 --- /dev/null +++ b/floof/model/forms.py @@ -0,0 +1,37 @@ +"""FormEncode validators.""" + +import formencode + +from floof.model.users import User, UserRelationshipTypes + +class UniqueExistingRow(formencode.FancyValidator): + """Given a column object, converts a unique value from that column into the + corresponding row object. + """ + def __init__(self, table, column): + self.table = table + self.column = column + super(formencode.FancyValidator, self).__init__() + + def _to_python(self, value, state): + try: + row = self.table.query.filter(self.column == value).one() + except BaseException, e: + raise formencode.Invalid( + 'No unique row.', + value, state + ) + return row + +### user_settings + +class UserRelationshipToggleForm(formencode.Schema): + target_user = UniqueExistingRow(User, User.id) + type = formencode.compound.Pipe( + formencode.validators.Int(), + formencode.validators.OneOf( + [v for (k, v) in UserRelationshipTypes.__dict__.items() + if k[0] != '_'] + ), + ) + add_remove = formencode.validators.OneOf([u'add', u'remove']) diff --git a/floof/model/meta.py b/floof/model/meta.py index 1a20aa7..2992ae7 100644 --- a/floof/model/meta.py +++ b/floof/model/meta.py @@ -9,7 +9,3 @@ engine = None # SQLAlchemy session manager. Updated by model.init_model() Session = scoped_session(sessionmaker()) - -# Global metadata. If you have multiple databases with overlapping table -# names, you'll need a metadata for each database -metadata = MetaData() diff --git a/floof/model/users.py b/floof/model/users.py index 4f83a75..7601cf5 100644 --- a/floof/model/users.py +++ b/floof/model/users.py @@ -16,6 +16,8 @@ class User(Entity): # galleries = OneToMany('GalleryWidget') pages = OneToMany('UserPage', inverse="owner") primary_page = OneToOne('UserPage', inverse="owner") + relationships = OneToMany('UserRelationship', inverse='user') + target_of_relationships = OneToMany('UserRelationship', inverse='target_user') def __unicode__(self): @@ -56,9 +58,23 @@ class UserPage(Entity): visible = Field(Boolean) galleries = OneToMany('GalleryWidget') - - - - -# class ArtRelation(Entity): -# \ No newline at end of file + + +class UserRelationshipTypes(object): + IS_WATCHING = 1 + +class UserRelationship(Entity): + """Represents some sort of connection between users. + + For the moment, this means "watching". Later, it may mean friending or + ignoring. + + XXX: Watching should be made more general than this; it should have the + power of an arbitrary query per watched artist without being unintelligible + to users. + """ + + user = ManyToOne('User') + target_user = ManyToOne('User') + type = Field(Integer) # UserRelationshipTypes above + diff --git a/floof/templates/base.mako b/floof/templates/base.mako index e4acc6f..60e8adb 100644 --- a/floof/templates/base.mako +++ b/floof/templates/base.mako @@ -13,10 +13,11 @@ % if c.user: | Add Art | Your Searches +| Watchstream ## | Your Page % endif -${h.form(h.url_for('search'), method='GET')} +${h.form(url('search'), method='GET')} ${h.text('query', c.query)} ${h.submit('button', 'Search')} diff --git a/floof/templates/users/view.mako b/floof/templates/users/view.mako index f9a9329..5b7e12d 100644 --- a/floof/templates/users/view.mako +++ b/floof/templates/users/view.mako @@ -3,7 +3,25 @@

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

+<%! from floof.model.users import UserRelationshipTypes %> +% if c.this_user == c.user: +## Nothing +<% pass %>\ +% else: +${h.form(url(controller='user_settings', action='rel_toggle', name=c.user.name.lower()), method='POST')} +

+ + + % if UserRelationshipTypes.IS_WATCHING in c.relationships: + + + % else: + + + % endif +% endif + % for gallery in c.this_user.primary_page.galleries:

${gallery.string}

${macros.thumbs(gallery.search.results)} -% endfor \ No newline at end of file +% endfor diff --git a/floof/tests/functional/test_user_settings.py b/floof/tests/functional/test_user_settings.py new file mode 100644 index 0000000..b28f8be --- /dev/null +++ b/floof/tests/functional/test_user_settings.py @@ -0,0 +1,7 @@ +from floof.tests import * + +class TestUserSettingsController(TestController): + + def test_index(self): + response = self.app.get(url(controller='user_settings', action='index')) + # Test response...