[ckan-changes] [okfn/ckan] c38ef4: [#2304] Initial model and api for following featur...
GitHub
noreply at github.com
Thu Apr 19 16:44:51 UTC 2012
Branch: refs/heads/feature-2304-follow
Home: https://github.com/okfn/ckan
Commit: c38ef44ddb1dad67b34412c577990093a1a9e44a
https://github.com/okfn/ckan/commit/c38ef44ddb1dad67b34412c577990093a1a9e44a
Author: Sean Hammond <seanhammond at lavabit.com>
Date: 2012-04-19 (Thu, 19 Apr 2012)
Changed paths:
M ckan/logic/action/create.py
M ckan/logic/action/get.py
M ckan/logic/auth/create.py
M ckan/logic/schema.py
M ckan/logic/validators.py
A ckan/migration/versions/054_follower_table.py
M ckan/model/__init__.py
A ckan/model/follower.py
A ckan/tests/functional/api/test_follow.py
Log Message:
-----------
[#2304] Initial model and api for following feature
Add ckan/model/follower.py with follower_table definition.
Add follower_create logic action and auth functions.
Add default_create_follower_schema.
Add follower_list, user_follower_list and dataset_follower_list logic
action functions.
Add follower_table migration script.
Add incomplete tests in ckan/tests/functional/api/test_follow.py.
diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py
index d142dcf..3b00428 100644
--- a/ckan/logic/action/create.py
+++ b/ckan/logic/action/create.py
@@ -1,4 +1,5 @@
import logging
+import datetime
from pylons.i18n import _
import ckan.lib.plugins as lib_plugins
@@ -478,3 +479,34 @@ def tag_create(context, tag_dict):
log.debug("Created tag '%s' " % tag)
return model_dictize.tag_dictize(tag, context)
+
+def follower_create(context, follower_dict):
+
+ model = context['model']
+ schema = (context.get('schema')
+ or ckan.logic.schema.default_create_follower_schema())
+
+ check_access('follower_create', context, follower_dict)
+
+ data, errors = validate(follower_dict, schema, context)
+
+ if errors:
+ 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)
+
+ if not context.get('defer_commit'):
+ model.Session.commit()
+
+ log.debug('Created follower {follower} -> {followee}'.format(
+ follower=data['follower_id'], followee=data['followee_id']))
+
+ data['datetime'] = data['datetime'].isoformat()
+ return data
diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py
index 21429d3..17361dc 100644
--- a/ckan/logic/action/get.py
+++ b/ckan/logic/action/get.py
@@ -1268,3 +1268,36 @@ def recently_changed_packages_activity_list_html(context, data_dict):
activity_stream = recently_changed_packages_activity_list(context,
data_dict)
return _activity_list_to_html(context, activity_stream)
+
+def follower_list(context, data_dict):
+ '''Return a list of all of the followers of an object (such as a user or a
+ dataset.
+
+ '''
+ 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:
+ results.append(table_dictize(row, context))
+ return results
+
+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',
+ })
diff --git a/ckan/logic/auth/create.py b/ckan/logic/auth/create.py
index 3ec5608..bba6abf 100644
--- a/ckan/logic/auth/create.py
+++ b/ckan/logic/auth/create.py
@@ -141,3 +141,9 @@ def activity_create(context, data_dict):
def tag_create(context, data_dict):
user = context['user']
return {'success': Authorizer.is_sysadmin(user)}
+
+def follower_create(context, follower_dict):
+ model = context['model']
+ user = model.User.get(context['user'])
+ success = (user == model.User.get(follower_dict['follower_id']))
+ return {'success': success}
diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py
index b07d2bb..4f55ac1 100644
--- a/ckan/logic/schema.py
+++ b/ckan/logic/schema.py
@@ -37,7 +37,9 @@
user_id_exists,
object_id_validator,
activity_type_exists,
- tag_not_in_vocabulary)
+ tag_not_in_vocabulary,
+ follower_id_exists,
+ followee_id_exists)
from formencode.validators import OneOf
import ckan.model
@@ -369,3 +371,15 @@ def default_create_activity_schema():
'data': [ignore_empty, ignore_missing, unicode],
}
return schema
+
+def default_create_follower_schema():
+ 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],
+ 'datetime': [ignore]
+ }
+ return schema
diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py
index 58c69ee..6ab8eee 100644
--- a/ckan/logic/validators.py
+++ b/ckan/logic/validators.py
@@ -476,3 +476,32 @@ def tag_not_in_vocabulary(key, tag_dict, errors, context):
(tag_name, vocabulary_id))
else:
return
+
+def follower_id_exists(key, follower_dict, errors, context):
+ follower_id_validators = {
+ 'user': user_id_exists,
+ }
+ follower_id = follower_dict[('follower_id',)]
+ follower_type = follower_dict.get(('follower_type',))
+ if not follower_type:
+ raise Invalid(_('Not found: {0}').format('follower_type'))
+ validator = follower_id_validators.get(follower_type)
+ if not validator:
+ raise Invalid(_('follower_type {type} not recognised').format(
+ type=follower_type))
+ return validator(follower_id, context)
+
+def followee_id_exists(key, followee_dict, errors, context):
+ followee_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)
+ if not validator:
+ raise Invalid(_('followee_type {type} not recognised').format(
+ type=followee_type))
+ return validator(followee_id, context)
diff --git a/ckan/migration/versions/054_follower_table.py b/ckan/migration/versions/054_follower_table.py
new file mode 100644
index 0000000..9083ddf
--- /dev/null
+++ b/ckan/migration/versions/054_follower_table.py
@@ -0,0 +1,20 @@
+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 df0d2f5..72ddcdf 100644
--- a/ckan/model/__init__.py
+++ b/ckan/model/__init__.py
@@ -28,6 +28,7 @@
from vocabulary import *
from activity import *
from term_translation import *
+from follower import follower_table
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
new file mode 100644
index 0000000..9597f0d
--- /dev/null
+++ b/ckan/model/follower.py
@@ -0,0 +1,16 @@
+import sqlalchemy
+from meta import metadata
+
+# FIXME: Should follower_type and followeee_type be part of the primary key too?
+follower_table = sqlalchemy.Table('follower',
+ 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),
+)
diff --git a/ckan/tests/functional/api/test_follow.py b/ckan/tests/functional/api/test_follow.py
new file mode 100644
index 0000000..8919671
--- /dev/null
+++ b/ckan/tests/functional/api/test_follow.py
@@ -0,0 +1,118 @@
+import datetime
+import paste
+import pylons.test
+import ckan
+from ckan.lib.helpers import json
+
+def datetime_from_string(s):
+ '''Return a standard datetime.datetime object initialised from a string in
+ the same format used for timestamps in dictized activities (the format
+ produced by datetime.datetime.isoformat())
+
+ '''
+ return datetime.datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%f')
+
+class TestFollow(object):
+
+ @classmethod
+ def setup_class(self):
+ ckan.tests.CreateTestData.create()
+ self.testsysadmin = ckan.model.User.get('testsysadmin')
+ self.annafan = ckan.model.User.get('annafan')
+ self.russianfan = ckan.model.User.get('russianfan')
+ self.warandpeace = ckan.model.Package.get('warandpeace')
+ self.annakarenina = ckan.model.Package.get('annakarenina')
+ self.app = paste.fixture.TestApp(pylons.test.pylonsapp)
+
+ @classmethod
+ def teardown_class(self):
+ ckan.model.repo.rebuild_db()
+
+ def test_user_follow_user(self):
+ '''Test a user following another user via the API.'''
+
+ # TODO: Test following and retrieving followers by name as well as by ID.
+
+ # 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',
+ })
+ extra_environ = {
+ 'Authorization': str(self.annafan.apikey)
+ }
+ response = self.app.post('/api/action/follower_create',
+ params=params, extra_environ=extra_environ)
+ after = datetime.datetime.now()
+ assert not response.errors
+ response = response.json
+ assert response['success'] is True
+ assert response['result']
+ 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'
+ 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.
+ params = json.dumps({'id': self.russianfan.id})
+ response = self.app.post('/api/action/user_follower_list',
+ params=params)
+ assert not response.errors
+ response = response.json
+ assert response['success'] is True
+ assert response['result']
+ followers = response['result']
+ assert len(followers) == 1
+ follower = followers[0]
+ assert follower['follower_id'] == self.annafan.id
+ assert follower['follower_type'] == 'user'
+ assert follower['followee_id'] == self.russianfan.id
+ assert follower['followee_type'] == 'user'
+ timestamp = datetime_from_string(follower['datetime'])
+ assert (timestamp >= before and timestamp <= after), str(timestamp)
+
+ def test_user_follow_dataset(self):
+ '''Test a user following a dataset via the API.'''
+ raise NotImplementedError
+
+ def test_follower_id_bad(self):
+ raise NotImplementedError
+
+ def test_follower_id_missing(self):
+ raise NotImplementedError
+
+ def test_follower_type_bad(self):
+ raise NotImplementedError
+
+ def test_follower_type_missing(self):
+ raise NotImplementedError
+
+ def test_followee_id_bad(self):
+ raise NotImplementedError
+
+ def test_followee_id_missing(self):
+ raise NotImplementedError
+
+ def test_followee_type_bad(self):
+ raise NotImplementedError
+
+ def test_followee_type_missing(self):
+ raise NotImplementedError
+
+ def test_follow_with_datetime(self):
+ raise NotImplementedError
+
+ def test_follow_already_exists(self):
+ raise NotImplementedError
+
+ def test_follow_not_logged_in(self):
+ raise NotImplementedError
+
+ def test_follow_not_authorized(self):
+ raise NotImplementedError
================================================================
More information about the ckan-changes
mailing list