try:
import pkg_resources
except ImportError:
- return do_download()
+ return do_download()
try:
pkg_resources.require("setuptools>="+version); return
except pkg_resources.VersionConflict, e:
input_encoding='utf-8', output_encoding='utf-8',
imports=['from webhelpers.html import escape'],
default_filters=['escape'])
-
+
# Setup the SQLAlchemy^W Elixir database engine
engine = engine_from_config(config, 'sqlalchemy.')
if model.elixir.options_defaults.get('autoload'):
else:
# Non-reflected tables
model.init_model(engine)
-
+
# CONFIGURATION OPTIONS HERE (note: all config options will override
# any Pylons config options)
map.connect('/account/login', controller='account', action='login')
map.connect('/account/login_begin', controller='account', action='login_begin', **require_POST)
map.connect('/account/login_finish', controller='account', action='login_finish')
+ map.connect('/account/logout', controller='account', action='logout', **require_POST)
+ map.connect('/account/register', controller='account', action='register')
+ map.connect('/account/register_finish', controller='account', action='register_finish', **require_POST)
+ map.connect('/users', controller='users', action='list')
+ map.connect('/users/{name}', controller='users', action='view')
+
+ map.connect('/search', controller='search', action='index')
+
+ # default routing is back so we can test stuff.
+ # please don't take it away until we have some more core features in.
map.connect('/{controller}/{action}')
map.connect('/{controller}/{action}/{id}')
from openid.store.filestore import FileOpenIDStore
from sqlalchemy.orm.exc import NoResultFound
-from pylons import request, response, session, tmpl_context as c
+from pylons import request, response, session, tmpl_context as c, url
from pylons.controllers.util import abort, redirect_to
from routes import url_for, request_config
q = User.query.filter(User.identity_urls.any(url=res.identity_url))
user = q.one()
except NoResultFound:
+ # Unrecognized URL. Redirect to a registration page to ask for a
+ # nickname, etc.
+ session['register:identity_url'] = res.identity_url
+
# Try to pull a name out of the SReg response
sreg_res = SRegResponse.fromSuccessResponse(res)
- try:
- username = unicode(sreg_res['nickname'])
- except:
- username = u'Anonymous'
+ if sreg_res and 'nickname' in sreg_res:
+ session['register:nickname'] = sreg_res['nickname']
- # Create db records
- user = User(name=username)
- identity_url = IdentityURL(url=res.identity_url)
- user.identity_urls.append(identity_url)
- elixir.session.commit()
+ session.save()
+ redirect_to(url.current(action='register'))
# Remember who's logged in, and we're good to go
session['user_id'] = user.id
# XXX send me where I came from
redirect_to('/')
+
+ def logout(self):
+ """Log user out."""
+
+ if 'user_id' in session:
+ del session['user_id']
+ session.save()
+
+ # XXX success message
+ # XXX send me where I came from
+ redirect_to('/')
+
+ def register(self):
+ """Logging in with an unrecognized identity URL redirects here."""
+
+ c.identity_url = session['register:identity_url']
+ c.nickname = session.get('register:nickname', None)
+
+ return render('/account/register.mako')
+
+ def register_finish(self):
+ """Complete a new-user registration. Create the user and log in."""
+
+ 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.'
+
+ if User.query.filter_by(name=username).count():
+ return 'That username is taken.'
+
+ # Create db records
+ user = User(name=username)
+ user.identity_urls.append(IdentityURL(url=identity_url))
+ elixir.session.commit()
+
+ # Log in
+ session['user_id'] = user.id
+ session.save()
+
+ # XXX how do we do success messages in some useful way?
+ # XXX send me where I came from
+ redirect_to('/')
def new(self):
""" New Art! """
return render("/art/new.mako")
-
-
+
+
def upload(self):
print "PARAMS", request.params
Art(uploaded_by=c.user, **request.params)
class SearchController(BaseController):
def index(self):
- # Return a rendered template
- #return render('/search.mako')
- # or, return a response
- return 'Hello World'
-
- def results(self):
- """ Search, implemented the stupid way! """
- query = request.params.get('query','')
- words = query.split()
+ """Search, implemented the stupid way!"""
+ query = request.params.get('query', '')
+ tags = 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)
+ tagtext_ids = [_.id for _ in tagtexts]
+
+ # Fetch art that has all the tags
+ c.artwork = Art.query.join(Tag) \
+ .filter(Tag.tagtext_id.in_(tagtext_ids)) \
+ .all()
- # TODO: this is wrong. Please fix it so it returns art that has all the tags.
- art_tag_pairs = elixir.session.query(Art,Tag).filter(Art.id == Tag.art_id).\
- filter(Tag.tagtext_id.in_(tagtext_ids)).all()
-
- # just the art please.
- c.artwork = map(lambda x: x[0], art_tag_pairs)
return render('/index.mako')
\ No newline at end of file
--- /dev/null
+import logging
+
+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
+from floof.model.users import User
+
+log = logging.getLogger(__name__)
+
+class UsersController(BaseController):
+
+ def list(self):
+ """List of all users."""
+
+ # TODO paging!
+ c.users = User.query.all()
+
+ return render('/users/index.mako')
+
+ def view(self, name):
+ """Userpage."""
+
+ try:
+ c.this_user = User.query.filter(func.lower(User.name) == name) \
+ .one()
+ except NoResultFound:
+ abort(404)
+
+ return render('/users/view.mako')
from formencode import *
-from formencode import validators
+from formencode import validators
import pylons
_ = validators._ # dummy translation string
# Model-based validators
class Unique(validators.FancyValidator):
-
+
"""
Checks if given value is unique to the model.Will check the state: if state object
is the same as the instance, or the state contains a property with the same name
as the context name. For example:
-
+
validator = validators.Unique(model.NewsItem, "title", context_name="news_item")
-
+
This will check if there is an existing instance with the same "title". If there
is a matching instance, will check if the state passed into the validator is the
same instance, or if the state contains a property "news_item" which is the same
instance.
"""
-
+
__unpackargs__ = ('model', 'attr', "model_name", "context_name", "attribute_name")
messages = {
'notUnique' : _("%(modelName)s already exists with this %(attrName)s"),
}
-
+
model_name = "Item"
attribute_name = None
context_name = None
-
+
def validate_python(self, value, state):
instance = self.model.get_by(**{self.attr : value})
if instance:
if state != instance and \
getattr(state, context_name, None) != instance:
attr_name = self.attribute_name or self.attr
- raise Invalid(self.message('notUnique', state,
+ raise Invalid(self.message('notUnique', state,
modelName=self.model_name,
- attrName=attr_name),
+ attrName=attr_name),
value, state)
-
+
validators.Unique = Unique
def get_path(space, hash):
return "/" + os.path.join( space, hash[:2], hash[2:] )
-
-def save_file(space, temp):
-
+
+def save_file(space, temp):
+
dest_root = os.path.join( config['app_conf']['static_root'], space )
-
+
# we don't know where we're going to save this stuff yet,
# so I guess we'll write it to another tempfile. One we know the path of.
- # I'm assuming the tempfile we get from pylons is set to delete itself
+ # I'm assuming the tempfile we get from pylons is set to delete itself
# when it closes, and has no visible path. Maybe I'm wrong?
intermediate_file_descriptor, intermediate_path = tempfile.mkstemp()
-
+
# that function gives me an integer file descriptor for some reason.
intermediate_file = os.fdopen(intermediate_file_descriptor, "wb")
-
+
sha1 = hashlib.sha1()
while 1:
data = temp.file.read(chunk_size)
dest_path = os.path.join( dest_dir, hash[2:] )
makedirs(dest_dir)
- os.rename(intermediate_path, dest_path)
+ shutil.move(intermediate_path, dest_path)
return hash
os.makedirs(dir)
except OSError:
pass
-
\ No newline at end of file
+
"""
This module can be used for loading data into your models, for example when setting up default application data,
-unit tests, JSON export/import and importing/exporting legacy data. Data is serialized to and from the JSON format.
+unit tests, JSON export/import and importing/exporting legacy data. Data is serialized to and from the JSON format.
"""
VALID_FIXTURE_FILE_EXTENSIONS = ['.json']
def load_data(model, filename=None, base_dir=None):
- """Installs provided fixture files into given model. Filename may be directory, file or list of dirs or files. If filename is
- None, assumes that source file is located in fixtures/model_module_name/model_tablename.yaml of your application directory,
- for example MyProject/fixtures/news/newsitems.yaml. The base_dir argument is the top package of the application unless
+ """Installs provided fixture files into given model. Filename may be directory, file or list of dirs or files. If filename is
+ None, assumes that source file is located in fixtures/model_module_name/model_tablename.yaml of your application directory,
+ for example MyProject/fixtures/news/newsitems.yaml. The base_dir argument is the top package of the application unless
specified. You can also pass the name of a table instead of a model class."""
if type(model) is types.StringType:
def load_data_to_table(table, filename=None, base_dir=None):
"""Installs data directly into a table. Useful if table does not have a corresponding model, for example a many-to-many join table.
"""
-
+
if filename is None:
filename = _default_fixture_path_for_table(table, base_dir)
_load_data_to_table(table, filename)
-
+
def dump_data(model, filename=None, **params):
- """Dumps data to given destination. Params are optional arguments for selecting data. If filename is None, assumes that destination
- file is located in fixtures/model_module_name/model_name_lowercase.yaml of your application directory, for example
+ """Dumps data to given destination. Params are optional arguments for selecting data. If filename is None, assumes that destination
+ file is located in fixtures/model_module_name/model_name_lowercase.yaml of your application directory, for example
MyProject/fixtures/news/newsitem.yaml.
"""
-
+
if filename is None:
filename = _default_fixture_path_for_model(model)
_dump_data_to_file(model, filename, **params)
module_dirs = model.__module__.split('.', 2)[-1].split('.')
for dir in module_dirs:
path = os.path.join(path, dir)
- return os.path.join(path, model.table.name + '.json')
+ return os.path.join(path, model.table.name + '.json')
def _default_fixture_path_for_table(table, base_dir=None):
if base_dir is None:
def _is_fixture_file(filename):
basename, ext = os.path.splitext(filename)
return (ext.lower() in VALID_FIXTURE_FILE_EXTENSIONS)
-
+
def _load_data_from_dir(model, dirname):
for dirpath, dirnames, filenames in os.walk(dirname):
for filename in filenames:
_load_data_from_file(model, filename)
-
+
def _load_data_from_file(model, filename):
- if not _is_fixture_file(filename):
+ if not _is_fixture_file(filename):
return
fp = file(filename, 'r')
data = simplejson.load(fp)
elif type(data) is types.DictType:
retval = {}
for key, item in data.iteritems():
- retval[key] = _load_instance_from_dict(model, item)
+ retval[key] = _load_instance_from_dict(model, item)
return retval
def _load_data_to_table(tablename, filename):
- if not _is_fixture_file(filename):
+ if not _is_fixture_file(filename):
return
fp = file(filename, 'r')
data = simplejson.load(fp)
for key, item in data.iteritems():
table.insert(item).execute()
return data
-
+
def _dump_data_to_file(model, filename, **params):
if params:
queryset = model.select_by(**params)
fp = file(filename, 'w')
simplejson.dump(data, fp)
fp.close()
-
+
def _load_instance_from_dict(model, dict):
- if not dict: return
+ if not dict: return
instance = model()
fields = model._descriptor.fields.keys()
for k, v in dict.iteritems():
for field in fields:
d[field] = getattr(instance, field)
return d
-
+
__all__ = ['load_data', 'dump_data']
from pylons.controllers.util import abort
"""
Returns object, or raises a 404 Not Found is object is not in db.
- Uses elixir-specific `get_by()` convenience function (see elixir source:
+ Uses elixir-specific `get_by()` convenience function (see elixir source:
http://elixir.ematia.de/trac/browser/elixir/trunk/elixir/entity.py#L1082)
Example: user = get_object_or_404(model.User, id = 1)
"""
from floof.lib.file_storage import get_path, save_file
from floof.lib.dbhelpers import find_or_create, update_or_create
-
class Art(Entity):
title = Field(Unicode(120))
original_filename = Field(Unicode(120))
hash = Field(String)
- uploaded_by = ManyToOne('User')
+ uploaded_by = ManyToOne('User')
tags = OneToMany('Tag')
# def __init__(self, **kwargs):
def set_file(self, file):
self.hash = save_file("art", file)
-
+
file = property(get_path, set_file)
def get_path(self):
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)
if not self.tagtext:
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
+/*** Main layout ***/
+body { font-family: sans-serif; font-size: 12px; }
+
#header { padding: 1em; background: #c0c0c0; }
#header #user { text-align: right; }
#footer { padding: 1em; background: #c0c0c0; }
.full {display:block;}
-.selected {color:red;}
\ No newline at end of file
+
+/*** Common bits and pieces ***/
+/* General form layout */
+dl.form { margin: 1em 0; padding-left: 1em; border-left: 0.5em solid gray; }
+dl.form dt { padding-bottom: 0.25em; font-style: italic; }
+dl.form dd { margin-bottom: 0.5em; }
+
+
+
+/*** Individual page layout ***/
+.selected {color:red;}
--- /dev/null
+<%inherit file="/base.mako" />
+
+<p>Registering from <strong>${c.identity_url}</strong>.</p>
+
+${h.form(url.current(action='register_finish'), method='POST')}
+<dl class="form">
+ <dt>Username</dt>
+ <dd>${h.text('username', value=c.username)}</dd>
+
+ <dd>${h.submit(None, 'Register')}</dd>
+</dl>
+${h.end_form()}
% for tag in c.art.tags:
<a href="${h.url_for (controller='tag', action='delete', id=tag.id)}">x</a>
-<a href="${h.url_for (controller='search', action='results')}?query=${tag}">${tag}</a>
+<a href="${h.url_for (controller='search', action='index')}?query=${tag}">${tag}</a>
% endfor
What do you think?
<div id="header">
<a href="${h.url_for("/")}">Home</a>
-${h.form (h.url_for(controller="search", action="results"))}
-${h.text('query')} ${h.submit('search','Search')}
+${h.form(h.url_for(controller='search'), method='GET')}
+${h.text('query')} ${h.submit(None, 'Search')}
${h.end_form()}
<div id="user">
% if c.user:
- <p>Logged in as ${c.user.name}</p>
+ <form action="${url(controller='account', action='logout')}" method="POST">
+ <p>Logged in as ${c.user.name}. ${h.submit(None, 'Log out')}</p>
+ </form>
% else:
<form action="${url(controller='account', action='login_begin')}" method="POST">
<p>
--- /dev/null
+<%inherit file="/base.mako" />
+
+<ul>
+% for user in c.users:
+## TODO normalize URL names better perhaps
+ <li><a href="${url.current(action='view', name=user.name.lower())}">${user.name}</a></li>
+% endfor
+</ul>
--- /dev/null
+<%inherit file="/base.mako" />
+
+<p>This is the userpage for ${c.this_user.name}.</p>
class TestModel(TestCase):
def setUp(self):
setup_all(True)
-
+
def tearDown(self):
drop_all(engine)
def test_session(self):
assert Session.connection().dialect.name is 'sqlite'
-
+
--- /dev/null
+from floof.tests import *
+
+class TestUsersController(TestController):
+
+ def test_index(self):
+ response = self.app.get(url(controller='users', action='index'))
+ # Test response...