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)
1  2 
floof/config/routing.py
floof/controllers/art.py
floof/model/users.py

diff --combined floof/config/routing.py
@@@ -42,9 -42,13 +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:
          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')
  
 +    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 --combined floof/controllers/art.py
@@@ -8,58 -8,14 +8,61 @@@ from floof.lib.base import BaseControll
  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
  
+ 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):
  
      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)
 -
 -        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.form = ArtUploadForm(request.params)
 +        if c.form.validate():
 +
 +            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):
          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 --combined floof/model/users.py
@@@ -16,13 -16,12 +16,15 @@@ 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):
          return self.name
 +    
 +    def __str__(self):
 +        return self.name
  
      def __init__(self, **kwargs):
          super(User, self).__init__(**kwargs)
@@@ -48,17 -47,31 +50,31 @@@ 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)
  
      visible = Field(Boolean)
      galleries = OneToMany('GalleryWidget')
-     
-     
-     
-     
- # class ArtRelation(Entity):
- #     
+ 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
++