require_POST = dict(conditions={'method': ['POST']})
# get rid of trailing slashes
- map.redirect('/*(url)/', '/{url}',
- _redirect_code='301 Moved Permanently')
+ # map.redirect('/*(url)/', '/{url}',
+ # _redirect_code='301 Moved Permanently')
# The ErrorController route (handles 404/500 error pages); it should
c.identity_url = session['register:identity_url']
c.nickname = session.get('register:nickname', None)
+ # XXX hey, uh. warn if this name is taken already.
return render('/account/register.mako')
identity_url = session['register:identity_url']
username = request.params.get('username', None)
- # XXX how do we return errors in some useful way?
-
if not username:
- return 'Please enter a username.'
+ h.flash(u'Please enter a username.')
+ return self.register()
if User.query.filter_by(name=username).count():
- return 'That username is taken.'
+ h.flash(u'This username is already taken.')
+ return self.register()
+
+ if not User.is_valid_name(username):
+ h.flash(u'This username is not valid.')
+ return self.register()
# Create db records
- user = User(name=username)
+ user = User(name=username, display_name=username)
user.identity_urls.append(IdentityURL(url=identity_url))
elixir.session.commit()
# Log in
session['user_id'] = user.id
session.save()
+ h.flash(u'You are now logged in.')
- # XXX how do we do success messages in some useful way?
# XXX send me where I came from
redirect('/')
import logging
-from pylons import config, request, response, session, tmpl_context as c, h
+from pylons import config, request, response, session, tmpl_context as c
from pylons.controllers.util import abort, redirect
from pylons import url
from floof.lib.base import BaseController, render
log = logging.getLogger(__name__)
-from floof.lib import file_storage as storage
-from floof.model.users import User
-from floof.model import Art, Rating, UserRelation
+from floof.lib import file_storage as storage, helpers as h
+from floof.model import Art, Rating, ArtUser
+from floof.model.art import ArtUserType
from floof.model.comments import Discussion
from floof.model.users import User, UserRelationship
import os.path
import PIL
import PIL.Image
-from sqlalchemy import func
from sqlalchemy.exceptions import IntegrityError
from sqlalchemy.orm.exc import NoResultFound
from wtforms.validators import ValidationError
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):
)
c.art.discussion = Discussion(count=0)
- if c.form.by_me and c.user not in c.form.by.data:
- UserRelation(user=c.user, creator=c.user, kind="by", art=c.art)
-
- for artist in c.form.by.data:
- UserRelation(user=artist, creator=c.user, kind="by", art=c.art)
+ # <<<<<<< HEAD
+ # if c.form.by_me and c.user not in c.form.by.data:
+ # UserRelation(user=c.user, creator=c.user, kind="by", art=c.art)
+ #
+ # for artist in c.form.by.data:
+ # UserRelation(user=artist, creator=c.user, kind="by", art=c.art)
+ # =======
+ # For the moment, cheerfully assume that people are uploading their own
+ # art
+ ArtUser(art=c.art, user=c.user, type=ArtUserType.BY)
+ # >>>>>>> origin/master
try:
def watchstream(self, name):
"""Watchstream for a certain user."""
try:
- c.watching_user = User.query.filter(func.lower(User.name) == name) \
- .one()
+ c.watching_user = User.get_by(name=name)
except NoResultFound:
abort(404)
import logging
-from pylons import request, response, session, tmpl_context as c, h
+from pylons import request, response, session, tmpl_context as c
from pylons.controllers.util import abort, redirect
from pylons import url
+from floof.lib import helpers as h
from floof.lib.base import BaseController, render
log = logging.getLogger(__name__)
import logging
-from pylons import request, response, session, tmpl_context as c, h, url
+from pylons import request, response, session, tmpl_context as c, url
from pylons.controllers.util import abort, redirect
+from floof.lib import helpers as h
from floof.lib.base import BaseController, render
log = logging.getLogger(__name__)
import logging
-from pylons import request, response, session, tmpl_context as c, h
+from pylons import request, response, session, tmpl_context as c
from pylons.controllers.util import abort, redirect
from pylons import url
+from floof.lib import helpers as h
from floof.lib.base import BaseController, render
-from floof.lib.search import do_search
+from floof.lib.tags import parse
log = logging.getLogger(__name__)
return self.save()
c.query = request.params.get('query', '')
- c.artwork = do_search(c.query)
+ c.artwork = parse(c.query).all()
return render('/index.mako')
# TODO: login required
import logging
+import re
-from pylons import request, response, session, tmpl_context as c, h
+from pylons import request, response, session, tmpl_context as c, url
from pylons.controllers.util import abort, redirect
+import elixir
+from floof.model import Art, ArtUser, ArtUserType, Tag, TagText, User
+from floof.lib import helpers as h
from floof.lib.base import BaseController, render
-from pylons import url
+from floof.lib.tags import add_tags
+from floof.lib.dbhelpers import find_or_create
log = logging.getLogger(__name__)
-import elixir
-from floof.model import Art, Tag
-
class TagController(BaseController):
# TODO: login required
# TODO: login required
def create(self, art_id):
c.art = h.get_object_or_404(Art, id=art_id)
- c.art.add_tags(request.params.get("tags",""), c.user)
- elixir.session.commit()
- redirect(url('show_art', id=c.art.id))
+ tag_string = request.params.get('tags', '')
+ add_tags(c.art, tag_string, c.user)
+
+ # Add or remove tags
+ redirect(url('show_art', id=c.art.id))
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
`type`, and `add_remove` as parameters.
"""
try:
- user = User.query.filter(func.lower(User.name) == name).one()
+ user = User.get_by(name=name)
except NoResultFound:
abort(404)
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
from floof.lib.base import BaseController, render
"""Userpage."""
try:
- c.this_user = User.query.filter(func.lower(User.name) == name) \
- .one()
+ c.this_user = User.get_by(name=name)
except NoResultFound:
abort(404)
+++ /dev/null
-from floof.model import Art, Tag, TagText
-
-def do_search(query):
- tags = query.split()
-
- tagtexts = TagText.query.filter(TagText.text.in_(tags))
- tagtext_ids = [_.id for _ in tagtexts]
-
- # Fetch art that has all the tags
- artwork = Art.query.join(Tag) \
- .filter(Tag.tagtext_id.in_(tagtext_ids)) \
- .all()
- return artwork
-
-
-
-
-
-
-# unfinished stuff
-def parse(query):
- words = query.split()
-
- tags = []
- for word in words:
- components = word.split(':')
- if len(components) == 1:
- # tags are plain.
- tags.append(word)
- elif components[0] == "rating":
- if components[1].isnumeric():
- score = int(components[1])
- else:
- score = Rating.reverse_options.get(components[1])
-
- if -1 <= score <= 3:
- pass
- # TODO: Find stuff that has this rating
- # Rating.query.filter(Rating.s)
-
- tagtexts = TagText.query.filter(TagText.text.in_(tags))
- tagtext_ids = map(lambda x:x.id, tagtexts)
-
--- /dev/null
+from floof.model import Art, ArtUser, ArtUserType, Tag, TagText, User
+
+from dbhelpers import find_or_create
+import re
+def parse(search_string):
+ """Parses a search query, and returns a query object on Art.
+
+ Queries can contain:
+ - Regular tags: foo
+ - User relations: by:kalu, of:eevee, for:ootachi
+
+ Later:
+ - Negative versions of anything above: -by:eevee, -dongs
+ """
+
+ # XXX doesn't do negative querying yet.
+ # XXX could use some sane limits.
+
+ # We'll be building this as we go.
+ q = Art.query
+
+ terms = search_string.split()
+ for tag in terms:
+ if ':' in tag:
+ # This is a special tag; at the moment, by/for/of to indicate
+ # related users
+ prefix, tag = tag.split(':', 1)
+
+ # XXX what to do if this fails? abort? return empty query?
+ target_user = User.get_by(name=tag)
+
+ if prefix == 'by':
+ rel = ArtUserType.BY
+ elif prefix == 'for':
+ rel = ArtUserType.FOR
+ elif prefix == 'of':
+ rel = ArtUserType.OF
+ else:
+ # Bogus tag. Not sure what to do here, so for the moment,
+ # ignore it
+ continue
+
+ # Inner join to the ArtUser table
+ q = q.join(ArtUser, aliased=True) \
+ .filter(ArtUser.user == target_user) \
+ .filter(ArtUser.type == rel)
+
+ else:
+ # Regular ol' tag
+ q = q.join(Tag, TagText, aliased=True) \
+ .filter(TagText.text == tag)
+
+ return q
+
+def add_tags(art, tag_string, user):
+ """Takes a string that looks like a tag query, and effectively modifies the
+ art's tags to match it.
+ """
+
+ # XXX what to do with invalid tags? just return them and let caller fix?
+ bad_tags = []
+ for tag_text in tag_string.split():
+ original_tag_text = tag_text
+ tag_text = tag_text.lower()
+
+ # Adding or removing a tag?
+ if tag_text[0] == '-':
+ add = False
+ tag_text = tag_text[1:]
+ else:
+ # Allow "+foo" to mean "add foo"
+ if tag_text[0] == '+':
+ tag_text = tag_text[1:]
+ add = True
+
+ # Check for special namespaces
+ prefix = None
+ if ':' in tag_text:
+ prefix, tag_text = tag_text.split(':', 1)
+ if prefix not in ['by', 'for', 'of']:
+ # This is bogus. Skip it.
+ bad_tags.append(original_tag_text)
+ continue
+
+ if prefix == 'by':
+ # XXX this needs supporting. silently ignore for now
+ continue
+
+ # Must be 3-50 alphanumeric characters
+ if not re.match('^[a-z0-9]{3,50}$', tag_text):
+ bad_tags.append(original_tag_text)
+ continue
+
+ # Do work!
+ if prefix:
+ target_user = User.get_by(name=tag_text)
+
+ # Special tag; at the moment, just a relationship
+ if prefix == 'by':
+ rel = ArtUserType.BY
+ elif prefix == 'for':
+ rel = ArtUserType.FOR
+ elif prefix == 'of':
+ rel = ArtUserType.OF
+
+ user_assoc_data = dict(art=art, user=target_user, type=rel)
+ if add:
+ find_or_create(ArtUser, **user_assoc_data)
+
+ else:
+ # XXX this will die for nonassociations
+ user_assoc = ArtUser.get_by(art=art, **user_assoc_data)
+ user_assoc.delete()
+
+ else:
+ # Regular tag
+ if add:
+ tag = find_or_create(TagText, text=tag_text)
+ find_or_create(Tag, art=art, tagger=user, tagtext=tag)
+
+ else:
+ tag = TagText.get_by(text=tag_text)
+ if tag:
+ # XXX this will die
+ tag_assoc = Tag.get_by(art=art, tagger=user, tagtext=tag)
+ tag_assoc.delete()
+
+ elixir.session.commit()
# Copyright (c) 2009 Scribblr
#
-# from elixir import Entity, Field, Integer, Unicode
from elixir import *
import elixir
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
+from floof.model.users import User
-# 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))
tags = OneToMany('Tag')
discussion = ManyToOne('Discussion')
- user_relations = OneToMany('UserRelation')
+ art_users = OneToMany('ArtUser')
@property
def file_path(self):
return get_path("art", self.hash)
+
+ # Associated users
+ # XXX ok these could stand to do the filtering sql-side
+ @property
+ def artists(self):
+ return (au.user for au in self.art_users if au.type == ArtUserType.BY)
+
+ @property
+ def recipients(self):
+ return (au.user for au in self.art_users if au.type == ArtUserType.FOR)
+
+ @property
+ def participants(self):
+ return (au.user for au in self.art_users if au.type == ArtUserType.OF)
+
+class ArtUserType(object):
+ BY = 1
+ FOR = 2
+ OF = 3
+
+class ArtUser(Entity):
+ art = ManyToOne('Art', required=True)
+ user = ManyToOne('User', required=True)
+ type = Field(Integer, required=True) # ArtUserType
+
+ # TODO: admin log ought to remember who confirmed the relation.
+ # (tag history will remember who proposed it)
+++ /dev/null
-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,)
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)
-
class GalleryWidget(Entity):
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.
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, )
# Copyright (c) 2009 Scribblr
#
-# from elixir import Entity, Field, Unicode, belongs_to, has_many
+import re
+
from elixir import *
+
from search import GalleryWidget
class User(Entity):
- name = Field(Unicode(20))
+ name = Field(Unicode(20), unique=True)
+ display_name = Field(Unicode(20))
uploads = OneToMany('Art')
has_many('identity_urls', of_kind='IdentityURL')
searches = OneToMany('SavedSearch')
relationships = OneToMany('UserRelationship', inverse='user')
target_of_relationships = OneToMany('UserRelationship', inverse='target_user')
+ @classmethod
+ def is_valid_name(cls, name):
+ """Returns True iff the name is a valid username.
+
+ Only lowercase letters, numbers, and hyphens are allowed.
+
+ Names must also be at least one character long, but no more than 20,
+ and cannot start or end with a hyphen.
+ """
+ return re.match('^[-a-z0-9]{1,20}$', name) \
+ and name[0] != '-' and name[-1] != '-'
+
def __unicode__(self):
return self.name
-
+
def __str__(self):
return self.name
/*** Common bits and pieces ***/
h1 { margin: 0.5em 0 0.25em; font-size: 2em; border-bottom: 1px solid #404040; text-shadow: #a0a0a0 1px 1px 1px; }
+h2 { margin: 0.5em 0 0.25em; font-size: 1.5em; border-bottom: 1px dotted #606060; text-shadow: #a0a0a0 1px 1px 1px; }
a { color: #647cc4; font-weight: bold; text-decoration: none; pointer: cursor; }
a:visited { color: #48598e; }
<%inherit file="/base.mako" />
<h1>Add New Art</h1>
-<p>Now: Upload a file. Later: Supply a link? Not exclusive to uploading.</p>
-
-## Todo: write some macros to make outputting form fields easier.
-
${h.form(h.url('create_art'), multipart=True)}
-<div>
- ${normal_field(c.form.by)}
- ${checkbox_field(c.form.by_me)}
-</div>
-
-<div>${normal_field(c.form.file)}</div>
-
+##<<<<<<< HEAD
+##<div>
+## ${normal_field(c.form.by)}
+## ${checkbox_field(c.form.by_me)}
+##</div>
+##
+##<div>${normal_field(c.form.file)}</div>
+##=======
+## Todo: write some macros to make outputting form fields easier.
+${normal_field(c.form.file)}
+##>>>>>>> origin/master
-##Artist: ${h.text('artist')}
-##${h.file('file')}
${h.submit(None, 'Upload!')}
${h.end_form()}
<h2>Relations</h2>
<ul>
-% for relation in c.art.user_relations:
-<li>${relation.kind}: ${relation.user}
+% for label, relations in (('Artist', c.art.artists), \
+ ('Recipient', c.art.recipients), \
+ ('Participant', c.art.participants)):
+% for user in relations:
+<li>${label}: ${user.name}
+% endfor
% endfor
</ul>
-<h2>Add Relations</h2>
-${h.form (h.url("create_relation", kind="by", art_id=c.art.id))}
-By: ${h.text('username')}
-${h.submit('add','Add')}
-${h.end_form()}
-
<img class="full" src="${h.storage_url('art/medium', c.art.hash)}">
${comments.comment_block(c.art.discussion.comments)}
% endif
% endif
+<%! from floof.lib.search import parse %>
% for gallery in c.this_user.primary_page.galleries:
<h2>${gallery.string}</h2>
-${macros.thumbs(gallery.search.results)}
+${macros.thumbs(parse(gallery.tags.string))}
% endfor
# Users
from floof.model.users import IdentityURL, User
identity_url = IdentityURL(url=u'http://eevee.livejournal.com/')
- user = User(name=u'Eevee')
+ user = User(name=u'eevee')
user.identity_urls.append(identity_url)
model.Session.commit()