merged. Oh no, we have two different user relationship models. Mine's in relations...
authorNick Retallack <nickretallack@gmail.com>
Tue, 1 Dec 2009 09:18:21 +0000 (01:18 -0800)
committerNick Retallack <nickretallack@gmail.com>
Tue, 1 Dec 2009 09:18:21 +0000 (01:18 -0800)
13 files changed:
LICENSE [new file with mode: 0644]
floof/config/routing.py
floof/controllers/art.py
floof/controllers/user_settings.py [new file with mode: 0644]
floof/controllers/users.py
floof/lib/base.py
floof/model/comments.py
floof/model/forms.py [new file with mode: 0644]
floof/model/meta.py
floof/model/users.py
floof/templates/base.mako
floof/templates/users/view.mako
floof/tests/functional/test_user_settings.py [new file with mode: 0644]

diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
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.
index 32451e9..f6906f4 100644 (file)
@@ -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')
index be75ee7..41298c8 100644 (file)
@@ -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 (file)
index 0000000..8d8adf0
--- /dev/null
@@ -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()
index 1e251f0..1a79c15 100644 (file)
@@ -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')
index 169493c..b42d2be 100644 (file)
@@ -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)
index 5e57c14..ef3a948 100644 (file)
@@ -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 (file)
index 0000000..c316c38
--- /dev/null
@@ -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'])
index 1a20aa7..2992ae7 100644 (file)
@@ -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()
index 4f83a75..7601cf5 100644 (file)
@@ -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
+
index e4acc6f..60e8adb 100644 (file)
 % 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')}
 
index f9a9329..5b7e12d 100644 (file)
@@ -3,7 +3,25 @@
 
 <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
diff --git a/floof/tests/functional/test_user_settings.py b/floof/tests/functional/test_user_settings.py
new file mode 100644 (file)
index 0000000..b28f8be
--- /dev/null
@@ -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...