fixed search query (awesome now thanks vee =]), tag links, default routing is back...
authorNick Retallack <nickretallack@gmil.com>
Mon, 5 Oct 2009 17:37:50 +0000 (10:37 -0700)
committerNick Retallack <nickretallack@gmil.com>
Mon, 5 Oct 2009 17:37:50 +0000 (10:37 -0700)
21 files changed:
ez_setup.py
floof/config/environment.py
floof/config/routing.py
floof/controllers/account.py
floof/controllers/art.py
floof/controllers/search.py
floof/controllers/users.py [new file with mode: 0644]
floof/forms/validators/unique.py
floof/lib/file_storage.py
floof/lib/fixtures.py
floof/lib/helpers.py
floof/model/art.py
floof/public/layout.css
floof/templates/account/register.mako [new file with mode: 0644]
floof/templates/art/show.mako
floof/templates/base.mako
floof/templates/users/index.mako [new file with mode: 0644]
floof/templates/users/view.mako [new file with mode: 0644]
floof/tests/__init__.py
floof/tests/functional/test_elixir.py
floof/tests/functional/test_users.py [new file with mode: 0644]

index d24e845..e33744b 100644 (file)
@@ -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:
index 3470f2a..cf6c77e 100644 (file)
@@ -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)
index f78246a..32093bb 100644 (file)
@@ -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}')
 
index 6c36310..e4f39f3 100644 (file)
@@ -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('/')
index 99027aa..7378f7a 100644 (file)
@@ -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)
index 5240a0b..abedd16 100644 (file)
@@ -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 (file)
index 0000000..1e251f0
--- /dev/null
@@ -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')
index 4fb3c8f..9b94869 100644 (file)
@@ -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
index 0b72690..90561d0 100644 (file)
@@ -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
+
index 0b2f3ec..8cec8ff 100644 (file)
@@ -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']
index 84c3394..28e9176 100644 (file)
@@ -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)
     """
index 7d93e6b..6fd2036 100644 (file)
@@ -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
         
index e32995e..74a3c05 100644 (file)
@@ -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 (file)
index 0000000..485012c
--- /dev/null
@@ -0,0 +1,12 @@
+<%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()}
index f2320fd..fc0a3f8 100644 (file)
@@ -11,7 +11,7 @@ ${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?
index 19f8455..f50f56a 100644 (file)
 <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>
diff --git a/floof/templates/users/index.mako b/floof/templates/users/index.mako
new file mode 100644 (file)
index 0000000..b16bbaf
--- /dev/null
@@ -0,0 +1,8 @@
+<%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>
diff --git a/floof/templates/users/view.mako b/floof/templates/users/view.mako
new file mode 100644 (file)
index 0000000..9f39bae
--- /dev/null
@@ -0,0 +1,3 @@
+<%inherit file="/base.mako" />
+
+<p>This is the userpage for ${c.this_user.name}.</p>
index 68cb322..fc14a48 100644 (file)
@@ -73,7 +73,7 @@ def teardown():
 class TestModel(TestCase):
     def setUp(self):
         setup_all(True)
-    
+
     def tearDown(self):
         drop_all(engine)
 
index bd0228b..1c907f0 100644 (file)
@@ -10,4 +10,4 @@ class TestElixir(TestModel):
 
     def test_session(self):
         assert Session.connection().dialect.name is 'sqlite'
-    
+
diff --git a/floof/tests/functional/test_users.py b/floof/tests/functional/test_users.py
new file mode 100644 (file)
index 0000000..cc1aeb1
--- /dev/null
@@ -0,0 +1,7 @@
+from floof.tests import *
+
+class TestUsersController(TestController):
+
+    def test_index(self):
+        response = self.app.get(url(controller='users', action='index'))
+        # Test response...