From: Nick Retallack Date: Mon, 5 Oct 2009 17:37:50 +0000 (-0700) Subject: fixed search query (awesome now thanks vee =]), tag links, default routing is back... X-Git-Url: http://git.veekun.com/zzz-floof.git/commitdiff_plain/55270a42bf0699bd78b95a945fddd73a165507ee?hp=11f3ff4140edcd5dd4d8e20f918c58003c634d10 fixed search query (awesome now thanks vee =]), tag links, default routing is back for now, merged in ratings. Tested stuff out, works nicely. This should be the definitive copy. --- diff --git a/ez_setup.py b/ez_setup.py index d24e845..e33744b 100644 --- a/ez_setup.py +++ b/ez_setup.py @@ -92,7 +92,7 @@ def use_setuptools( try: import pkg_resources except ImportError: - return do_download() + return do_download() try: pkg_resources.require("setuptools>="+version); return except pkg_resources.VersionConflict, e: diff --git a/floof/config/environment.py b/floof/config/environment.py index 3470f2a..cf6c77e 100644 --- a/floof/config/environment.py +++ b/floof/config/environment.py @@ -35,7 +35,7 @@ def load_environment(global_conf, app_conf): 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'): @@ -46,6 +46,6 @@ def load_environment(global_conf, app_conf): else: # Non-reflected tables model.init_model(engine) - + # CONFIGURATION OPTIONS HERE (note: all config options will override # any Pylons config options) diff --git a/floof/config/routing.py b/floof/config/routing.py index f78246a..32093bb 100644 --- a/floof/config/routing.py +++ b/floof/config/routing.py @@ -25,7 +25,17 @@ def make_map(): 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}') diff --git a/floof/controllers/account.py b/floof/controllers/account.py index 6c36310..e4f39f3 100644 --- a/floof/controllers/account.py +++ b/floof/controllers/account.py @@ -5,7 +5,7 @@ from openid.extensions.sreg import SRegRequest, SRegResponse 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 @@ -53,18 +53,17 @@ class AccountController(BaseController): 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 @@ -72,3 +71,49 @@ class AccountController(BaseController): # 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('/') diff --git a/floof/controllers/art.py b/floof/controllers/art.py index 99027aa..7378f7a 100644 --- a/floof/controllers/art.py +++ b/floof/controllers/art.py @@ -19,8 +19,8 @@ class ArtController(BaseController): def new(self): """ New Art! """ return render("/art/new.mako") - - + + def upload(self): print "PARAMS", request.params Art(uploaded_by=c.user, **request.params) diff --git a/floof/controllers/search.py b/floof/controllers/search.py index 5240a0b..abedd16 100644 --- a/floof/controllers/search.py +++ b/floof/controllers/search.py @@ -13,43 +13,16 @@ import elixir 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 diff --git a/floof/controllers/users.py b/floof/controllers/users.py new file mode 100644 index 0000000..1e251f0 --- /dev/null +++ b/floof/controllers/users.py @@ -0,0 +1,32 @@ +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') diff --git a/floof/forms/validators/unique.py b/floof/forms/validators/unique.py index 4fb3c8f..9b94869 100644 --- a/floof/forms/validators/unique.py +++ b/floof/forms/validators/unique.py @@ -1,5 +1,5 @@ from formencode import * -from formencode import validators +from formencode import validators import pylons _ = validators._ # dummy translation string @@ -14,29 +14,29 @@ class FilteringSchema(Schema): # 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: @@ -44,9 +44,9 @@ class Unique(validators.FancyValidator): 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 diff --git a/floof/lib/file_storage.py b/floof/lib/file_storage.py index 0b72690..90561d0 100644 --- a/floof/lib/file_storage.py +++ b/floof/lib/file_storage.py @@ -20,21 +20,21 @@ guess_type(temp.filename)[0] 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) @@ -52,7 +52,7 @@ def save_file(space, temp): 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 @@ -63,4 +63,4 @@ def makedirs(dir): os.makedirs(dir) except OSError: pass - \ No newline at end of file + diff --git a/floof/lib/fixtures.py b/floof/lib/fixtures.py index 0b2f3ec..8cec8ff 100644 --- a/floof/lib/fixtures.py +++ b/floof/lib/fixtures.py @@ -7,15 +7,15 @@ from floof import model as model """ 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: @@ -28,17 +28,17 @@ def load_data(model, filename=None, base_dir=None): 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) @@ -52,7 +52,7 @@ def _default_fixture_path_for_model(model, base_dir=None): 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: @@ -66,14 +66,14 @@ def _default_fixture_path_for_table(table, base_dir=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) @@ -86,11 +86,11 @@ def _load_data_from_file(model, filename): 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) @@ -104,7 +104,7 @@ def _load_data_to_table(tablename, filename): 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) @@ -116,9 +116,9 @@ def _dump_data_to_file(model, filename, **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(): @@ -135,5 +135,5 @@ def _dump_instance_to_dict(instance): for field in fields: d[field] = getattr(instance, field) return d - + __all__ = ['load_data', 'dump_data'] diff --git a/floof/lib/helpers.py b/floof/lib/helpers.py index 84c3394..28e9176 100644 --- a/floof/lib/helpers.py +++ b/floof/lib/helpers.py @@ -21,7 +21,7 @@ def get_object_or_404(model, **kw): 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) """ diff --git a/floof/model/art.py b/floof/model/art.py index 7d93e6b..6fd2036 100644 --- a/floof/model/art.py +++ b/floof/model/art.py @@ -13,13 +13,12 @@ from pylons import config 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): @@ -34,7 +33,7 @@ class Art(Entity): def set_file(self, file): self.hash = save_file("art", file) - + file = property(get_path, set_file) def get_path(self): @@ -57,8 +56,6 @@ class Art(Entity): 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) @@ -98,12 +95,12 @@ class Tag(Entity): 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 diff --git a/floof/public/layout.css b/floof/public/layout.css index e32995e..74a3c05 100644 --- a/floof/public/layout.css +++ b/floof/public/layout.css @@ -1,3 +1,6 @@ +/*** Main layout ***/ +body { font-family: sans-serif; font-size: 12px; } + #header { padding: 1em; background: #c0c0c0; } #header #user { text-align: right; } @@ -6,4 +9,14 @@ #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;} diff --git a/floof/templates/account/register.mako b/floof/templates/account/register.mako new file mode 100644 index 0000000..485012c --- /dev/null +++ b/floof/templates/account/register.mako @@ -0,0 +1,12 @@ +<%inherit file="/base.mako" /> + +

Registering from ${c.identity_url}.

+ +${h.form(url.current(action='register_finish'), method='POST')} +
+
Username
+
${h.text('username', value=c.username)}
+ +
${h.submit(None, 'Register')}
+
+${h.end_form()} diff --git a/floof/templates/art/show.mako b/floof/templates/art/show.mako index f2320fd..fc0a3f8 100644 --- a/floof/templates/art/show.mako +++ b/floof/templates/art/show.mako @@ -11,7 +11,7 @@ ${h.end_form()} % for tag in c.art.tags: x -${tag} +${tag} % endfor What do you think? diff --git a/floof/templates/base.mako b/floof/templates/base.mako index 19f8455..f50f56a 100644 --- a/floof/templates/base.mako +++ b/floof/templates/base.mako @@ -10,13 +10,15 @@