Skip to content

Commit

Permalink
[#2304] Refactor the followers backend a bit
Browse files Browse the repository at this point in the history
..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.
  • Loading branch information
Sean Hammond committed Apr 24, 2012
1 parent 8e12115 commit 8c188b9
Show file tree
Hide file tree
Showing 18 changed files with 239 additions and 163 deletions.
2 changes: 1 addition & 1 deletion ckan/config/routing.py
Expand Up @@ -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')
Expand Down
22 changes: 12 additions & 10 deletions ckan/controllers/package.py
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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', '')
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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')

Expand Down Expand Up @@ -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:
Expand Down
13 changes: 7 additions & 6 deletions ckan/controllers/user.py
Expand Up @@ -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__)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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')
3 changes: 3 additions & 0 deletions ckan/lib/dictization/model_dictize.py
Expand Up @@ -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)
7 changes: 7 additions & 0 deletions ckan/lib/dictization/model_save.py
Expand Up @@ -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
35 changes: 13 additions & 22 deletions ckan/logic/action/create.py
@@ -1,5 +1,4 @@
import logging
import datetime
from pylons.i18n import _

import ckan.lib.plugins as lib_plugins
Expand Down Expand Up @@ -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)

Expand All @@ -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)
26 changes: 26 additions & 0 deletions ckan/logic/action/delete.py
Expand Up @@ -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()
91 changes: 32 additions & 59 deletions ckan/logic/action/get.py
Expand Up @@ -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)
6 changes: 6 additions & 0 deletions ckan/logic/auth/delete.py
Expand Up @@ -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}
8 changes: 4 additions & 4 deletions ckan/logic/schema.py
Expand Up @@ -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

Expand Down Expand Up @@ -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
20 changes: 10 additions & 10 deletions ckan/logic/validators.py
Expand Up @@ -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)
20 changes: 0 additions & 20 deletions ckan/migration/versions/054_follower_table.py

This file was deleted.

0 comments on commit 8c188b9

Please sign in to comment.