[ckan-changes] [okfn/ckan] 82a04c: [#2304] Add number of followers into Follow button...

GitHub noreply at github.com
Wed Apr 25 13:01:41 UTC 2012


  Branch: refs/heads/feature-2304-follow
  Home:   https://github.com/okfn/ckan
  Commit: 82a04c0c44de0419538dce32f48896aee8a59b85
      https://github.com/okfn/ckan/commit/82a04c0c44de0419538dce32f48896aee8a59b85
  Author: Sean Hammond <seanhammond at lavabit.com>
  Date:   2012-04-24 (Tue, 24 Apr 2012)

  Changed paths:
    M ckan/controllers/package.py
    M ckan/templates/package/layout.html

  Log Message:
  -----------
  [#2304] Add number of followers into Follow button on dataset pages

num_followers should probably be added to the package dict to avoid
having to add it to the template context in multiple places, but this'll
do for now.


diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py
index 51e7a62..953009e 100644
--- a/ckan/controllers/package.py
+++ b/ckan/controllers/package.py
@@ -371,6 +371,10 @@ def history(self, id):
         except NotFound:
             abort(404, _('Dataset not found'))
 
+        # Add the package's number of followers to the context for templates.
+        c.num_followers = ckan.logic.action.get.dataset_follower_count(
+                context, {'id':c.pkg.id})
+
         format = request.params.get('format', '')
         if format == 'atom':
             # Generate and return Atom 1.0 document.
@@ -489,6 +493,10 @@ def edit(self, id, data=None, errors=None, error_summary=None):
         else:
             c.form = render(self._package_form(package_type=package_type), extra_vars=vars)
 
+        # Add the package's number of followers to the context for templates.
+        c.num_followers = ckan.logic.action.get.dataset_follower_count(
+                context, {'id':c.pkg.id})
+
         if (c.action == u'editresources'):
           return render('package/editresources.html')
         else:
@@ -663,6 +671,11 @@ def authz(self, id):
 
         roles = self._handle_update_of_authz(pkg)
         self._prepare_authz_info_for_render(roles)
+
+        # Add the package's number of followers to the context for templates.
+        c.num_followers = ckan.logic.action.get.dataset_follower_count(
+                context, {'id':c.pkg.id})
+
         return render('package/authz.html')
 
     def autocomplete(self):
@@ -758,6 +771,8 @@ def followers(self, id=None):
             c.pkg = context['package']
             c.followers = get_action('dataset_follower_list')(context,
                     {'id': c.pkg_dict['id']})
+            c.num_followers = ckan.logic.action.get.dataset_follower_count(
+                    context, {'id':c.pkg.id})
         except NotFound:
             abort(404, _('Dataset not found'))
         except NotAuthorized:
diff --git a/ckan/templates/package/layout.html b/ckan/templates/package/layout.html
index fbc8ab5..6473c64 100644
--- a/ckan/templates/package/layout.html
+++ b/ckan/templates/package/layout.html
@@ -35,7 +35,13 @@
       </py:otherwise>
       </py:choose>
       <li class="${'active' if c.action=='history' else ''}">${h.subnav_link(h.icon('page_stack') + _('History'), controller='package', action='history', id=c.pkg.name)}</li>
-      <li class="${'active' if c.action=='followers' else ''}">${h.subnav_link(h.icon('authorization_group') + _('Followers'), controller='package', action='followers', id=c.pkg.name)}</li>
+      <li class="${'active' if c.action=='followers' else ''}">
+        ${h.subnav_link(
+            h.icon('authorization_group') + _('Followers ({num_followers})').format(num_followers=c.num_followers),
+            controller='package', 
+            action='followers',
+            id=c.pkg.name)}
+      </li>
       <py:if test="h.check_access('package_update',{'id':c.pkg.id})">
         <li class="divider">|</li>
         <li class="${'active' if c.action=='edit' else ''}">


================================================================
  Commit: b9ed12d8dd93d6e2b59c5bd0f156338a5d89ff7d
      https://github.com/okfn/ckan/commit/b9ed12d8dd93d6e2b59c5bd0f156338a5d89ff7d
  Author: Sean Hammond <seanhammond at lavabit.com>
  Date:   2012-04-24 (Tue, 24 Apr 2012)

  Changed paths:
    M ckan/controllers/user.py
    M ckan/templates/user/layout.html

  Log Message:
  -----------
  [#2304] Add number of followers into Follow button on user pages


diff --git a/ckan/controllers/user.py b/ckan/controllers/user.py
index 35e8ce2..1c5834d 100644
--- a/ckan/controllers/user.py
+++ b/ckan/controllers/user.py
@@ -421,4 +421,5 @@ def followers(self, id=None):
 
         c.user_dict = user_dict
         c.followers = user_follower_list(context, {'id':c.user_dict['id']})
+        c.num_followers = len(c.followers)
         return render('user/followers.html')
diff --git a/ckan/templates/user/layout.html b/ckan/templates/user/layout.html
index c428ce7..f980965 100644
--- a/ckan/templates/user/layout.html
+++ b/ckan/templates/user/layout.html
@@ -15,7 +15,13 @@
       <py:otherwise>
         <py:if test="c.id">
           <li class="${'active' if c.action=='read' else ''}"><a href="${h.url_for(controller='user', action='read', id=c.user_dict.name)}">View Profile</a></li>
-          <li class="${'active' if c.action=='followers' else ''}">${h.subnav_link(h.icon('authorization_group') + _('Followers'), controller='user', action='followers', id=c.user_dict.name)}</li>
+          <li class="${'active' if c.action=='followers' else ''}">
+            ${h.subnav_link(
+                h.icon('authorization_group') + _('Followers ({num_followers})').format(num_followers=c.num_followers),
+                controller='user',
+                action='followers',
+                id=c.user_dict.name)}
+          </li>
           <li><button userid="${c.user_dict.id}" class="btn user-follow">Follow</button></li>
         </py:if>
         <py:if test="not c.id">


================================================================
  Commit: a96301ce8f6a57bb92649c94f6953958aeaa4da5
      https://github.com/okfn/ckan/commit/a96301ce8f6a57bb92649c94f6953958aeaa4da5
  Author: Sean Hammond <seanhammond at lavabit.com>
  Date:   2012-04-24 (Tue, 24 Apr 2012)

  Changed paths:
    M ckan/controllers/package.py

  Log Message:
  -----------
  [#2304] Save an unnecessary db access


diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py
index 953009e..e76b701 100644
--- a/ckan/controllers/package.py
+++ b/ckan/controllers/package.py
@@ -771,8 +771,7 @@ def followers(self, id=None):
             c.pkg = context['package']
             c.followers = get_action('dataset_follower_list')(context,
                     {'id': c.pkg_dict['id']})
-            c.num_followers = ckan.logic.action.get.dataset_follower_count(
-                    context, {'id':c.pkg.id})
+            c.num_followers = len(c.followers)
         except NotFound:
             abort(404, _('Dataset not found'))
         except NotAuthorized:


================================================================
  Commit: 36fba00c0d93775af9f2371701fbe57f27c5f7a3
      https://github.com/okfn/ckan/commit/36fba00c0d93775af9f2371701fbe57f27c5f7a3
  Author: Sean Hammond <seanhammond at lavabit.com>
  Date:   2012-04-24 (Tue, 24 Apr 2012)

  Changed paths:
    M ckan/public/scripts/application.js
    M ckan/templates/package/layout.html

  Log Message:
  -----------
  [#2304] Add Follow button to dataset pages


diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js
index 82eb567..c530070 100644
--- a/ckan/public/scripts/application.js
+++ b/ckan/public/scripts/application.js
@@ -100,6 +100,10 @@ CKAN.Utils = CKAN.Utils || {};
       $( ".drag-drop-list" ).disableSelection();
     }
 
+    // This only needs to happen on dataset pages, but it doesn't seem to do
+    // any harm to call it anyway.
+    CKAN.Utils.setupDatasetFollowButton();
+
     var isGroupEdit = $('body.group.edit').length > 0;
     if (isGroupEdit) {
       var urlEditor = new CKAN.View.UrlEditor({
@@ -1248,7 +1252,6 @@ CKAN.Utils = function($, my) {
   };
 
   my.setupUserFollowButton = function() {
-    var select = $('button.user-follow');
     $('button.user-follow').click(function(e) {
       $.ajax({
         contentType: 'application/json',
@@ -1265,6 +1268,23 @@ CKAN.Utils = function($, my) {
     });
   };
 
+  my.setupDatasetFollowButton = function() {
+    $('button.dataset-follow').click(function(e) {
+      $.ajax({
+        contentType: 'application/json',
+        url: '/api/action/follower_create',
+        data: JSON.stringify({
+               followee_id: this.attributes.package_id.nodeValue,
+               followee_type: 'dataset',
+        }),
+        dataType: 'json',
+        processData: false,
+        type: 'POST',
+      });
+      return false;
+    });
+  };
+
   return my;
 }(jQuery, CKAN.Utils || {});
 
diff --git a/ckan/templates/package/layout.html b/ckan/templates/package/layout.html
index 6473c64..7d9e2fc 100644
--- a/ckan/templates/package/layout.html
+++ b/ckan/templates/package/layout.html
@@ -51,6 +51,7 @@
       <li class="${'active' if c.action=='authz' else ''}" py:if="h.check_access('package_edit_permissions',{'id':c.pkg.id})">
         ${h.subnav_link(h.icon('lock') + _('Authorization'), controller='package', action='authz', id=c.pkg.name)}
       </li>
+      <li><button package_id="${c.pkg.id}" class="btn dataset-follow">Follow</button></li>
     </ul>
   </py:match>
   


================================================================
  Commit: 8e1211549883d1b8789df40b61d55b5b30ef14e9
      https://github.com/okfn/ckan/commit/8e1211549883d1b8789df40b61d55b5b30ef14e9
  Author: Sean Hammond <seanhammond at lavabit.com>
  Date:   2012-04-24 (Tue, 24 Apr 2012)

  Changed paths:
    M ckan/templates/package/read.html

  Log Message:
  -----------
  [#2304] Remove follower count from dataset sidebar

It's in the Follow button now


diff --git a/ckan/templates/package/read.html b/ckan/templates/package/read.html
index f74ee8c..a9da991 100644
--- a/ckan/templates/package/read.html
+++ b/ckan/templates/package/read.html
@@ -54,10 +54,6 @@
       </ul>
     </li>
 
-    <li>
-      <h3>${c.num_followers} Followers</h3>
-    </li>
-
     <li py:if="c.package_relationships" class="sidebar-section">
       <h3>Related Datasets</h3>
       <ul class="related-datasets">


================================================================
  Commit: 8c188b98812d34f7f16a64388dfb02c3363a870c
      https://github.com/okfn/ckan/commit/8c188b98812d34f7f16a64388dfb02c3363a870c
  Author: Sean Hammond <seanhammond at lavabit.com>
  Date:   2012-04-24 (Tue, 24 Apr 2012)

  Changed paths:
    M ckan/config/routing.py
    M ckan/controllers/package.py
    M ckan/controllers/user.py
    M ckan/lib/dictization/model_dictize.py
    M ckan/lib/dictization/model_save.py
    M ckan/logic/action/create.py
    M ckan/logic/action/delete.py
    M ckan/logic/action/get.py
    M ckan/logic/auth/delete.py
    M ckan/logic/schema.py
    M ckan/logic/validators.py
    R ckan/migration/versions/054_follower_table.py
    M ckan/model/__init__.py
    M ckan/model/follower.py
    M ckan/public/scripts/application.js
    M ckan/templates/package/layout.html
    M ckan/templates/user/layout.html
    M ckan/tests/functional/api/test_follow.py

  Log Message:
  -----------
  [#2304] Refactor the followers backend a bit

..and also add follower_delete and make the Follow buttons turn into
Unfollow buttons when the user is following the object.

It now uses an ORM class Follower, where each follower has follower_id,
follower_type, object_id and object_type (got rid of the word 'followee'
because it was confusing, not that 'object' is very good either).

Got rid of user_follower_list, dataset_follower_list,
user_follower_count, dataset_follower_count, just have follower_list and
follower_count.

Don't bother letting people specify the follower when calling
follower_create or follower_delete just always use the authorized user.

Move some code from logic functions into follower model.


diff --git a/ckan/config/routing.py b/ckan/config/routing.py
index fa08e2c..0642a4e 100644
--- a/ckan/config/routing.py
+++ b/ckan/config/routing.py
@@ -236,7 +236,7 @@ def make_map():
         m.connect('/user/edit', action='edit')
         # Note: openid users have slashes in their ids, so need the wildcard
         # in the route.
-        m.connect('/user/{id:.*}/followers', action='followers')
+        m.connect('/user/followers/{id:.*}', action='followers')
         m.connect('/user/edit/{id:.*}', action='edit')
         m.connect('/user/reset/{id:.*}', action='perform_reset')
         m.connect('/user/register', action='register')
diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py
index e76b701..d43668f 100644
--- a/ckan/controllers/package.py
+++ b/ckan/controllers/package.py
@@ -20,7 +20,6 @@
 import ckan.authz
 import ckan.rating
 import ckan.misc
-import ckan.logic.action.get
 from home import CACHE_PARAMETER
 
 from ckan.lib.plugins import lookup_package_plugin
@@ -301,12 +300,15 @@ def read(self, id, format='html'):
         # template context for the package/read.html template to retrieve
         # later.
         c.package_activity_stream = \
-                ckan.logic.action.get.package_activity_list_html(context,
+                get_action('package_activity_list_html')(context,
                     {'id': c.current_package_id})
 
         # Add the package's number of followers to the context for templates.
-        c.num_followers = ckan.logic.action.get.dataset_follower_count(
-                context, {'id':c.pkg.id})
+        c.num_followers = get_action('follower_count')(context,
+                {'id':c.pkg.id})
+
+        c.am_following = get_action('am_following')(context,
+                {'id': c.pkg.id})
 
         PackageSaver().render_package(c.pkg_dict, context)
 
@@ -372,7 +374,7 @@ def history(self, id):
             abort(404, _('Dataset not found'))
 
         # Add the package's number of followers to the context for templates.
-        c.num_followers = ckan.logic.action.get.dataset_follower_count(
+        c.num_followers = get_action('follower_count')(
                 context, {'id':c.pkg.id})
 
         format = request.params.get('format', '')
@@ -494,8 +496,8 @@ def edit(self, id, data=None, errors=None, error_summary=None):
             c.form = render(self._package_form(package_type=package_type), extra_vars=vars)
 
         # Add the package's number of followers to the context for templates.
-        c.num_followers = ckan.logic.action.get.dataset_follower_count(
-                context, {'id':c.pkg.id})
+        c.num_followers = get_action('follower_count')(context,
+                {'id':c.pkg.id})
 
         if (c.action == u'editresources'):
           return render('package/editresources.html')
@@ -673,8 +675,8 @@ def authz(self, id):
         self._prepare_authz_info_for_render(roles)
 
         # Add the package's number of followers to the context for templates.
-        c.num_followers = ckan.logic.action.get.dataset_follower_count(
-                context, {'id':c.pkg.id})
+        c.num_followers = get_action('follower_count')(context,
+                {'id':c.pkg.id})
 
         return render('package/authz.html')
 
@@ -769,7 +771,7 @@ def followers(self, id=None):
         try:
             c.pkg_dict = get_action('package_show')(context, data_dict)
             c.pkg = context['package']
-            c.followers = get_action('dataset_follower_list')(context,
+            c.followers = get_action('follower_list')(context,
                     {'id': c.pkg_dict['id']})
             c.num_followers = len(c.followers)
         except NotFound:
diff --git a/ckan/controllers/user.py b/ckan/controllers/user.py
index 1c5834d..7009584 100644
--- a/ckan/controllers/user.py
+++ b/ckan/controllers/user.py
@@ -13,8 +13,6 @@
 from ckan.logic import check_access, get_action
 from ckan.logic import tuplize_dict, clean_dict, parse_params
 from ckan.logic.schema import user_new_form_schema, user_edit_form_schema
-from ckan.logic.action.get import user_activity_list_html
-from ckan.logic.action.get import user_follower_count, user_follower_list
 from ckan.lib.captcha import check_recaptcha, CaptchaError
 
 log = logging.getLogger(__name__)
@@ -100,10 +98,12 @@ def read(self, id=None):
         c.user_dict = user_dict
         c.is_myself = user_dict['name'] == c.user
         c.about_formatted = self._format_about(user_dict['about'])
-        c.user_activity_stream = user_activity_list_html(context,
-            {'id':c.user_dict['id']})
-        c.num_followers = user_follower_count(context,
+        c.user_activity_stream = get_action('user_activity_list_html')(
+                context, {'id':c.user_dict['id']})
+        c.num_followers = get_action('follower_count')(context,
                 {'id':c.user_dict['id']})
+        c.am_following = get_action('am_following')(context,
+                {'id': c.user_dict['id']})
         return render('user/read.html')
 
     def me(self, locale=None):
@@ -420,6 +420,7 @@ def followers(self, id=None):
             abort(401, _('Not authorized to see this page'))
 
         c.user_dict = user_dict
-        c.followers = user_follower_list(context, {'id':c.user_dict['id']})
+        c.followers = get_action('follower_list')(context,
+                {'id':c.user_dict['id']})
         c.num_followers = len(c.followers)
         return render('user/followers.html')
diff --git a/ckan/lib/dictization/model_dictize.py b/ckan/lib/dictization/model_dictize.py
index e3bbd1c..3cde55f 100644
--- a/ckan/lib/dictization/model_dictize.py
+++ b/ckan/lib/dictization/model_dictize.py
@@ -485,3 +485,6 @@ def tag_to_api2(tag, context):
     # DEPRICIATED set api_version in context and use tag_to_api()
     context['api_version'] = 2
     return tag_to_api(tag, context)
+
+def follower_dictize(follower, context):
+    return d.table_dictize(follower, context)
diff --git a/ckan/lib/dictization/model_save.py b/ckan/lib/dictization/model_save.py
index aa9f1a5..349ce85 100644
--- a/ckan/lib/dictization/model_save.py
+++ b/ckan/lib/dictization/model_save.py
@@ -562,3 +562,10 @@ def tag_dict_save(tag_dict, context):
         tag_dict['id'] = tag.id
     tag = d.table_dict_save(tag_dict, model.Tag, context)
     return tag
+
+def follower_dict_save(follower_dict, context):
+    model = context['model']
+    session = context['session']
+    follower_obj = model.Follower(**follower_dict)
+    session.add(follower_obj)
+    return follower_obj
diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py
index 57b27f2..94b4266 100644
--- a/ckan/logic/action/create.py
+++ b/ckan/logic/action/create.py
@@ -1,5 +1,4 @@
 import logging
-import datetime
 from pylons.i18n import _
 
 import ckan.lib.plugins as lib_plugins
@@ -486,15 +485,14 @@ def follower_create(context, follower_dict):
     schema = (context.get('schema')
             or ckan.logic.schema.default_create_follower_schema())
 
-    # If no follower_id is given in follower_dict, we use the logged-in user.
-    if not follower_dict.has_key('follower_id'):
-        if not context.has_key('user'):
-            raise logic.NotAuthorized
-        userobj = model.User.get(context['user'])
-        if not userobj:
-            raise logic.NotAuthorized
-        follower_dict['follower_id'] = userobj.id
-        follower_dict['follower_type'] = 'user'
+    # FIXME: Should the schema do this?
+    if not context.has_key('user'):
+        raise logic.NotAuthorized
+    userobj = model.User.get(context['user'])
+    if not userobj:
+        raise logic.NotAuthorized
+    follower_dict['follower_id'] = userobj.id
+    follower_dict['follower_type'] = 'user'
 
     check_access('follower_create', context, follower_dict)
 
@@ -504,19 +502,12 @@ def follower_create(context, follower_dict):
         model.Session.rollback()
         raise ValidationError(errors, error_summary(errors))
 
-    # FIXME: Maybe the schema should be doing this.
-    data['datetime'] = datetime.datetime.now()
-
-    follower_table = model.follower_table
-    insert = follower_table.insert().values(**data)
-    conn = model.Session.connection()
-    result = conn.execute(insert)
+    follower = model_save.follower_dict_save(follower_dict, context)
 
     if not context.get('defer_commit'):
-        model.Session.commit()
+        model.repo.commit()
 
-    log.debug('Created follower {follower} -> {followee}'.format(
-        follower=data['follower_id'], followee=data['followee_id']))
+    log.debug('Created follower {follower} -> {object}'.format(
+        follower=data['follower_id'], object=data['object_id']))
 
-    data['datetime'] = data['datetime'].isoformat()
-    return data
+    return model_dictize.follower_dictize(follower, context)
diff --git a/ckan/logic/action/delete.py b/ckan/logic/action/delete.py
index a8368ec..7579332 100644
--- a/ckan/logic/action/delete.py
+++ b/ckan/logic/action/delete.py
@@ -190,3 +190,29 @@ def package_relationship_delete_rest(context, data_dict):
     data_dict = ckan.logic.action.rename_keys(data_dict, key_map, destructive=True)
 
     package_relationship_delete(context, data_dict)
+
+def follower_delete(context, data_dict):
+    model = context['model']
+
+    if not context.has_key('user'):
+        raise ckan.logic.NotAuthorized
+    userobj = model.User.get(context['user'])
+    if not userobj:
+        raise ckan.logic.NotAuthorized
+    follower_id = userobj.id
+
+    object_id = data_dict.get('id')
+    if not object_id:
+        raise ValidationError({'id': _('id not in data')})
+
+    follower_obj = model.Follower.get(follower_id, object_id)
+    if follower_obj is None:
+        raise NotFound(
+                _('Could not find follower {follower} -> {object}').format(
+                    follower=follower_id, object=object_id))
+
+    check_access('follower_delete', context,
+            {'follower_id': follower_id, 'object_id':object_id})
+
+    follower_obj.delete()
+    model.repo.commit()
diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py
index 347b487..0ec0508 100644
--- a/ckan/logic/action/get.py
+++ b/ckan/logic/action/get.py
@@ -1317,68 +1317,41 @@ def recently_changed_packages_activity_list_html(context, data_dict):
     return _activity_list_to_html(context, activity_stream)
 
 def follower_count(context, data_dict):
+    '''Return the number of followers of an object.'''
     model = context['model']
-    followee_id = data_dict['id']
-    followee_type = data_dict['type']
-    follower_table = model.follower_table
-    q = select([func.count(follower_table.c.followee_id)])
-    q = q.where(follower_table.c.followee_id == followee_id)
-    q = q.where(follower_table.c.followee_type == followee_type)
-    conn = model.Session.connection()
-    cursor = conn.execute(q)
-    result_rows = cursor.fetchall()
-    assert len(result_rows) == 1
-    result_row = result_rows[0]
-    assert len(result_row) == 1
-    count = result_row[0]
-    return count
-
-def user_follower_count(context, data_dict):
-    return follower_count(context, {
-        'id': data_dict['id'],
-        'type': 'user',
-        })
-
-def dataset_follower_count(context, data_dict):
-    return follower_count(context, {
-        'id': data_dict['id'],
-        'type': 'dataset',
-        })
+    object_id = data_dict.get('id')
+    if not object_id:
+        raise ValidationError({'id': 'id not in data'})
+    return model.Follower.follower_count(object_id)
 
 def follower_list(context, data_dict):
-    '''Return a list of all of the followers of an object (such as a user or a
-    dataset.
+    '''Return a list of all of the followers of an object.'''
 
-    '''
+    # Get the list of Follower objects.
     model = context['model']
-    followee_id = data_dict['id']
-    followee_type = data_dict['type']
-    follower_table = model.follower_table
-    q = select((follower_table,))
-    q = q.where(follower_table.c.followee_id == followee_id)
-    q = q.where(follower_table.c.followee_type == followee_type)
-    conn = model.Session.connection()
-    cursor = conn.execute(q)
-    results = []
-    for row in cursor:
-        follower_id = row['follower_id']
-        assert row['follower_type'] == 'user', (
-                "Currently only users (and not other domain objects) are "
-                "supported as followers.")
-        user = model.User.get(follower_id)
-        results.append(model_dictize.user_dictize(user, context))
-    return results
+    object_id = data_dict.get('id')
+    if not object_id:
+        raise ValidationError({'id': 'id not in data'})
+    followers = model.Follower.follower_list(object_id)
+
+    # Convert the list of Follower objects to a list of User objects.
+    users = [model.User.get(follower.follower_id) for follower in followers]
+
+    # Dictize the list of user objects.
+    return [model_dictize.user_dictize(user,context) for user in users]
+
+def am_following(context, data_dict):
+    model = context['model']
+
+    object_id = data_dict.get('id')
+    if not object_id:
+        raise ValidationError({'id': 'id not in data'})
+
+    if not context.has_key('user'):
+        raise logic.NotAuthorized
+    userobj = model.User.get(context['user'])
+    if not userobj:
+        raise logic.NotAuthorized
+    follower_id = userobj.id
 
-def user_follower_list(context, data_dict):
-    '''Return a list a of all of a user's followers.'''
-    return follower_list(context, {
-        'id': data_dict['id'],
-        'type': 'user',
-        })
-
-def dataset_follower_list(context, data_dict):
-    '''Return a list a of all of a dataset's followers.'''
-    return follower_list(context, {
-        'id': data_dict['id'],
-        'type': 'dataset',
-        })
+    return model.Follower.is_following(follower_id, object_id)
diff --git a/ckan/logic/auth/delete.py b/ckan/logic/auth/delete.py
index 12937c2..402850d 100644
--- a/ckan/logic/auth/delete.py
+++ b/ckan/logic/auth/delete.py
@@ -64,3 +64,9 @@ def vocabulary_delete(context, data_dict):
 def tag_delete(context, data_dict):
     user = context['user']
     return {'success': Authorizer.is_sysadmin(user)}
+
+def follower_delete(context, data_dict):
+    model = context['model']
+    user = model.User.get(context['user'])
+    success = (user == model.User.get(data_dict['follower_id']))
+    return {'success': success}
diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py
index ded431a..fef6acb 100644
--- a/ckan/logic/schema.py
+++ b/ckan/logic/schema.py
@@ -39,7 +39,7 @@
                                    activity_type_exists,
                                    tag_not_in_vocabulary,
                                    follower_id_exists,
-                                   followee_id_exists)
+                                   follower_object_id_exists)
 from formencode.validators import OneOf
 import ckan.model
 
@@ -378,9 +378,9 @@ def default_create_follower_schema():
             'follower_id': [not_missing, not_empty, unicode,
                 follower_id_exists],
             'follower_type': [not_missing, not_empty, unicode],
-            'followee_id': [not_missing, not_empty, unicode,
-                followee_id_exists],
-            'followee_type': [not_missing, not_empty, unicode],
+            'object_id': [not_missing, not_empty, unicode,
+                follower_object_id_exists],
+            'object_type': [not_missing, not_empty, unicode],
             'datetime': [ignore]
             }
     return schema
diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py
index 6ab8eee..ee44cdd 100644
--- a/ckan/logic/validators.py
+++ b/ckan/logic/validators.py
@@ -491,17 +491,17 @@ def follower_id_exists(key, follower_dict, errors, context):
             type=follower_type))
     return validator(follower_id, context)
 
-def followee_id_exists(key, followee_dict, errors, context):
-    followee_id_validators = {
+def follower_object_id_exists(key, object_dict, errors, context):
+    object_id_validators = {
             'user': user_id_exists,
             'dataset': package_id_exists,
             }
-    followee_id = followee_dict[('followee_id',)]
-    followee_type = followee_dict.get(('followee_type',))
-    if not followee_type:
-        raise Invalid(_('Not found: {0}').format('followee_type'))
-    validator = followee_id_validators.get(followee_type)
+    object_id = object_dict[('object_id',)]
+    object_type = object_dict.get(('object_type',))
+    if not object_type:
+        raise Invalid(_('Not found: {0}').format('object_type'))
+    validator = object_id_validators.get(object_type)
     if not validator:
-        raise Invalid(_('followee_type {type} not recognised').format(
-            type=followee_type))
-    return validator(followee_id, context)
+        raise Invalid(_('object_type {type} not recognised').format(
+            type=object_type))
+    return validator(object_id, context)
diff --git a/ckan/migration/versions/054_follower_table.py b/ckan/migration/versions/054_follower_table.py
deleted file mode 100644
index 9083ddf..0000000
--- a/ckan/migration/versions/054_follower_table.py
+++ /dev/null
@@ -1,20 +0,0 @@
-import sqlalchemy
-import ckan
-
-follower_table = sqlalchemy.Table('follower',
-        sqlalchemy.MetaData(),
-    sqlalchemy.Column('follower_id', sqlalchemy.types.UnicodeText,
-        nullable=False, primary_key=True),
-    sqlalchemy.Column('follower_type', sqlalchemy.types.UnicodeText,
-        nullable=False),
-    sqlalchemy.Column('followee_id', sqlalchemy.types.UnicodeText,
-        nullable=False, primary_key=True),
-    sqlalchemy.Column('followee_type', sqlalchemy.types.UnicodeText,
-        nullable=False),
-    sqlalchemy.Column('datetime', sqlalchemy.types.DateTime, nullable=False),
-)
-
-def upgrade(migrate_engine):
-    meta = sqlalchemy.MetaData()
-    meta.bind = migrate_engine
-    ckan.model.follower.follower_table.create()
diff --git a/ckan/model/__init__.py b/ckan/model/__init__.py
index d37767e..91037f2 100644
--- a/ckan/model/__init__.py
+++ b/ckan/model/__init__.py
@@ -28,7 +28,7 @@
 from vocabulary import *
 from activity import *
 from term_translation import *
-from follower import follower_table
+from follower import Follower
 import ckan.migration
 from ckan.lib.helpers import OrderedDict, datetime_to_date_str
 from vdm.sqlalchemy.base import SQLAlchemySession
diff --git a/ckan/model/follower.py b/ckan/model/follower.py
index 9597f0d..293a507 100644
--- a/ckan/model/follower.py
+++ b/ckan/model/follower.py
@@ -1,16 +1,69 @@
 import sqlalchemy
-from meta import metadata
+import core
+import meta
+import datetime
 
-# FIXME: Should follower_type and followeee_type be part of the primary key too?
+# FIXME: Should follower_type and object_type be part of the primary key too?
+# FIXME: Should follower rows be automatically deleted when the objects are deleted?
 follower_table = sqlalchemy.Table('follower',
-        metadata,
+        meta.metadata,
     sqlalchemy.Column('follower_id', sqlalchemy.types.UnicodeText,
         nullable=False, primary_key=True),
     sqlalchemy.Column('follower_type', sqlalchemy.types.UnicodeText,
         nullable=False),
-    sqlalchemy.Column('followee_id', sqlalchemy.types.UnicodeText,
+    sqlalchemy.Column('object_id', sqlalchemy.types.UnicodeText,
         nullable=False, primary_key=True),
-    sqlalchemy.Column('followee_type', sqlalchemy.types.UnicodeText,
+    sqlalchemy.Column('object_type', sqlalchemy.types.UnicodeText,
         nullable=False),
     sqlalchemy.Column('datetime', sqlalchemy.types.DateTime, nullable=False),
 )
+
+class Follower(core.DomainObject):
+    '''A follower relationship between one domain object and another.
+
+    A Follower is a relationship between one domain object, the follower (e.g.
+    a user), and another domain object, the object (e.g. another user or a
+    dataset). The follower relationship declares that one object is currently
+    following another. For example, a user may follow another user or a
+    dataset.
+
+    '''
+    def __init__(self, follower_id, follower_type, object_id, object_type):
+        self.follower_id = follower_id
+        self.follower_type = follower_type
+        self.object_id = object_id
+        self.object_type = object_type
+        self.datetime = datetime.datetime.now()
+
+    @classmethod
+    def get(self, follower_id, object_id):
+        '''Return a Follower object for the given follower_id and object_id,
+        or None if no such follower exists.
+
+        '''
+        query = meta.Session.query(Follower)
+        query = query.filter(Follower.follower_id==follower_id)
+        query = query.filter(Follower.object_id==object_id)
+        return query.first()
+
+    @classmethod
+    def follower_count(cls, object_id):
+        '''Return the number of followers of an object.'''
+        return meta.Session.query(Follower).filter(
+                Follower.object_id == object_id).count()
+
+    @classmethod
+    def follower_list(cls, object_id):
+        '''Return a list of all of the followers of an object.'''
+        return meta.Session.query(Follower).filter(
+                Follower.object_id == object_id).all()
+
+    @classmethod
+    def is_following(cls, follower_id, object_id):
+        '''Return True if follower_id is currently following object_id, False
+        otherwise.
+
+        '''
+        return Follower.get(follower_id, object_id) is not None
+
+core.mapper(Follower, follower_table)
diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js
index c530070..c7bd375 100644
--- a/ckan/public/scripts/application.js
+++ b/ckan/public/scripts/application.js
@@ -40,11 +40,6 @@ CKAN.Utils = CKAN.Utils || {};
       CKAN.Utils.setupNotesExtract();
     }
 
-    var isUserView = $('body.user.read').length > 0;
-    if (isUserView) {
-      CKAN.Utils.setupUserFollowButton();
-    }
-
     var isResourceView = $('body.package.resource_read').length > 0;
     if (isResourceView) {
       CKAN.DataPreview.loadPreviewDialog(preload_resource);
@@ -103,6 +98,9 @@ CKAN.Utils = CKAN.Utils || {};
     // This only needs to happen on dataset pages, but it doesn't seem to do
     // any harm to call it anyway.
     CKAN.Utils.setupDatasetFollowButton();
+    CKAN.Utils.setupDatasetUnfollowButton();
+    CKAN.Utils.setupUserFollowButton();
+    CKAN.Utils.setupUserUnfollowButton();
 
     var isGroupEdit = $('body.group.edit').length > 0;
     if (isGroupEdit) {
@@ -1257,8 +1255,24 @@ CKAN.Utils = function($, my) {
         contentType: 'application/json',
         url: '/api/action/follower_create',
         data: JSON.stringify({
-               followee_id: this.attributes.userid.nodeValue,
-               followee_type: 'user',
+               object_id: this.attributes.userid.nodeValue,
+               object_type: 'user',
+        }),
+        dataType: 'json',
+        processData: false,
+        type: 'POST',
+      });
+      return false;
+    });
+  };
+
+  my.setupUserUnfollowButton = function() {
+    $('button.user-unfollow').click(function(e) {
+      $.ajax({
+        contentType: 'application/json',
+        url: '/api/action/follower_delete',
+        data: JSON.stringify({
+               id: this.attributes.userid.nodeValue,
         }),
         dataType: 'json',
         processData: false,
@@ -1274,8 +1288,24 @@ CKAN.Utils = function($, my) {
         contentType: 'application/json',
         url: '/api/action/follower_create',
         data: JSON.stringify({
-               followee_id: this.attributes.package_id.nodeValue,
-               followee_type: 'dataset',
+               object_id: this.attributes.package_id.nodeValue,
+               object_type: 'dataset',
+        }),
+        dataType: 'json',
+        processData: false,
+        type: 'POST',
+      });
+      return false;
+    });
+  };
+
+  my.setupDatasetUnfollowButton = function() {
+    $('button.dataset-unfollow').click(function(e) {
+      $.ajax({
+        contentType: 'application/json',
+        url: '/api/action/follower_delete',
+        data: JSON.stringify({
+               id: this.attributes.package_id.nodeValue,
         }),
         dataType: 'json',
         processData: false,
diff --git a/ckan/templates/package/layout.html b/ckan/templates/package/layout.html
index 7d9e2fc..e86342d 100644
--- a/ckan/templates/package/layout.html
+++ b/ckan/templates/package/layout.html
@@ -51,7 +51,10 @@
       <li class="${'active' if c.action=='authz' else ''}" py:if="h.check_access('package_edit_permissions',{'id':c.pkg.id})">
         ${h.subnav_link(h.icon('lock') + _('Authorization'), controller='package', action='authz', id=c.pkg.name)}
       </li>
-      <li><button package_id="${c.pkg.id}" class="btn dataset-follow">Follow</button></li>
+      <py:choose test="c.am_following">
+        <li py:when="True"><button package_id="${c.pkg.id}" class="btn dataset-unfollow">Unfollow</button></li>
+        <li py:otherwise=""><button package_id="${c.pkg.id}" class="btn dataset-follow">Follow</button></li>
+      </py:choose>
     </ul>
   </py:match>
   
diff --git a/ckan/templates/user/layout.html b/ckan/templates/user/layout.html
index f980965..8e01eb2 100644
--- a/ckan/templates/user/layout.html
+++ b/ckan/templates/user/layout.html
@@ -22,7 +22,10 @@
                 action='followers',
                 id=c.user_dict.name)}
           </li>
-          <li><button userid="${c.user_dict.id}" class="btn user-follow">Follow</button></li>
+          <py:choose test="c.am_following">
+            <li py:when="True"><button userid="${c.user_dict.id}" class="btn user-unfollow">Unfollow</button></li>
+            <li py:otherwise=""><button userid="${c.user_dict.id}" class="btn user-follow">Follow</button></li>
+          </py:choose>
         </py:if>
         <py:if test="not c.id">
           <li class="${'active' if c.action=='login' else ''}"><a href="${h.url_for(controller='user', action='login')}">Login</a></li>
diff --git a/ckan/tests/functional/api/test_follow.py b/ckan/tests/functional/api/test_follow.py
index f2144d4..18b40cf 100644
--- a/ckan/tests/functional/api/test_follow.py
+++ b/ckan/tests/functional/api/test_follow.py
@@ -35,7 +35,7 @@ def test_user_follow_user(self):
 
         # Record the users number of followers before.
         params = json.dumps({'id': self.russianfan.id})
-        response = self.app.post('/api/action/user_follower_count',
+        response = self.app.post('/api/action/follower_count',
                 params=params).json
         assert response['success'] is True
         count_before = response['result']
@@ -43,10 +43,8 @@ def test_user_follow_user(self):
         # Make one user a follower of another user.
         before = datetime.datetime.now()
         params = json.dumps({
-            'follower_id': self.annafan.id,
-            'follower_type': 'user',
-            'followee_id': self.russianfan.id,
-            'followee_type': 'user',
+            'object_id': self.russianfan.id,
+            'object_type': 'user',
             })
         extra_environ = {
                 'Authorization': str(self.annafan.apikey)
@@ -59,14 +57,14 @@ def test_user_follow_user(self):
         follower = response['result']
         assert follower['follower_id'] == self.annafan.id
         assert follower['follower_type'] == 'user'
-        assert follower['followee_id'] == self.russianfan.id
-        assert follower['followee_type'] == 'user'
+        assert follower['object_id'] == self.russianfan.id
+        assert follower['object_type'] == 'user'
         timestamp = datetime_from_string(follower['datetime'])
         assert (timestamp >= before and timestamp <= after), str(timestamp)
 
-        # Check that the follower appears in the followee's list of followers.
+        # Check that the follower appears in the object's list of followers.
         params = json.dumps({'id': self.russianfan.id})
-        response = self.app.post('/api/action/user_follower_list',
+        response = self.app.post('/api/action/follower_list',
                 params=params).json
         assert response['success'] is True
         assert response['result']
@@ -77,7 +75,7 @@ def test_user_follow_user(self):
 
         # Check that the user's follower count has increased by 1.
         params = json.dumps({'id': self.russianfan.id})
-        response = self.app.post('/api/action/user_follower_count',
+        response = self.app.post('/api/action/follower_count',
                 params=params).json
         assert response['success'] is True
         assert response['result'] == count_before + 1
@@ -98,16 +96,16 @@ def test_follower_type_bad(self):
     def test_follower_type_missing(self):
         raise NotImplementedError
 
-    def test_followee_id_bad(self):
+    def test_object_id_bad(self):
         raise NotImplementedError
 
-    def test_followee_id_missing(self):
+    def test_object_id_missing(self):
         raise NotImplementedError
 
-    def test_followee_type_bad(self):
+    def test_object_type_bad(self):
         raise NotImplementedError
 
-    def test_followee_type_missing(self):
+    def test_object_type_missing(self):
         raise NotImplementedError
 
     def test_follow_with_datetime(self):


================================================================
  Commit: 8244875bcabdd94e17d08c242c0cf48963043802
      https://github.com/okfn/ckan/commit/8244875bcabdd94e17d08c242c0cf48963043802
  Author: Sean Hammond <seanhammond at lavabit.com>
  Date:   2012-04-25 (Wed, 25 Apr 2012)

  Changed paths:
    M ckan/controllers/user.py
    M ckan/logic/auth/create.py
    M ckan/templates/user/layout.html

  Log Message:
  -----------
  [#2304] Don't show Follow button on user pages if not authorized to follow

e.g. if not logged in


diff --git a/ckan/controllers/user.py b/ckan/controllers/user.py
index 7009584..f0a16d5 100644
--- a/ckan/controllers/user.py
+++ b/ckan/controllers/user.py
@@ -49,6 +49,35 @@ def _db_to_edit_form_schema(self):
     def _setup_template_variables(self, context):
         c.is_sysadmin = Authorizer().is_sysadmin(c.user)
 
+    def _setup_follow_button(self, context):
+        '''Setup some template context variables needed for the Follow/Unfollow
+        button.
+
+        '''
+
+        # If the user is logged in set the am_following variable.
+        userid = context.get('user')
+        if not userid:
+            return
+        userobj = model.User.get(userid)
+        if not userobj:
+            return
+        c.user_dict['am_following'] = get_action('am_following')(context,
+                {'id': c.user_dict['id']})
+
+        # If the user is authorized set the authorized_to_follow variable.
+        try:
+            data_dict = {
+                    'follower_id': userobj.id,
+                    'follower_type': 'user',
+                    'object_id': c.user_dict['id'],
+                    'object_type': 'user',
+                    }
+            check_access('follower_create', context, data_dict)
+            c.authorized_to_follow = True
+        except NotAuthorized:
+            pass
+
     ## end hooks
 
     def index(self):
@@ -102,8 +131,7 @@ def read(self, id=None):
                 context, {'id':c.user_dict['id']})
         c.num_followers = get_action('follower_count')(context,
                 {'id':c.user_dict['id']})
-        c.am_following = get_action('am_following')(context,
-                {'id': c.user_dict['id']})
+        self._setup_follow_button(context)
         return render('user/read.html')
 
     def me(self, locale=None):
@@ -423,4 +451,5 @@ def followers(self, id=None):
         c.followers = get_action('follower_list')(context,
                 {'id':c.user_dict['id']})
         c.num_followers = len(c.followers)
+        self._setup_follow_button(context)
         return render('user/followers.html')
diff --git a/ckan/logic/auth/create.py b/ckan/logic/auth/create.py
index bba6abf..0e1f254 100644
--- a/ckan/logic/auth/create.py
+++ b/ckan/logic/auth/create.py
@@ -144,6 +144,11 @@ def tag_create(context, data_dict):
 
 def follower_create(context, follower_dict):
     model = context['model']
-    user = model.User.get(context['user'])
-    success = (user == model.User.get(follower_dict['follower_id']))
+    userid = context.get('user')
+    if not userid:
+        return {'success': False}
+    userobj = model.User.get(userid)
+    if not userobj:
+        return {'success': False}
+    success = (userobj == model.User.get(follower_dict['follower_id']))
     return {'success': success}
diff --git a/ckan/templates/user/layout.html b/ckan/templates/user/layout.html
index 8e01eb2..f52056c 100644
--- a/ckan/templates/user/layout.html
+++ b/ckan/templates/user/layout.html
@@ -22,10 +22,12 @@
                 action='followers',
                 id=c.user_dict.name)}
           </li>
-          <py:choose test="c.am_following">
-            <li py:when="True"><button userid="${c.user_dict.id}" class="btn user-unfollow">Unfollow</button></li>
-            <li py:otherwise=""><button userid="${c.user_dict.id}" class="btn user-follow">Follow</button></li>
-          </py:choose>
+          <li py:if="c.authorized_to_follow">
+            <py:choose test="c.user_dict.am_following">
+              <button py:when="True" userid="${c.user_dict.id}" class="btn user-unfollow">Unfollow</button>
+              <button py:otherwise="" userid="${c.user_dict.id}" class="btn user-follow">Follow</button>
+            </py:choose>
+          </li>
         </py:if>
         <py:if test="not c.id">
           <li class="${'active' if c.action=='login' else ''}"><a href="${h.url_for(controller='user', action='login')}">Login</a></li>


================================================================
Compare: https://github.com/okfn/ckan/compare/147a3e5...8244875


More information about the ckan-changes mailing list