[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