[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