--- /dev/null
+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.
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:
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')
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 *
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')
--- /dev/null
+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()
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__)
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')
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
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)
# 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
--- /dev/null
+"""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'])
# 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()
# 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):
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
+
% if c.user:
| <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(controller="art", action="watchstream", name=c.user.name.lower())}">Watchstream</a>
## | <a href="${h.url_for("/users/"+c.user}">Your Page</a>
% endif
-${h.form(h.url_for('search'), method='GET')}
+${h.form(url('search'), method='GET')}
${h.text('query', c.query)}
${h.submit('button', 'Search')}
<p>This is the userpage for ${c.this_user.name}.</p>
+<%! 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')}
+<p>
+ <input type="hidden" name="target_user" value="${c.this_user.id}">
+ <input type="hidden" name="type" value="${UserRelationshipTypes.IS_WATCHING}">
+ % if UserRelationshipTypes.IS_WATCHING in c.relationships:
+ <input type="hidden" name="add_remove" value="remove">
+ <input type="submit" value="Unwatch">
+ % else:
+ <input type="hidden" name="add_remove" value="add">
+ <input type="submit" value="Watch">
+ % endif
+% endif
+
% 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
+% endfor
--- /dev/null
+from floof.tests import *
+
+class TestUserSettingsController(TestController):
+
+ def test_index(self):
+ response = self.app.get(url(controller='user_settings', action='index'))
+ # Test response...