[ckan-changes] commit/ckanext-follower: 10 new changesets
Bitbucket
commits-noreply at bitbucket.org
Thu May 19 17:00:36 UTC 2011
10 new changesets in ckanext-follower:
http://bitbucket.org/okfn/ckanext-follower/changeset/f6027e0b4dcc/
changeset: r28:f6027e0b4dcc
user: johnglover
date: 2011-05-10 16:40:18
summary: Update readme: leave buttons as they are for now
affected #: 1 file (75 bytes)
--- a/README.markdown Thu Apr 21 18:56:06 2011 +0100
+++ b/README.markdown Tue May 10 15:40:18 2011 +0100
@@ -21,10 +21,6 @@
A follower count and a button to follow/unfollow packages should be
available beside the RSS subscribe link on all package pages.
-To Do
------
-* Change style of buttons to integrate better with CKAN theme
-
Tests
-----
From the ckanext-follower directory, run:
http://bitbucket.org/okfn/ckanext-follower/changeset/f343ee1f72e1/
changeset: r29:f343ee1f72e1
user: johnglover
date: 2011-05-10 19:37:33
summary: Update model and controller so that Follower table now stores User.id and Package.id fields instead of User.name and Package.name
affected #: 6 files (1.3 KB)
--- a/ckanext/follower/controller.py Tue May 10 15:40:18 2011 +0100
+++ b/ckanext/follower/controller.py Tue May 10 18:37:33 2011 +0100
@@ -17,24 +17,43 @@
from ckan.lib.base import BaseController, response, render, abort
from ckanext.follower import model
-def packages_followed_by(id):
+def packages_followed_by(user_id):
"""
Return a list of packages followed by user id.
"""
query = model.Session.query(model.Follower)\
- .filter(model.Follower.table == 'package')\
- .filter(model.Follower.user_id == id)
+ .filter(model.Follower.user_id == user_id)
+ return [p.package_id for p in query if query]
- packages = []
- for package in query:
- packages.append(package.object_id)
- return packages
+def get_package_name(package_id):
+ """
+ Get the name of the package with the given ID.
+ """
+ query = model.Session.query(model.Package)\
+ .filter(model.Package.id == package_id)
+ return query.first().name if query else None
+
+def get_package_id(package_name):
+ """
+ Return the ID of user_name, or None if no such user ID exists
+ """
+ query = model.Session.query(model.Package)\
+ .filter(model.Package.name == package_name)
+ return query.first().id if query else None
+
+def get_user_id(user_name):
+ """
+ Return the ID of user_name, or None if no such user ID exists
+ """
+ query = model.Session.query(model.User)\
+ .filter(model.User.name == user_name)
+ return query.first().id if query else None
class FollowerController(BaseController):
"""
The CKANEXT-Follower Controller.
"""
- def _follow_package(self, user_id, table, package_id):
+ def _follow_package(self, user_id, package_id):
"""
Update the database, setting user_id to follow
package_id.
@@ -43,7 +62,6 @@
try:
follower = model.Follower(unicode(user_id),
- unicode(table),
unicode(package_id))
session.add(follower)
session.commit()
@@ -54,7 +72,7 @@
session.rollback()
return False
- def _unfollow_package(self, user_id, table, package_id):
+ def _unfollow_package(self, user_id, package_id):
"""
Update the database, removing user_id from package_id followers
"""
@@ -63,8 +81,7 @@
try:
query = model.Session.query(model.Follower)\
.filter(model.Follower.user_id == user_id)\
- .filter(model.Follower.table == table)\
- .filter(model.Follower.object_id == package_id)
+ .filter(model.Follower.package_id == package_id)
follower = query.first()
session.delete(follower)
@@ -83,8 +100,6 @@
Performs the following checks:
* user_id field is present in request
* user_id matches id of currently logged in user
- * object_type field is present in request
- * object_type is valid
* package_id field is present in request
returns: (http_status, json_response)
@@ -95,19 +110,9 @@
user_id = request.params.get('user_id')
# make sure this matches the user_id of the current user
- user_name = request.environ.get('REMOTE_USER')
- if not user_id == user_name:
+ if not user_id == get_user_id(request.environ.get('REMOTE_USER')):
return (403, {'error': "You are not authorized to make this request"})
- # check for an object type - specifies the type of object to follow
- if not request.params.get('object_type'):
- return (400, {'error': "No object type specified"})
- object_type = request.params.get('object_type')
-
- # make sure that the object_type is valid
- if not object_type in model.VALID_OBJECT_TYPES:
- return (400, {'error': "Invalid object type"})
-
# check for a package ID
if not request.params.get('package_id'):
return (400, {'error': "No package ID specified"})
@@ -121,8 +126,7 @@
follows this package.
"""
query = model.Session.query(model.Follower)\
- .filter(model.Follower.table == 'package')\
- .filter(model.Follower.object_id == package_id)
+ .filter(model.Follower.package_id == package_id)
users = []
for follower in query:
@@ -142,7 +146,7 @@
def follow(self):
"""
follower API endpoint: Follow a given package.
- Format: {user_id, object_type, object_id, action}
+ Format: {user_id, package_id, action}
"""
status, result = self._validate_request()
if status != 200:
@@ -151,9 +155,8 @@
# update the database
user_id = request.params.get('user_id')
- object_type = request.params.get('object_type')
package_id = request.params.get('package_id')
- if self._follow_package(user_id, object_type, package_id):
+ if self._follow_package(user_id, package_id):
return result
else:
response.status_int = 500
@@ -163,7 +166,7 @@
def unfollow(self):
"""
follower API endpoint: Unfollow a given package.
- Format: {user_id, object_type, object_id, action}
+ Format: {user_id, package_id, action}
"""
status, result = self._validate_request()
if status != 200:
@@ -172,9 +175,8 @@
# update the database
user_id = request.params.get('user_id')
- object_type = request.params.get('object_type')
package_id = request.params.get('package_id')
- if self._unfollow_package(user_id, object_type, package_id):
+ if self._unfollow_package(user_id, package_id):
return result
else:
response.status_int = 500
@@ -190,14 +192,14 @@
"""
return self._get_followers(id)
- def package_followers_page(self, id):
+ def package_followers_page(self, name):
"""
Display a page containing all of the users that are following
a given package.
"""
- c.pkg = model.Package.get(id)
+ c.pkg = model.Package.get(name)
if not c.pkg:
abort(404, _('Package not found'))
- c.followers = self._get_followers(id)
+ c.followers = self._get_followers(get_package_id(name))
return render("package_followers.html")
--- a/ckanext/follower/html.py Tue May 10 15:40:18 2011 +0100
+++ b/ckanext/follower/html.py Tue May 10 18:37:33 2011 +0100
@@ -1,10 +1,8 @@
HEAD_CODE = """
+<link rel="stylesheet" href="/ckanext-follower/css/main.css"
+ type="text/css" media="screen" /><link rel="stylesheet" href="/ckanext-follower/css/buttons.css"
type="text/css" media="screen" />
-<style type="text/css">
-span#follower { display: inline }
-#follower-error { color: #b00 }
-</style>
"""
BODY_CODE = """
@@ -12,7 +10,7 @@
<script type="text/javascript" src="/ckanext-follower/follower.js"></script><script type="text/javascript">
$('document').ready(function($){
- CKANEXT.FOLLOWER.init('%(package_id)s', '%(user_id)s');
+ CKANEXT.FOLLOWER.init('%(package_id)s', '%(package_name)s', '%(user_id)s');
});
</script>
"""
@@ -22,6 +20,9 @@
<a id="package-followers"></a><a id="follow-button"></a></span>
+"""
+
+ERROR_CODE = """
<div id="follower-error"></div>
"""
--- a/ckanext/follower/model.py Tue May 10 15:40:18 2011 +0100
+++ b/ckanext/follower/model.py Tue May 10 18:37:33 2011 +0100
@@ -7,31 +7,26 @@
from ckan.model.types import make_uuid
from datetime import datetime
-VALID_OBJECT_TYPES = ['package']
-
follower_table = meta.Table('follower', meta.metadata,
meta.Column('id', meta.types.UnicodeText, primary_key=True,
default=make_uuid),
meta.Column('user_id', meta.types.UnicodeText,
- # meta.ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'),
+ meta.ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'),
nullable=False),
- meta.Column('table', meta.types.UnicodeText, nullable=False),
- meta.Column('object_id', meta.types.UnicodeText, nullable=False),
+ meta.Column('package_id', meta.types.UnicodeText,
+ meta.ForeignKey('package.id', onupdate='CASCADE', ondelete='CASCADE'),
+ nullable=False),
meta.Column('created', meta.DateTime, default=datetime.now),
- sa.UniqueConstraint('user_id', 'table', 'object_id')
- )
+ sa.UniqueConstraint('user_id', 'package_id'))
class Follower(object):
- def __init__(self, user_id, table, object_id):
+ def __init__(self, user_id, package_id):
self.user_id = user_id
- self.table = table
- self.object_id = object_id
+ self.package_id = package_id
self.created = None
def __repr__(self):
- return "<Follower('%s', '%s', '%s')>" % (self.user_id,
- self.table,
- self.object_id)
+ return "<Follower('%s', '%s')>" % (self.user_id, self.package_id)
meta.mapper(Follower, follower_table)
--- a/ckanext/follower/plugin.py Tue May 10 15:40:18 2011 +0100
+++ b/ckanext/follower/plugin.py Tue May 10 18:37:33 2011 +0100
@@ -120,7 +120,7 @@
map.connect('package_followers', '/api/2/follower/package/{id}',
controller='ckanext.follower.controller:FollowerController',
action='package')
- map.connect('package_followers_page', '/package/followers/{id}',
+ map.connect('package_followers_page', '/package/followers/{name}',
controller='ckanext.follower.controller:FollowerController',
action='package_followers_page')
return map
@@ -146,9 +146,10 @@
c.pkg.id):
# pass data to the javascript file that creates the
# follower count and follow/unfollow buttons
- user_name = request.environ.get('REMOTE_USER') or ""
- data = {'package_id': c.pkg.name,
- 'user_id': user_name}
+ user_id = controller.get_user_id(request.environ.get('REMOTE_USER'))
+ data = {'package_id': c.pkg.id,
+ 'package_name': c.pkg.name,
+ 'user_id': user_id}
# add CSS styles for follower HTML
stream = stream | Transformer('head').append(HTML(html.HEAD_CODE))
# add jquery and follower.js links
@@ -158,20 +159,25 @@
# RSS 'subscribe' link
stream = stream | Transformer('body//div[@id="package"]//h2[@class="head"]')\
.append(HTML(html.FOLLOWER_CODE))
+ # add the follower error DIV after the H2 tag
+ stream = stream | Transformer('body//div[@id="package"]//h2[@class="head"]')\
+ .after(HTML(html.ERROR_CODE))
# if this is the read action of a user page, show packages being followed
if(routes.get('controller') == 'user' and
routes.get('action') == 'read' and
c.user):
- packages = controller.packages_followed_by(c.user)
+ user_id = controller.get_user_id(c.user)
+ packages = controller.packages_followed_by(user_id)
if packages:
packages_html = ""
for package_number, package in enumerate(packages):
# add a link to the package page
+ package_name = controller.get_package_name(package)
packages_html += \
- h.link_to(package,
+ h.link_to(package_name,
h.url_for(controller='package', action='read',
- id=package))
+ id=package_name))
# add comma and space if this isn't the last package in the
# list
if package_number < len(packages) - 1:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ckanext/follower/public/ckanext-follower/css/main.css Tue May 10 18:37:33 2011 +0100
@@ -0,0 +1,6 @@
+span#follower { display: inline }
+#follower-error { color: #b00 }
+#follower-list {
+ list-style-type: none;
+ margin-left: 0;
+}
--- a/ckanext/follower/public/ckanext-follower/follower.js Tue May 10 15:40:18 2011 +0100
+++ b/ckanext/follower/public/ckanext-follower/follower.js Tue May 10 18:37:33 2011 +0100
@@ -6,8 +6,9 @@
var CKANEXT = CKANEXT || {};
CKANEXT.FOLLOWER = {
- init:function(packageID, userID){
+ init:function(packageID, packageName, userID){
this.packageID = packageID;
+ this.packageName = packageName;
this.userID = userID;
this.isFollowing = this.isFollowingPackage();
this.packageFollowers();
@@ -45,15 +46,25 @@
// show the number of people following this package
packageFollowers:function(){
var packageID = this.packageID;
+ var packageName = this.packageName;
$.getJSON('/api/2/follower/package/' + packageID,
function(data){
- var html = '<a href="HREF" id="package-followers" ' +
- 'class="button pcb"><span>TEXT</span></a>'
- var text = data.length + " Following";
- var followersURL = "/package/followers/" + packageID;
- html = html.replace('HREF', followersURL);
- html = html.replace('TEXT', text);
+ // if no followers, disable button
+ if(data.length == 0){
+ var html = '<a id="package-followers" class="disabled-button pcb">' +
+ '<span>0 Following</span></a>';
+ }
+ else{
+ // if followers, show the count and provide a link to the
+ // page with a list of package followers
+ var html = '<a href="HREF" id="package-followers" ' +
+ 'class="button pcb"><span>TEXT</span></a>'
+ var text = data.length + " Following";
+ var followersURL = "/package/followers/" + packageName;
+ html = html.replace('HREF', followersURL);
+ html = html.replace('TEXT', text);
+ }
// replace the package followers button
$('a#package-followers').replaceWith(html);
@@ -64,7 +75,6 @@
follow:function(){
// post follow info to follower API
followerData = {user_id: CKANEXT.FOLLOWER.userID,
- object_type: 'package',
package_id: CKANEXT.FOLLOWER.packageID,
action: 'follow'};
$.post("/api/2/follower", followerData,
@@ -93,7 +103,6 @@
// callback function for the unfollow button being clicked
unfollow:function(){
followerData = {user_id: CKANEXT.FOLLOWER.userID,
- object_type: 'package',
package_id: CKANEXT.FOLLOWER.packageID,
action: 'unfollow'};
http://bitbucket.org/okfn/ckanext-follower/changeset/1f6df39632d8/
changeset: r30:1f6df39632d8
user: johnglover
date: 2011-05-10 19:38:34
summary: Merge main style and pages.css into main.css
affected #: 1 file (0 bytes)
--- a/ckanext/follower/public/ckanext-follower/css/pages.css Tue May 10 18:37:33 2011 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-#follower-list {
- list-style-type: none;
- margin-left: 0;
-}
http://bitbucket.org/okfn/ckanext-follower/changeset/6bdeaf4afdc1/
changeset: r31:6bdeaf4afdc1
user: johnglover
date: 2011-05-11 14:14:03
summary: Fix two bugs that resulted in the package page not displaying correctly if the user was not logged in
affected #: 2 files (30 bytes)
--- a/ckanext/follower/controller.py Tue May 10 18:38:34 2011 +0100
+++ b/ckanext/follower/controller.py Wed May 11 13:14:03 2011 +0100
@@ -31,7 +31,7 @@
"""
query = model.Session.query(model.Package)\
.filter(model.Package.id == package_id)
- return query.first().name if query else None
+ return query.first().name if query.first() else None
def get_package_id(package_name):
"""
@@ -39,7 +39,7 @@
"""
query = model.Session.query(model.Package)\
.filter(model.Package.name == package_name)
- return query.first().id if query else None
+ return query.first().id if query.first() else None
def get_user_id(user_name):
"""
@@ -47,7 +47,7 @@
"""
query = model.Session.query(model.User)\
.filter(model.User.name == user_name)
- return query.first().id if query else None
+ return query.first().id if query.first() else None
class FollowerController(BaseController):
"""
--- a/ckanext/follower/plugin.py Tue May 10 18:38:34 2011 +0100
+++ b/ckanext/follower/plugin.py Wed May 11 13:14:03 2011 +0100
@@ -146,7 +146,7 @@
c.pkg.id):
# pass data to the javascript file that creates the
# follower count and follow/unfollow buttons
- user_id = controller.get_user_id(request.environ.get('REMOTE_USER'))
+ user_id = controller.get_user_id(request.environ.get('REMOTE_USER')) or ""
data = {'package_id': c.pkg.id,
'package_name': c.pkg.name,
'user_id': user_id}
http://bitbucket.org/okfn/ckanext-follower/changeset/94c9bdf26a98/
changeset: r32:94c9bdf26a98
user: johnglover
date: 2011-05-11 15:01:40
summary: Add extension status to README
affected #: 1 file (26 bytes)
--- a/README.markdown Wed May 11 13:14:03 2011 +0100
+++ b/README.markdown Wed May 11 14:01:40 2011 +0100
@@ -4,6 +4,8 @@
The Follower extension for CKAN adds the ability for users to follow
data packages.
+**Current Status:** Beta
+
Installation and Activation
---------------------------
http://bitbucket.org/okfn/ckanext-follower/changeset/d7a7fe82b523/
changeset: r33:d7a7fe82b523
user: johnglover
date: 2011-05-11 15:58:49
summary: Remove test code from model file
affected #: 1 file (284 bytes)
--- a/ckanext/follower/model.py Wed May 11 14:01:40 2011 +0100
+++ b/ckanext/follower/model.py Wed May 11 14:58:49 2011 +0100
@@ -29,12 +29,3 @@
return "<Follower('%s', '%s')>" % (self.user_id, self.package_id)
meta.mapper(Follower, follower_table)
-
-
-if __name__ == "__main__":
- # TODO: for testing only, remove
- # wipe follower table
- engine = sa.create_engine('postgresql://ckantest:pass@localhost/ckantest')
- model.init_model(engine)
- follower_table.drop(checkfirst=True)
- follower_table.create(checkfirst=True)
http://bitbucket.org/okfn/ckanext-follower/changeset/f0b125eba6cf/
changeset: r34:f0b125eba6cf
user: johnglover
date: 2011-05-11 15:59:20
summary: Add additional check for missing packages to _validate_request method
affected #: 1 file (301 bytes)
--- a/ckanext/follower/controller.py Wed May 11 14:58:49 2011 +0100
+++ b/ckanext/follower/controller.py Wed May 11 14:59:20 2011 +0100
@@ -116,6 +116,13 @@
# check for a package ID
if not request.params.get('package_id'):
return (400, {'error': "No package ID specified"})
+ package_id = request.params.get('package_id')
+
+ # check that package ID exists in the database
+ query = model.Session.query(model.Package)\
+ .filter(model.Package.id == package_id)
+ if not query.first():
+ return (404, {'error': "Package not found"})
# valid request
return (200, {'status': "OK" })
http://bitbucket.org/okfn/ckanext-follower/changeset/4f5198121c6c/
changeset: r35:4f5198121c6c
user: johnglover
date: 2011-05-11 15:59:58
summary: Add more controller unit tests: test_follow_get and test_follow_post_invalid_request
affected #: 2 files (3.1 KB)
--- a/test.ini Wed May 11 14:59:20 2011 +0100
+++ b/test.ini Wed May 11 14:59:58 2011 +0100
@@ -103,7 +103,7 @@
keys = generic
[logger_root]
-level = WARN
+level = INFO
handlers = console
[logger_ckan]
--- a/tests/test_follower_controller.py Wed May 11 14:59:20 2011 +0100
+++ b/tests/test_follower_controller.py Wed May 11 14:59:58 2011 +0100
@@ -3,6 +3,7 @@
import paste.fixture
from ckan.config.middleware import make_app
from ckan.tests import conf_dir, url_for, CreateTestData, TestController
+from ckanext.follower.controller import get_user_id
class TestFollowerController(TestController):
@classmethod
@@ -18,6 +19,12 @@
CreateTestData.delete()
def test_index(self):
+ """
+ Tests that the index action for follower returns application/json
+ as the content type, returns a correctly formatted JSON response,
+ and the the JSON response consists of two objects with the keys
+ 'doc' and 'doc_url'.
+ """
response = self.app.get(url_for('follower'))
# make sure that the response content type is JSON
assert response.header('Content-Type') == "application/json" ,\
@@ -29,3 +36,70 @@
"response does not contain a 'doc' key"
assert 'doc_url' in json_response, \
"response does not contain a 'doc_url' key"
+
+ def test_follow_get(self):
+ """
+ Tests to ensure that get requests to the follow action returns
+ the same content as index action.
+ """
+ f = self.app.get(url_for('follow'))
+ i = self.app.get(url_for('follower'))
+ # compare content-type headers
+ assert f.header('Content-Type') == i.header('Content-Type')
+ f_js = json.loads(f.body)
+ i_js = json.loads(i.body)
+ # make sure json responses are same length
+ assert len(f_js) == len(i_js)
+ # make sure json response key/values are equal
+ for key in f_js:
+ assert key in i_js
+ assert f_js[key] == i_js[key]
+
+ def test_follow_post_invalid_request(self):
+ """
+ Tests to ensure that invalid post requests to the follow action
+ return the appropriate error code.
+ Invalid requests:
+ - not containing a field called 'action', which is set to 'follow'
+ (results in error 404)
+ - no user_id supplied (results in error 400)
+ - given user_id matches that of the currently logged in user
+ - no package_id supplied (results in error 400)
+ - package_id not found in database (results in error 404)
+ """
+ # no action
+ self.app.post(url_for('follow'), status = 404)
+ # no user_id
+ self.app.post(url_for('follow'),
+ params = {'action': 'follow'},
+ status = 400)
+ # not logged in:
+ self.app.post(url_for('follow'),
+ params = {'action': 'follow', 'user_id': 'invalid_user'},
+ status = 403)
+ # logged in but giving a different user_id:
+ environ = {'REMOTE_USER': 'tester'}
+ user_id = get_user_id('invalid_user')
+ self.app.post(url_for('follow'),
+ extra_environ = environ,
+ params = {'action': 'follow', 'user_id': user_id},
+ status = 403)
+ environ = {'REMOTE_USER': 'invalid_user'}
+ user_id = get_user_id('tester')
+ self.app.post(url_for('follow'),
+ extra_environ = environ,
+ params = {'action': 'follow', 'user_id': user_id},
+ status = 403)
+ # valid user_id, no package_id
+ environ = {'REMOTE_USER': 'tester'}
+ self.app.post(url_for('follow'),
+ extra_environ = environ,
+ params = {'action': 'follow', 'user_id': user_id},
+ status = 400)
+ # package_id not found
+ self.app.post(url_for('follow'),
+ extra_environ = environ,
+ params = {'action': 'follow',
+ 'user_id': user_id,
+ 'package_id': 'invalid_package_id'},
+ status = 404)
http://bitbucket.org/okfn/ckanext-follower/changeset/681ec5fff9d0/
changeset: r36:681ec5fff9d0
user: johnglover
date: 2011-05-12 14:26:05
summary: Change button SPAN to display on its own line (block mode), so that long package names do not cause text wrapping problems
affected #: 1 file (1 byte)
--- a/ckanext/follower/public/ckanext-follower/css/main.css Wed May 11 14:59:58 2011 +0100
+++ b/ckanext/follower/public/ckanext-follower/css/main.css Thu May 12 13:26:05 2011 +0100
@@ -1,4 +1,4 @@
-span#follower { display: inline }
+span#follower { display: block }
#follower-error { color: #b00 }
#follower-list {
list-style-type: none;
http://bitbucket.org/okfn/ckanext-follower/changeset/5fd97f807c9e/
changeset: r37:5fd97f807c9e
user: johnglover
date: 2011-05-17 15:26:47
summary: Fix JQuery bug and update a dead url
affected #: 2 files (3 bytes)
--- a/ckanext/follower/public/ckanext-follower/follower.js Thu May 12 13:26:05 2011 +0100
+++ b/ckanext/follower/public/ckanext-follower/follower.js Tue May 17 14:26:47 2011 +0100
@@ -81,7 +81,7 @@
// successful follow
function(response){
// remove any existing error message
- $('div#follower-error').remove();
+ $('div#follower-error').empty();
// update the follower count
CKANEXT.FOLLOWER.packageFollowers();
// update the follow button
@@ -110,7 +110,7 @@
// successful follow
function(response){
// remove any existing error message
- $('div#follower-error').remove();
+ $('div#follower-error').empty();
// update the follower count
CKANEXT.FOLLOWER.packageFollowers();
// update the follow button
--- a/ckanext/follower/templates/package_followers.html Thu May 12 13:26:05 2011 +0100
+++ b/ckanext/follower/templates/package_followers.html Tue May 17 14:26:47 2011 +0100
@@ -5,7 +5,7 @@
<py:def function="page_title">${c.pkg.title or c.pkg.name} - Data Packages - Followers</py:def><link rel="stylesheet" type="text/css" media="screen, print"
- href="/ckanext-follower/css/pages.css"
+ href="/ckanext-follower/css/main.css"
py:def="optional_head()" /><div py:match="content" class="package">
Repository URL: https://bitbucket.org/okfn/ckanext-follower/
--
This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.
More information about the ckan-changes
mailing list