{% endblock %}
diff --git a/ckan/tests/functional/api/test_activity.py b/ckan/tests/functional/api/test_activity.py
index 1126efd0484..4f7eff2a380 100644
--- a/ckan/tests/functional/api/test_activity.py
+++ b/ckan/tests/functional/api/test_activity.py
@@ -216,10 +216,14 @@ def teardown_class(self):
import ckan.model as model
model.repo.rebuild_db()
- def dashboard_activity_stream(self, user_id):
- response = self.app.get(
- "/api/2/rest/user/{0}/dashboard_activity".format(user_id))
- return json.loads(response.body)
+ def dashboard_activity_stream(self, apikey):
+
+ response = self.app.get("/api/action/dashboard_activity_list",
+ json.dumps({}),
+ extra_environ={'Authorization': str(apikey)})
+ response_dict = json.loads(response.body)
+ assert response_dict['success'] is True
+ return response_dict['result']
def user_activity_stream(self, user_id, apikey=None):
if apikey:
@@ -286,35 +290,43 @@ def record_details(self, user_id, package_id=None, group_ids=None,
details['recently changed datasets stream'] = \
self.recently_changed_datasets_stream(apikey)
+ details['user dashboard activity stream'] = (
+ self.dashboard_activity_stream(apikey))
+
details['follower dashboard activity stream'] = (
- self.dashboard_activity_stream(self.follower['id']))
+ self.dashboard_activity_stream(self.follower['apikey']))
details['time'] = datetime.datetime.now()
return details
- def check_dashboard(
- self,
- before, after, wanted_difference,
- potential_followees):
- difference = find_new_activities(
- before['follower dashboard activity stream'],
- after['follower dashboard activity stream'])
- if any(potential_followee in self.followees
- for potential_followee in potential_followees):
- assert difference == wanted_difference
- else:
- assert len(difference) == 0
+ def check_dashboards(self, before, after, activity):
+ new_activities = [activity_ for activity_ in
+ after['user dashboard activity stream']
+ if activity_ not in before['user dashboard activity stream']]
+ assert [activity['id'] for activity in new_activities] == [
+ activity['id']]
+
+ new_activities = [activity_ for activity_ in
+ after['follower dashboard activity stream']
+ if activity_ not in before['follower dashboard activity stream']]
+ assert [activity['id'] for activity in new_activities] == [
+ activity['id']]
def _create_package(self, user, name=None):
if user:
user_id = user['id']
+ apikey = user['apikey']
else:
user_id = 'not logged in'
+ apikey = None
+
+ before = self.record_details(user_id, apikey=apikey)
# Create a new package.
request_data = make_package(name)
before = self.record_details(user_id=user_id,
- group_ids=[group['name'] for group in request_data['groups']])
+ group_ids=[group['name'] for group in request_data['groups']],
+ apikey=apikey)
extra_environ = {'Authorization': str(user['apikey'])}
response = self.app.post('/api/action/package_create',
json.dumps(request_data), extra_environ=extra_environ)
@@ -324,7 +336,8 @@ def _create_package(self, user, name=None):
after = self.record_details(user_id=user_id,
package_id=package_created['id'],
- group_ids=[group['name'] for group in package_created['groups']])
+ group_ids=[group['name'] for group in package_created['groups']],
+ apikey=apikey)
# Find the new activity in the user's activity stream.
user_new_activities = (find_new_activities(
@@ -345,7 +358,30 @@ def _create_package(self, user, name=None):
after['recently changed datasets stream'])
assert new_rcd_activities == [activity]
- self.check_dashboard(before, after, user_new_activities, [user_id])
+ # The new activity should appear in the user's dashboard activity
+ # stream.
+ new_activities = [activity_ for activity_ in
+ after['user dashboard activity stream']
+ if activity_ not in before['user dashboard activity stream']]
+ # There will be other new activities besides the 'follow dataset' one
+ # because all the dataset's old activities appear in the user's
+ # dashboard when she starts to follow the dataset.
+ assert activity['id'] in [
+ activity['id'] for activity in new_activities]
+
+ # The new activity should appear in the user "follower"'s dashboard
+ # activity stream because she follows all the other users and datasets.
+ new_activities = [activity_ for activity_ in
+ after['follower dashboard activity stream']
+ if activity_ not in before['follower dashboard activity stream']]
+ # There will be other new activities besides the 'follow dataset' one
+ # because all the dataset's old activities appear in the user's
+ # dashboard when she starts to follow the dataset.
+ assert [activity['id'] for activity in new_activities] == [
+ activity['id']]
+
+ # The same new activity should appear on the dashboard's of the user's
+ # followers.
# The same new activity should appear in the activity streams of the
# package's groups.
@@ -353,7 +389,8 @@ def _create_package(self, user, name=None):
grp_new_activities = find_new_activities(
before['group activity streams'][group_dict['name']],
after['group activity streams'][group_dict['name']])
- assert grp_new_activities == [activity]
+ assert [activity['id'] for activity in grp_new_activities] == [
+ activity['id']]
# Check that the new activity has the right attributes.
assert activity['object_id'] == package_created['id'], \
@@ -409,11 +446,13 @@ def _create_package(self, user, name=None):
def _add_resource(self, package, user):
if user:
user_id = user['id']
+ apikey = user['apikey']
else:
user_id = 'not logged in'
+ apikey = None
before = self.record_details(user_id, package['id'],
- [group['id'] for group in package['groups']])
+ [group['name'] for group in package['groups']], apikey=apikey)
resource_ids_before = [resource['id'] for resource in
package['resources']]
@@ -424,7 +463,7 @@ def _add_resource(self, package, user):
updated_package = package_update(self.app, package, user['apikey'])
after = self.record_details(user_id, package['id'],
- [group['id'] for group in package['groups']])
+ [group['name'] for group in package['groups']], apikey=apikey)
resource_ids_after = [resource['id'] for resource in
updated_package['resources']]
assert len(resource_ids_after) == len(resource_ids_before) + 1
@@ -458,8 +497,7 @@ def _add_resource(self, package, user):
after['group activity streams'][group_dict['name']])
assert grp_new_activities == [activity]
- self.check_dashboard(before, after, user_new_activities,
- [user_id, package['id']])
+ self.check_dashboards(before, after, activity)
# Check that the new activity has the right attributes.
assert activity['object_id'] == updated_package['id'], \
@@ -497,10 +535,14 @@ def _add_resource(self, package, user):
def _delete_extra(self, package_dict, user):
if user:
user_id = user['id']
+ apikey = user['apikey']
else:
user_id = 'not logged in'
+ apikey = None
- before = self.record_details(user_id, package_dict['id'])
+ before = self.record_details(user_id, package_dict['id'],
+ [group['name'] for group in package_dict['groups']],
+ apikey=apikey)
extras_before = list(package_dict['extras'])
assert len(extras_before) > 0, (
@@ -511,7 +553,9 @@ def _delete_extra(self, package_dict, user):
updated_package = package_update(self.app, package_dict,
user['apikey'])
- after = self.record_details(user_id, package_dict['id'])
+ after = self.record_details(user_id, package_dict['id'],
+ [group['name'] for group in package_dict['groups']],
+ apikey=apikey)
extras_after = updated_package['extras']
assert len(extras_after) == len(extras_before) - 1, (
"%s != %s" % (len(extras_after), len(extras_before) - 1))
@@ -545,8 +589,7 @@ def _delete_extra(self, package_dict, user):
after['group activity streams'][group_dict['name']])
assert grp_new_activities == [activity]
- self.check_dashboard(before, after, user_new_activities,
- [user_id, package_dict['id']])
+ self.check_dashboards(before, after, activity)
# Check that the new activity has the right attributes.
assert activity['object_id'] == updated_package['id'], \
@@ -585,11 +628,14 @@ def _delete_extra(self, package_dict, user):
def _update_extra(self, package_dict, user):
if user:
user_id = user['id']
+ apikey = user['apikey']
else:
user_id = 'not logged in'
+ apikey=None
before = self.record_details(user_id, package_dict['id'],
- [group['name'] for group in package_dict['groups']])
+ [group['name'] for group in package_dict['groups']],
+ apikey=apikey)
extras_before = package_dict['extras']
assert len(extras_before) > 0, (
@@ -606,7 +652,8 @@ def _update_extra(self, package_dict, user):
user['apikey'])
after = self.record_details(user_id, package_dict['id'],
- [group['name'] for group in package_dict['groups']])
+ [group['name'] for group in package_dict['groups']],
+ apikey=apikey)
extras_after = updated_package['extras']
assert len(extras_after) == len(extras_before), (
"%s != %s" % (len(extras_after), len(extras_before)))
@@ -632,8 +679,7 @@ def _update_extra(self, package_dict, user):
after['recently changed datasets stream']) \
== user_new_activities
- self.check_dashboard(before, after, user_new_activities,
- [user_id, package_dict['id']])
+ self.check_dashboards(before, after, activity)
# If the package has any groups, the same new activity should appear
# in the activity stream of each group.
@@ -682,10 +728,14 @@ def _add_extra(self, package_dict, user, key=None):
key = 'quality'
if user:
user_id = user['id']
+ apikey = user['apikey']
else:
user_id = 'not logged in'
+ apikey = None
- before = self.record_details(user_id, package_dict['id'])
+ before = self.record_details(user_id, package_dict['id'],
+ [group['name'] for group in package_dict['groups']],
+ apikey=apikey)
# Make a copy of the package's extras before we add a new extra,
# so we can compare the extras before and after updating the package.
@@ -697,7 +747,9 @@ def _add_extra(self, package_dict, user, key=None):
updated_package = package_update(self.app, package_dict,
user['apikey'])
- after = self.record_details(user_id, package_dict['id'])
+ after = self.record_details(user_id, package_dict['id'],
+ [group['name'] for group in package_dict['groups']],
+ apikey=apikey)
extras_after = updated_package['extras']
assert len(extras_after) == len(extras_before) + 1, (
"%s != %s" % (len(extras_after), len(extras_before) + 1))
@@ -731,8 +783,7 @@ def _add_extra(self, package_dict, user, key=None):
after['group activity streams'][group_dict['name']])
assert grp_new_activities == [activity]
- self.check_dashboard(before, after, user_new_activities,
- [user_id, package_dict['id']])
+ self.check_dashboards(before, after, activity)
# Check that the new activity has the right attributes.
assert activity['object_id'] == updated_package['id'], \
@@ -769,14 +820,16 @@ def _add_extra(self, package_dict, user, key=None):
str(detail['activity_type']))
def _create_activity(self, user, package, params):
- before = self.record_details(user['id'], package['id'])
+ before = self.record_details(user['id'], package['id'],
+ apikey=user['apikey'])
response = self.app.post('/api/action/activity_create',
params=json.dumps(params),
extra_environ={'Authorization': str(self.sysadmin_user['apikey'])})
assert response.json['success'] is True
- after = self.record_details(user['id'], package['id'])
+ after = self.record_details(user['id'], package['id'],
+ apikey=user['apikey'])
# Find the new activity in the user's activity stream.
user_new_activities = (find_new_activities(
@@ -792,8 +845,7 @@ def _create_activity(self, user, package, params):
after['package activity stream']))
assert pkg_new_activities == user_new_activities
- self.check_dashboard(before, after, user_new_activities,
- [user['id'], package['id']])
+ self.check_dashboards(before, after, activity)
# Check that the new activity has the right attributes.
assert activity['object_id'] == params['object_id'], (
@@ -840,7 +892,7 @@ def _delete_group(self, group, user):
new_activities, ("The same activity should also "
"appear in the group's activity stream.")
- self.check_dashboard(before, after, new_activities, [user['id']])
+ self.check_dashboards(before, after, activity)
# Check that the new activity has the right attributes.
assert activity['object_id'] == group['id'], str(activity['object_id'])
@@ -862,13 +914,15 @@ def _update_group(self, group, user):
item and detail are emitted.
"""
- before = self.record_details(user['id'], group_ids=[group['id']])
+ before = self.record_details(user['id'], group_ids=[group['id']],
+ apikey=user['apikey'])
# Update the group.
group_dict = {'id': group['id'], 'title': 'edited'}
group_update(self.app, group_dict, user['apikey'])
- after = self.record_details(user['id'], group_ids=[group['id']])
+ after = self.record_details(user['id'], group_ids=[group['id']],
+ apikey=user['apikey'])
# Find the new activity.
new_activities = find_new_activities(before['user activity stream'],
@@ -883,7 +937,7 @@ def _update_group(self, group, user):
new_activities, ("The same activity should also "
"appear in the group's activity stream.")
- self.check_dashboard(before, after, new_activities, [user['id']])
+ self.check_dashboards(before, after, activity)
# Check that the new activity has the right attributes.
assert activity['object_id'] == group['id'], str(activity['object_id'])
@@ -912,7 +966,8 @@ def _update_user(self, user):
assert response_dict['success'] is True
user_dict = response_dict['result']
- before = self.record_details(user_dict['id'])
+ before = self.record_details(user_dict['id'],
+ apikey=user_dict['apikey'])
# Update the user.
user_dict['about'] = 'edited'
@@ -921,7 +976,8 @@ def _update_user(self, user):
self.app.post('/api/action/user_update', json.dumps(user_dict),
extra_environ={'Authorization': str(user['apikey'])})
- after = self.record_details(user_dict['id'])
+ after = self.record_details(user_dict['id'],
+ apikey=user_dict['apikey'])
# Find the new activity.
new_activities = find_new_activities(before['user activity stream'],
@@ -930,7 +986,7 @@ def _update_user(self, user):
"the user's activity stream, but found %i" % len(new_activities))
activity = new_activities[0]
- self.check_dashboard(before, after, new_activities, [user_dict['id']])
+ self.check_dashboards(before, after, activity)
# Check that the new activity has the right attributes.
assert activity['object_id'] == user_dict['id'], (
@@ -954,7 +1010,8 @@ def _delete_resources(self, package):
"""
before = self.record_details(self.normal_user['id'], package['id'],
- [group['name'] for group in package['groups']])
+ [group['name'] for group in package['groups']],
+ apikey=self.normal_user['apikey'])
num_resources = len(package['resources'])
assert num_resources > 0, \
@@ -965,7 +1022,8 @@ def _delete_resources(self, package):
package_update(self.app, package, self.normal_user['apikey'])
after = self.record_details(self.normal_user['id'], package['id'],
- [group['name'] for group in package['groups']])
+ [group['name'] for group in package['groups']],
+ apikey=self.normal_user['apikey'])
# Find the new activity in the user's activity stream.
user_new_activities = (find_new_activities(
@@ -996,8 +1054,7 @@ def _delete_resources(self, package):
after['group activity streams'][group_dict['name']])
assert grp_new_activities == [activity]
- self.check_dashboard(before, after, user_new_activities,
- [package['id']])
+ self.check_dashboards(before, after, activity)
# Check that the new activity has the right attributes.
assert activity['object_id'] == package['id'], (
@@ -1037,10 +1094,12 @@ def _update_package(self, package, user):
"""
if user:
user_id = user['id']
+ apikey = user['apikey']
else:
user_id = 'not logged in'
+ apikey = None
- before = self.record_details(user_id, package['id'])
+ before = self.record_details(user_id, package['id'], apikey=apikey)
# Update the package.
if package['title'] != 'edited':
@@ -1050,7 +1109,7 @@ def _update_package(self, package, user):
package['title'] = 'edited again'
package_update(self.app, package, user['apikey'])
- after = self.record_details(user_id, package['id'])
+ after = self.record_details(user_id, package['id'], apikey=apikey)
# Find the new activity in the user's activity stream.
user_new_activities = (find_new_activities(
@@ -1073,8 +1132,7 @@ def _update_package(self, package, user):
after['recently changed datasets stream']) \
== user_new_activities
- self.check_dashboard(before, after, user_new_activities,
- [user_id, package['id']])
+ self.check_dashboards(before, after, activity)
# If the package has any groups, the same new activity should appear
# in the activity stream of each group.
@@ -1119,16 +1177,18 @@ def _update_resource(self, package, resource, user):
"""
if user:
user_id = user['id']
+ apikey = user['apikey']
else:
user_id = 'not logged in'
+ apikey = None
- before = self.record_details(user_id, package['id'])
+ before = self.record_details(user_id, package['id'], apikey=apikey)
# Update the resource.
resource['name'] = 'edited'
package_update(self.app, package)
- after = self.record_details(user_id, package['id'])
+ after = self.record_details(user_id, package['id'], apikey=apikey)
# Find the new activity in the user's activity stream.
user_new_activities = (find_new_activities(
@@ -1151,8 +1211,7 @@ def _update_resource(self, package, resource, user):
after['recently changed datasets stream']) \
== user_new_activities
- self.check_dashboard(before, after, user_new_activities,
- [user_id, package['id']])
+ self.check_dashboards(before, after, activity)
# If the package has any groups, the same new activity should appear
# in the activity stream of each group.
@@ -1230,8 +1289,7 @@ def _delete_package(self, package):
after['recently changed datasets stream']) \
== user_new_activities
- self.check_dashboard(before, after, user_new_activities,
- [self.sysadmin_user['id'], package['id']])
+ self.check_dashboards(before, after, activity)
# If the package has any groups, the same new activity should appear
# in the activity stream of each group.
@@ -1316,14 +1374,16 @@ def test_01_remove_tag(self):
assert len(pkg_dict['tags']) >= 1, ("The package has to have at least"
" one tag to test removing a tag.")
before = self.record_details(user['id'], pkg_dict['id'],
- [group['name'] for group in pkg_dict['groups']])
+ [group['name'] for group in pkg_dict['groups']],
+ apikey=user['apikey'])
data_dict = {
'id': pkg_dict['id'],
'tags': pkg_dict['tags'][0:-1],
}
package_update(self.app, data_dict, user['apikey'])
after = self.record_details(user['id'], pkg_dict['id'],
- [group['name'] for group in pkg_dict['groups']])
+ [group['name'] for group in pkg_dict['groups']],
+ apikey=user['apikey'])
# Find the new activity in the user's activity stream.
user_new_activities = (find_new_activities(
@@ -1346,8 +1406,7 @@ def test_01_remove_tag(self):
after['recently changed datasets stream']) \
== user_new_activities
- self.check_dashboard(before, after, user_new_activities,
- [user['id'], pkg_dict['id']])
+ self.check_dashboards(before, after, activity)
# If the package has any groups, the same new activity should appear
# in the activity stream of each group.
@@ -1491,7 +1550,8 @@ def test_create_user(self):
assert response_dict['success'] is True
user_created = response_dict['result']
- after = self.record_details(user_created['id'])
+ after = self.record_details(user_created['id'],
+ apikey=user_created['apikey'])
user_activities = after['user activity stream']
assert len(user_activities) == 1, ("There should be 1 activity in "
@@ -1533,7 +1593,7 @@ def test_create_group(self):
user = self.normal_user
- before = self.record_details(user['id'])
+ before = self.record_details(user['id'], apikey=user['apikey'])
# Create a new group.
request_data = {'name': 'a-new-group', 'title': 'A New Group'}
@@ -1545,7 +1605,7 @@ def test_create_group(self):
group_created = response_dict['result']
after = self.record_details(user['id'],
- group_ids=[group_created['id']])
+ group_ids=[group_created['id']], apikey=user['apikey'])
# Find the new activity.
new_activities = find_new_activities(before['user activity stream'],
@@ -1558,7 +1618,7 @@ def test_create_group(self):
new_activities, ("The same activity should also appear in "
"the group's activity stream.")
- self.check_dashboard(before, after, new_activities, [user['id']])
+ self.check_dashboards(before, after, activity)
# Check that the new activity has the right attributes.
assert activity['object_id'] == group_created['id'], \
@@ -1601,13 +1661,15 @@ def test_add_tag(self):
pkg_dict = package_show(self.app, {'id': pkg_name})
# Add one new tag to the package.
- before = self.record_details(user['id'], pkg_dict['id'])
+ before = self.record_details(user['id'], pkg_dict['id'],
+ apikey=user['apikey'])
new_tag_name = 'test tag'
assert new_tag_name not in [tag['name'] for tag in pkg_dict['tags']]
pkg_dict['tags'].append({'name': new_tag_name})
package_update(self.app, pkg_dict, user['apikey'])
- after = self.record_details(user['id'], pkg_dict['id'])
+ after = self.record_details(user['id'], pkg_dict['id'],
+ apikey=user['apikey'])
# Find the new activity in the user's activity stream.
user_new_activities = (find_new_activities(
@@ -1630,8 +1692,7 @@ def test_add_tag(self):
after['recently changed datasets stream']) \
== user_new_activities
- self.check_dashboard(before, after, user_new_activities,
- [user['id'], pkg_dict['id']])
+ self.check_dashboards(before, after, activity)
# If the package has any groups, the same new activity should appear
# in the activity stream of each group.
@@ -2033,7 +2094,8 @@ def test_delete_extras(self):
def test_follow_dataset(self):
user = self.normal_user
- before = self.record_details(user['id'])
+ before = self.record_details(user['id'], self.warandpeace['id'],
+ apikey=user['apikey'])
data = {'id': self.warandpeace['id']}
extra_environ = {'Authorization': str(user['apikey'])}
response = self.app.post('/api/action/follow_dataset',
@@ -2041,7 +2103,8 @@ def test_follow_dataset(self):
response_dict = json.loads(response.body)
assert response_dict['success'] is True
- after = self.record_details(user['id'], self.warandpeace['id'])
+ after = self.record_details(user['id'], self.warandpeace['id'],
+ apikey=user['apikey'])
# Find the new activity in the user's activity stream.
user_new_activities = (find_new_activities(
@@ -2056,7 +2119,27 @@ def test_follow_dataset(self):
for activity in user_new_activities:
assert activity in pkg_new_activities
- self.check_dashboard(before, after, user_new_activities, [user['id']])
+ # The new activity should appear in the user's dashboard activity
+ # stream.
+ new_activities = [activity_ for activity_ in
+ after['user dashboard activity stream']
+ if activity_ not in before['user dashboard activity stream']]
+ # There will be other new activities besides the 'follow dataset' one
+ # because all the dataset's old activities appear in the user's
+ # dashboard when she starts to follow the dataset.
+ assert activity['id'] in [
+ activity['id'] for activity in new_activities]
+
+ # The new activity should appear in the user "follower"'s dashboard
+ # activity stream because she follows all the other users and datasets.
+ new_activities = [activity_ for activity_ in
+ after['follower dashboard activity stream']
+ if activity_ not in before['follower dashboard activity stream']]
+ # There will be other new activities besides the 'follow dataset' one
+ # because all the dataset's old activities appear in the user's
+ # dashboard when she starts to follow the dataset.
+ assert [activity['id'] for activity in new_activities] == [
+ activity['id']]
# Check that the new activity has the right attributes.
assert activity['object_id'] == self.warandpeace['id'], \
@@ -2077,8 +2160,9 @@ def test_follow_dataset(self):
def test_follow_user(self):
user = self.normal_user
- before = self.record_details(user['id'])
- followee_before = self.record_details(self.sysadmin_user['id'])
+ before = self.record_details(user['id'], apikey=user['apikey'])
+ followee_before = self.record_details(self.sysadmin_user['id'],
+ apikey=self.sysadmin_user['apikey'])
data = {'id': self.sysadmin_user['id']}
extra_environ = {'Authorization': str(user['apikey'])}
response = self.app.post('/api/action/follow_user',
@@ -2086,8 +2170,9 @@ def test_follow_user(self):
response_dict = json.loads(response.body)
assert response_dict['success'] is True
- after = self.record_details(user['id'])
- followee_after = self.record_details(self.sysadmin_user['id'])
+ after = self.record_details(user['id'], apikey=user['apikey'])
+ followee_after = self.record_details(self.sysadmin_user['id'],
+ apikey=self.sysadmin_user['apikey'])
# Find the new activity in the user's activity stream.
user_new_activities = (find_new_activities(
@@ -2105,17 +2190,7 @@ def test_follow_user(self):
assert len(user_new_activities) == 1, ("There should be 1 new "
" activity in the user's activity stream, but found %i" %
len(user_new_activities))
- assert user_new_activities[0] == activity
-
- # Check that the new activity appears in the followee's private
- # activity stream.
- followee_new_activities = (find_new_activities(
- followee_before['follower dashboard activity stream'],
- followee_after['follower dashboard activity stream']))
- assert len(followee_new_activities) == 1, ("There should be 1 new "
- " activity in the user's activity stream, but found %i" %
- len(followee_new_activities))
- assert followee_new_activities[0] == activity
+ assert user_new_activities[0]['id'] == activity['id']
# Check that the new activity has the right attributes.
assert activity['object_id'] == self.sysadmin_user['id'], \
diff --git a/ckan/tests/functional/api/test_dashboard.py b/ckan/tests/functional/api/test_dashboard.py
new file mode 100644
index 00000000000..5c5cbd8443f
--- /dev/null
+++ b/ckan/tests/functional/api/test_dashboard.py
@@ -0,0 +1,205 @@
+import ckan
+from ckan.lib.helpers import json
+import paste
+import pylons.test
+
+
+class TestDashboard(object):
+ '''Tests for the logic action functions related to the user's dashboard.'''
+
+ @classmethod
+ def user_create(cls):
+ '''Create a new user.'''
+ params = json.dumps({
+ 'name': 'mr_new_user',
+ 'email': 'mr@newuser.com',
+ 'password': 'iammrnew',
+ })
+ response = cls.app.post('/api/action/user_create', params=params,
+ extra_environ={'Authorization': str(cls.joeadmin['apikey'])})
+ assert response.json['success'] is True
+ new_user = response.json['result']
+ return new_user
+
+ @classmethod
+ def setup_class(cls):
+ ckan.tests.CreateTestData.create()
+ cls.app = paste.fixture.TestApp(pylons.test.pylonsapp)
+ joeadmin = ckan.model.User.get('joeadmin')
+ cls.joeadmin = {
+ 'id': joeadmin.id,
+ 'apikey': joeadmin.apikey
+ }
+ annafan = ckan.model.User.get('annafan')
+ cls.annafan = {
+ 'id': annafan.id,
+ 'apikey': annafan.apikey
+ }
+ testsysadmin = ckan.model.User.get('testsysadmin')
+ cls.testsysadmin = {
+ 'id': testsysadmin.id,
+ 'apikey': testsysadmin.apikey
+ }
+ cls.new_user = cls.user_create()
+
+ @classmethod
+ def teardown_class(cls):
+ ckan.model.repo.rebuild_db()
+
+ def dashboard_new_activities_count(self, user):
+ '''Return the given user's new activities count from the CKAN API.'''
+ params = json.dumps({})
+ response = self.app.post('/api/action/dashboard_new_activities_count',
+ params=params,
+ extra_environ={'Authorization': str(user['apikey'])})
+ assert response.json['success'] is True
+ new_activities_count = response.json['result']
+ return new_activities_count
+
+ def dashboard_activity_list(self, user):
+ '''Return the given user's dashboard activity list from the CKAN API.
+
+ '''
+ params = json.dumps({})
+ response = self.app.post('/api/action/dashboard_activity_list',
+ params=params,
+ extra_environ={'Authorization': str(user['apikey'])})
+ assert response.json['success'] is True
+ activity_list = response.json['result']
+ return activity_list
+
+ def dashboard_new_activities(self, user):
+ '''Return the activities from the user's dashboard activity stream
+ that are currently marked as new.'''
+ activity_list = self.dashboard_activity_list(user)
+ return [activity for activity in activity_list if activity['is_new']]
+
+ def dashboard_mark_all_new_activities_as_old(self, user):
+ params = json.dumps({})
+ response = self.app.post(
+ '/api/action/dashboard_mark_all_new_activities_as_old',
+ params=params,
+ extra_environ={'Authorization': str(user['apikey'])})
+ assert response.json['success'] is True
+
+ def test_01_new_activities_count_for_new_user(self):
+ '''Test that a newly registered user's new activities count is 0.'''
+ assert self.dashboard_new_activities_count(self.new_user) == 0
+
+ def test_01_new_activities_for_new_user(self):
+ '''Test that a newly registered user has no activities marked as new
+ in their dashboard activity stream.'''
+ assert len(self.dashboard_new_activities(self.new_user)) == 0
+
+ def test_02_own_activities_do_not_count_as_new(self):
+ '''Make a user do some activities and check that her own activities
+ don't increase her new activities count.'''
+
+ # The user has to view her dashboard activity stream first to mark any
+ # existing activities as read. For example when she follows a dataset
+ # below, past activities from the dataset (e.g. when someone created
+ # the dataset, etc.) will appear in her dashboard, and if she has never
+ # viewed her dashboard then those activities will be considered
+ # "unseen".
+ # We would have to do this if, when you follow something, you only get
+ # the activities from that object since you started following it, and
+ # not all its past activities as well.
+ self.dashboard_mark_all_new_activities_as_old(self.new_user)
+
+ # Create a new dataset.
+ params = json.dumps({
+ 'name': 'my_new_package',
+ })
+ response = self.app.post('/api/action/package_create', params=params,
+ extra_environ={'Authorization': str(self.new_user['apikey'])})
+ assert response.json['success'] is True
+
+ # Follow a dataset.
+ params = json.dumps({'id': 'warandpeace'})
+ response = self.app.post('/api/action/follow_dataset', params=params,
+ extra_environ={'Authorization': str(self.new_user['apikey'])})
+ assert response.json['success'] is True
+
+ # Follow a user.
+ params = json.dumps({'id': 'annafan'})
+ response = self.app.post('/api/action/follow_user', params=params,
+ extra_environ={'Authorization': str(self.new_user['apikey'])})
+ assert response.json['success'] is True
+
+ # Follow a group.
+ params = json.dumps({'id': 'roger'})
+ response = self.app.post('/api/action/follow_group', params=params,
+ extra_environ={'Authorization': str(self.new_user['apikey'])})
+ assert response.json['success'] is True
+
+ # Update the dataset that we're following.
+ params = json.dumps({'name': 'warandpeace', 'notes': 'updated'})
+ response = self.app.post('/api/action/package_update', params=params,
+ extra_environ={'Authorization': str(self.new_user['apikey'])})
+ assert response.json['success'] is True
+
+ # User's own actions should not increase her activity count.
+ assert self.dashboard_new_activities_count(self.new_user) == 0
+
+ def test_03_own_activities_not_marked_as_new(self):
+ '''Make a user do some activities and check that her own activities
+ aren't marked as new in her dashboard activity stream.'''
+ assert len(self.dashboard_new_activities(self.new_user)) == 0
+
+ def test_04_new_activities_count(self):
+ '''Test that new activities from objects that a user follows increase
+ her new activities count.'''
+
+ # Make someone else who new_user is not following update a dataset that
+ # new_user is following.
+ params = json.dumps({'name': 'warandpeace', 'notes': 'updated again'})
+ response = self.app.post('/api/action/package_update', params=params,
+ extra_environ={'Authorization': str(self.joeadmin['apikey'])})
+ assert response.json['success'] is True
+
+ # Make someone that the user is following create a new dataset.
+ params = json.dumps({'name': 'annas_new_dataset'})
+ response = self.app.post('/api/action/package_create', params=params,
+ extra_environ={'Authorization': str(self.annafan['apikey'])})
+ assert response.json['success'] is True
+
+ # Make someone that the user is not following update a dataset that
+ # the user is not following, but that belongs to a group that the user
+ # is following.
+ params = json.dumps({'name': 'annakarenina', 'notes': 'updated'})
+ response = self.app.post('/api/action/package_update', params=params,
+ extra_environ={'Authorization': str(self.testsysadmin['apikey'])})
+ assert response.json['success'] is True
+
+ # FIXME: The number here should be 3 but activities from followed
+ # groups are not appearing in dashboard. When that is fixed, fix this
+ # number.
+ assert self.dashboard_new_activities_count(self.new_user) == 2
+
+ def test_05_activities_marked_as_new(self):
+ '''Test that new activities from objects that a user follows are
+ marked as new in her dashboard activity stream.'''
+ # FIXME: The number here should be 3 but activities from followed
+ # groups are not appearing in dashboard. When that is fixed, fix this
+ # number.
+ assert len(self.dashboard_new_activities(self.new_user)) == 2
+
+ def test_06_mark_new_activities_as_read(self):
+ '''Test that a user's new activities are marked as old when she views
+ her dashboard activity stream.'''
+ assert self.dashboard_new_activities_count(self.new_user) > 0
+ assert len(self.dashboard_new_activities(self.new_user)) > 0
+ self.dashboard_mark_all_new_activities_as_old(self.new_user)
+ assert self.dashboard_new_activities_count(self.new_user) == 0
+ assert len(self.dashboard_new_activities(self.new_user)) == 0
+
+ def test_07_maximum_number_of_new_activities(self):
+ '''Test that the new activities count does not go higher than 15, even
+ if there are more than 15 new activities from the user's followers.'''
+ for n in range(0,20):
+ notes = "Updated {n} times".format(n=n)
+ params = json.dumps({'name': 'warandpeace', 'notes': notes})
+ response = self.app.post('/api/action/package_update', params=params,
+ extra_environ={'Authorization': str(self.joeadmin['apikey'])})
+ assert response.json['success'] is True
+ assert self.dashboard_new_activities_count(self.new_user) == 15
diff --git a/ckan_deb/usr/lib/ckan/common.sh b/ckan_deb/usr/lib/ckan/common.sh
index 0c4a33306eb..acd9abf863b 100644
--- a/ckan_deb/usr/lib/ckan/common.sh
+++ b/ckan_deb/usr/lib/ckan/common.sh
@@ -155,7 +155,7 @@ ckan_ensure_db_exists () {
COMMAND_OUTPUT=`sudo -u postgres psql -c "select datname from pg_database where datname='$INSTANCE'"`
if ! [[ "$COMMAND_OUTPUT" =~ ${INSTANCE} ]] ; then
echo "Creating the database ..."
- sudo -u postgres createdb -O ${INSTANCE} ${INSTANCE}
+ sudo -u postgres createdb -O ${INSTANCE} ${INSTANCE} -E utf-8
paster --plugin=ckan db init --config=/etc/ckan/${INSTANCE}/${INSTANCE}.ini
fi
fi
diff --git a/doc/architecture.rst b/doc/architecture.rst
new file mode 100644
index 00000000000..13098bddb5b
--- /dev/null
+++ b/doc/architecture.rst
@@ -0,0 +1,206 @@
+======================
+CKAN Code Architecture
+======================
+
+This section tries to give some guidelines for writing code that is consistent
+with the intended, overall design and architecture of CKAN.
+
+
+Encapsulate SQLAlchemy in ``ckan.model``
+````````````````````````````````````````
+
+Ideally SQLAlchemy should only be used within ``ckan.model`` and not from other
+packages such as ``ckan.logic``. For example instead of using an SQLAlchemy
+query from the logic package to retrieve a particular user from the database,
+we add a ``get()`` method to ``ckan.model.user.User``::
+
+ @classmethod
+ def get(cls, user_id):
+ query = ...
+ .
+ .
+ .
+ return query.first()
+
+Now we can call this method from the logic package.
+
+Database Migrations
+```````````````````
+
+When changes are made to the model classes in ``ckan.model`` that alter CKAN's
+database schema, a migration script has to be added to migrate old CKAN
+databases to the new database schema when they upgrade their copies of CKAN.
+See :doc:`migration`.
+
+Always go through the Action Functions
+``````````````````````````````````````
+
+Whenever some code, for example in ``ckan.lib`` or ``ckan.controllers``, wants
+to get, create, update or delete an object from CKAN's model it should do so by
+calling a function from the ``ckan.logic.action`` package, and *not* by
+accessing ``ckan.model`` directly.
+
+
+Action Functions are Exposed in the API
+```````````````````````````````````````
+
+The functions in ``ckan.logic.action`` are exposed to the world as the
+:doc:`apiv3`. The API URL for an action function is automatically generated
+from the function name, for example
+``ckan.logic.action.create.package_create()`` is exposed at
+``/api/action/package_create``. See `Steve Yegge's Google platforms rant
+`_ for some
+interesting discussion about APIs.
+
+**All** publicly visible functions in the
+``ckan.logic.action.{create,delete,get,update}`` namespaces will be exposed
+through the :doc:`apiv3`. **This includes functions imported** by those
+modules, **as well as any helper functions** defined within those modules. To
+prevent inadvertent exposure of non-action functions through the action api,
+care should be taken to:
+
+1. Import modules correctly (see `Imports`_). For example: ::
+
+ import ckan.lib.search as search
+
+ search.query_for(...)
+
+2. Hide any locally defined helper functions: ::
+
+ def _a_useful_helper_function(x, y, z):
+ '''This function is not exposed because it is marked as private```
+ return x+y+z
+
+3. Bring imported convenience functions into the module namespace as private
+ members: ::
+
+ _get_or_bust = logic.get_or_bust
+
+
+Use ``get_action()``
+````````````````
+
+Don't call ``logic.action`` functions directly, instead use ``get_action()``.
+This allows plugins to override action functions using the ``IActions`` plugin
+interface. For example::
+
+ ckan.logic.get_action('group_activity_list_html')(...)
+
+Instead of ::
+
+ ckan.logic.action.get.group_activity_list_html(...)
+
+
+Auth Functions and ``check_access()``
+``````````````
+
+Each action function defined in ``ckan.logic.action`` should use its own
+corresponding auth function defined in ``ckan.logic.auth``. Instead of calling
+its auth function directly, an action function should go through
+``ckan.logic.check_access`` (which is aliased ``_check_access`` in the action
+modules) because this allows plugins to override auth functions using the
+``IAuthFunctions`` plugin interface. For example::
+
+ def package_show(context, data_dict):
+ _check_access('package_show', context, data_dict)
+
+``check_access`` will raise an exception if the user is not authorized, which
+the action function should not catch. When this happens the user will be shown
+an authorization error in their browser (or will receive one in their response
+from the API).
+
+
+``logic.get_or_bust()``
+`````````````
+
+The ``data_dict`` parameter of logic action functions may be user provided, so
+required files may be invalid or absent. Naive Code like::
+
+ id = data_dict['id']
+
+may raise a ``KeyError`` and cause CKAN to crash with a 500 Server Error
+and no message to explain what went wrong. Instead do::
+
+ id = _get_or_bust(data_dict, "id")
+
+which will raise ``ValidationError`` if ``"id"`` is not in ``data_dict``. The
+``ValidationError`` will be caught and the user will get a 400 Bad Request
+response and an error message explaining the problem.
+
+
+Validation and ``ckan.logic.schema``
+````````````````````````````````````
+
+Logic action functions can use schema defined in ``ckan.logic.schema`` to
+validate the contents of the ``data_dict`` parameters that users pass to them.
+
+An action function should first check for a custom schema provided in the
+context, and failing that should retrieve its default schema directly, and
+then call ``_validate()`` to validate and convert the data. For example, here
+is the validation code from the ``user_create()`` action function::
+
+ schema = context.get('schema') or ckan.logic.schema.default_user_schema()
+ session = context['session']
+ validated_data_dict, errors = _validate(data_dict, schema, context)
+ if errors:
+ session.rollback()
+ raise ValidationError(errors)
+
+
+Controller & Template Helper Functions
+--------------------------------------
+
+``ckan.lib.helpers`` contains helper functions that can be used from
+``ckan.controllers`` or from templates. When developing for ckan core, only use
+the helper functions found in ``ckan.lib.helpers.__allowed_functions__``.
+
+
+.. _Testing:
+
+Testing
+-------
+
+- Functional tests which test the behaviour of the web user interface, and the
+ APIs should be placed within ``ckan/tests/functional``. These tests can be a
+ lot slower to run that unit tests which don't access the database or solr. So
+ try to bear that in mind, and attempt to cover just what is neccessary, leaving
+ what can be tested via unit-testing in unit-tests.
+
+- ``nose.tools.assert_in`` and ``nose.tools.assert_not_in`` are only available
+ in Python>=2.7. So import them from ``ckan.tests``, which will provide
+ alternatives if they're not available.
+
+- the `mock`_ library can be used to create and interrogate mock objects.
+
+See :doc:`test` for further information on testing in CKAN.
+
+.. _mock: http://pypi.python.org/pypi/mock
+
+Writing Extensions
+------------------
+
+Please see :doc:`writing-extensions` for information about writing ckan
+extensions, including details on the API available to extensions.
+
+Deprecation
+-----------
+
+- Anything that may be used by extensions (see :doc:`writing-extensions`) needs
+ to maintain backward compatibility at call-site. ie - template helper
+ functions and functions defined in the plugins toolkit.
+
+- The length of time of deprecation is evaluated on a function-by-function
+ basis. At minimum, a function should be marked as deprecated during a point
+ release.
+
+- To mark a helper function, use the ``deprecated`` decorator found in
+ ``ckan.lib.maintain`` eg: ::
+
+
+ @deprecated()
+ def facet_items(*args, **kwargs):
+ """
+ DEPRECATED: Use the new facet data structure, and `unselected_facet_items()`
+ """
+ # rest of function definition.
+
diff --git a/doc/buildbot.rst b/doc/buildbot.rst
deleted file mode 100644
index 484518b555e..00000000000
--- a/doc/buildbot.rst
+++ /dev/null
@@ -1,169 +0,0 @@
-================
-Install Buildbot
-================
-
-This section provides information for CKAN core developers setting up buildbot on an Ubuntu Lucid machine.
-
-If you simply want to check the status of the latest CKAN builds, visit http://buildbot.okfn.org/.
-
-Apt Installs
-============
-
-Install CKAN core dependencies from Lucid distribution::
-
- sudo apt-get install build-essential libxml2-dev libxslt-dev
- sudo apt-get install wget mercurial postgresql libpq-dev git-core
- sudo apt-get install python-dev python-psycopg2 python-virtualenv
- sudo apt-get install subversion
-
-Maybe need this too::
-
- sudo apt-get install python-include
-
-Buildbot software::
-
- sudo apt-get install buildbot
-
-Deb building software::
-
- sudo apt-get install -y dh-make devscripts fakeroot cdbs
-
-Fabric::
-
- sudo apt-get install -y fabric
-
-If you get errors with postgres and locales you might need to do these::
-
- sudo apt-get install language-pack-en-base
- sudo dpkg-reconfigure locales
-
-
-Postgres Setup
-==============
-
-If installation before failed to create a cluster, do this after fixing errors::
-
- sudo pg_createcluster 8.4 main --start
-
-Create users and databases::
-
- sudo -u postgres createuser -S -D -R -P buildslave
- # set this password (matches buildbot scripts): biomaik15
- sudo -u postgres createdb -O buildslave ckan1
- sudo -u postgres createdb -O buildslave ckanext
-
-
-Buildslave Setup
-================
-
-Rough commands::
-
- sudo useradd -m -s /bin/bash buildslave
- sudo chown buildslave:buildslave /home/buildslave
- sudo su buildslave
- cd ~
- git clone https://github.com/okfn/buildbot-scripts.git
- ssh-keygen -t rsa
- cp /home/buildslave/.ssh/id_rsa.pub ~/.ssh/authorized_keys
- mkdir -p ckan/build
- cd ckan/build
- python ~/ckan-default.py
- buildbot create-slave ~ localhost:9989 okfn
- vim ~/info/admin
- vim ~/info/host
- mkdir /home/buildslave/pip_cache
- virtualenv pyenv-tools
- pip -E pyenv-tools install buildkit
-
-
-Buildmaster Setup
-=================
-
-Rough commands::
-
- mkdir ~/buildmaster
- buildbot create-master ~/buildmaster
- ln -s /home/buildslave/master/master.cfg ~/buildmaster/master.cfg
- cd ~/buildmaster
- buildbot checkconfig
-
-
-Startup
-=======
-
-Setup the daemons for master and slave::
-
- sudo vim /etc/default/buildbot
-
-This file should be edited to be like this::
-
- BB_NUMBER[0]=0 # index for the other values; negative disables the bot
- BB_NAME[0]="okfn" # short name printed on startup / stop
- BB_USER[0]="okfn" # user to run as
- BB_BASEDIR[0]="/home/okfn/buildmaster" # basedir argument to buildbot (absolute path)
- BB_OPTIONS[0]="" # buildbot options
- BB_PREFIXCMD[0]="" # prefix command, i.e. nice, linux32, dchroot
-
- BB_NUMBER[1]=1 # index for the other values; negative disables the bot
- BB_NAME[1]="okfn" # short name printed on startup / stop
- BB_USER[1]="buildslave" # user to run as
- BB_BASEDIR[1]="/home/buildslave" # basedir argument to buildbot (absolute path)
- BB_OPTIONS[1]="" # buildbot options
- BB_PREFIXCMD[1]="" # prefix command, i.e. nice, linux32, dchroot
-
-Start master and slave (according to /etc/default/buildbot)::
-
- sudo /etc/init.d/buildbot start
-
-Now check you can view buildbot at http://localhost:8010/
-
-
-Connect Ports
-=============
-
-It's preferable to view the buildbot site at port 80 rather than 8010.
-
-If there is no other web service on this machine, you might connect up the addresses using ``iptables``::
-
- sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8010
-
-Otherwise it is best to set up a reverse proxy. Using Apache, edit this file::
-
- sudo vim /etc/apache2/sites-available/buildbot.okfn.org
-
-to look like this::
-
-
- ServerName buildbot.okfn.org
-
- ProxyPassReverse ts Off
-
- Order deny,allow
- Allow from all
-
- ProxyPass / http://127.0.0.1:8010/
- ProxyPassReverse / http://127.0.0.1:8010/
- ProxyPreserveHost On
-
-
-or the old one had::
-
-
- ServerAdmin sysadmin@okfn.org
- ServerName buildbot.okfn.org
- DocumentRoot /var/www/
-
- Order allow,deny
- allow from all
-
- RewriteEngine On
- RewriteRule /(.*) http://localhost:8010/$1 [P,L]
-
-
-Then::
-
- sudo apt-get install libapache2-mod-proxy-html
- sudo a2enmod proxy_http
- sudo a2ensite buildbot.okfn.org
- sudo /etc/init.d/apache2 reload
-
diff --git a/doc/coding-standards.rst b/doc/coding-standards.rst
deleted file mode 100644
index 731c12cb760..00000000000
--- a/doc/coding-standards.rst
+++ /dev/null
@@ -1,1346 +0,0 @@
-=====================
-CKAN Coding Standards
-=====================
-
-Commit Guidelines
-=================
-
-Generally, follow the `commit guidelines from the Pro Git book`_:
-
-- Try to make each commit a logically separate, digestible changeset.
-
-- The first line of the commit message should concisely summarise the
- changeset.
-
-- Optionally, follow with a blank line and then a more detailed explanation of
- the changeset.
-
-- Use the imperative present tense as if you were giving commands to the
- codebase to change its behaviour, e.g. *Add tests for...*, *make xyzzy do
- frotz...*, this helps to make the commit message easy to read.
-
-- Try to write the commit message so that a new CKAN developer could understand
- it, i.e. using plain English as far as possible, and not referring to too
- much assumed knowledge or to external resources such as mailing list
- discussions (summarize the relevant points in the commit message instead).
-
-.. _commit guidelines from the Pro Git book: http://git-scm.com/book/en/Distributed-Git-Contributing-to-a-Project#Commit-Guidelines
-
-In CKAN we also refer to `trac.ckan.org`_ ticket numbers in commit messages
-wherever relevant. This makes the release manager's job much easier! Of
-course, you don't have to reference a ticket from your commit message if there
-isn't a ticket for it, e.g. if you find a typo in a docstring and quickly fix
-it you wouldn't bother to create a ticket for this.
-
-Put the ticket number in square brackets (e.g. ``[#123]``) at the start of the
-first line of the commit message. You can also reference other Trac tickets
-elsewhere in your commit message by just using the ticket number on its own
-(e.g. ``see #456``). For example:
-
-::
-
- [#2505] Update source install instructions
-
- Following feedback from markw (see #2406).
-
-.. _trac.ckan.org: http://trac.ckan.org/
-
-Longer example CKAN commit message:
-
-::
-
- [#2304] Refactor user controller a little
-
- Move initialisation of a few more template variables into
- _setup_template_variables(), and change read(), edit(), and followers() to use
- it. This removes some code duplication and fixes issues with the followers
- count and follow button not being initialisd on all user controller pages.
-
- Change new() to _not_ use _setup_template_variables() as it only needs
- c.is_sysadmin and not the rest.
-
- Also fix templates/user/layout.html so that the Followers tab appears on both
- your own user page (when logged in) and on other user's pages.
-
-Feature Branches
-----------------
-
-All ticketed work should be developed on a corresponding feature branch forked
-from master. The name of the branch should inlude the ticket's number, the
-ticket type, and a brief one-line synopsis of the purpose of the ticket. eg:
-``2298-feature-add-sort-by-controls-to-search-page``. This allows the ticket
-number to be esaily searchable through github's web interface.
-
-Once work on the branch has been completed and it is ready to be merged into
-master, make a pull request on github. Another member of the CKAN team will
-review the changes; and provide feedback through the github pull request page.
-If the piece of work touches on an area of code `owned` by another team member,
-then notify them of the changes by email.
-
-Submitting Code Patches
------------------------
-
-See the wiki for instructions on `how to submit a patch`_ via GitHub or email.
-
-.. _how to submit a patch: http://wiki.ckan.org/Submitting_a_code_patch
-
-Releases
---------
-
-See :doc:`release-cycle` for details on the release process.
-
-Merging
--------
-
-When merging a feature or bug branch into master:
-
-- Use the ``--no-ff`` option in the ``git merge`` command
-- Add an entry to the ``CHANGELOG`` file
-
-The full postgresql test suite must pass before merging into master. ::
-
- nosetests --ckan --with-pylons=test-core.ini ckan
-
-See :doc:`test` for more information on running tests, including running the
-core extension tests.
-
-Python Coding Standards
-=======================
-
-For python code, we follow `PEP 8`_, plus a few of our own rules. The
-important bits are laid out below, but if in doubt, refer to `PEP 8`_ and
-common sense.
-
-Layout and formatting
----------------------
-
-- Don't use tabs. Use 4 spaces.
-
-- Maximum line length is 79 characters.
-
-- Continuation lines should align vertically within the parentheses, or with
- a hanging indent. See `PEP 8's Indent Section`_ for more details.
-
-- Avoid extraneous whitespace. See `PEP 8's Whitespace Section`_ for more details.
-
-- Clean up formatting issues in master, not on a feature branch. Unless of
- course you're changing that piece of code anyway. This will help avoid
- spurious merge conflicts, and aid in reading pull requests.
-
-- Use the single-quote character, ``'``, rather than the double-quote
- character, ``"``, for string literals.
-
-.. _PEP 8: http://www.python.org/dev/peps/pep-0008/
-.. _PEP 8's Indent Section: http://www.python.org/dev/peps/pep-0008/#indentation
-.. _PEP 8's Whitespace Section: http://www.python.org/dev/peps/pep-0008/#whitespace-in-expressions-and-statements
-
-Imports
--------
-
-- Import whole modules, rather than using ``from foo import bar``. It's ok
- to alias imported modules to make things more concise, ie this *is*
- acceptable: ::
-
- import foo.bar.baz as f
-
-- Make all imports at the start of the file, after the module docstring.
- Imports should be grouped in the following order:
-
- 1. Standard library imports
- 2. Third-party imports
- 3. CKAN imports
-
-Logging
--------
-
-- Keep messages short.
-
-- Don't include object representations in the log message. It **is** useful
- to include an domain model identifier where appropriate.
-
-- Choose an appropriate log-level:
-
- +----------+--------------------------------------------------------------+
- | Level | Description |
- +==========+==============================================================+
- | DEBUG | Detailed information, of no interest when everything is |
- | | working well but invaluable when diagnosing problems. |
- +----------+--------------------------------------------------------------+
- | INFO | Affirmations that things are working as expected, e.g. |
- | | "service has started" or "indexing run complete". Often |
- | | ignored. |
- +----------+--------------------------------------------------------------+
- | WARNING | There may be a problem in the near future, and this gives |
- | | advance warning of it. But the application is able to proceed|
- | | normally. |
- +----------+--------------------------------------------------------------+
- | ERROR | The application has been unable to proceed as expected, due |
- | | to the problem being logged. |
- +----------+--------------------------------------------------------------+
- | CRITICAL | This is a serious error, and some kind of application |
- | | meltdown might be imminent. |
- +----------+--------------------------------------------------------------+
-
- (`Source
- `_)
-
-i18n
-----
-
-To construct an internationalised string, use `str.format`_, giving
-meaningful names to each replacement field. For example: ::
-
- _(' ... {foo} ... {bar} ...').format(foo='foo-value', bar='bar-value')
-
-.. _str.format: http://docs.python.org/library/stdtypes.html#str.format
-
-Docstring Standards
--------------------
-
-We want CKAN's docstrings to be clear and easy to read for programmers who are
-smart and competent but who may not know a lot of CKAN technical jargon and
-whose first language may not be English. We also want it to be easy to maintain
-the docstrings and keep them up to date with the actual behaviour of the code
-as it changes over time. So:
-
-- Keep docstrings short, describe only what's necessary and no more
-- Keep docstrings simple, use plain English, try not to use a long word
- where a short one will do, and try to cut out words where possible
-- Try to avoid repetition
-
-PEP 257
-```````
-
-Generally, follow `PEP 257`_. We'll only describe the ways that CKAN differs
-from or extends PEP 257 below.
-
-.. _PEP 257: http://www.python.org/dev/peps/pep-0257/
-
-CKAN docstrings deviate from PEP 257 in a couple of ways:
-
-- We use ``'''triple single quotes'''`` around docstrings, not ``"""triple
- double quotes"""`` (put triple single quotes around one-line docstrings as
- well as multi-line ones, it makes them easier to expand later)
-- We use Sphinx directives for documenting parameters, exceptions and return
- values (see below)
-
-Sphinx
-``````
-Use `Sphinx directives`_ for documenting the parameters, exceptions and returns
-of functions:
-
-- Use ``:param`` and ``:type`` to describe each parameter
-- Use ``:returns`` and ``:rtype`` to describe each return
-- Use ``:raises`` to describe each exception raised
-
-Example of a short docstring:
-
-::
-
- @property
- def packages(self):
- '''Return a list of all packages that have this tag, sorted by name.
-
- :rtype: list of ckan.model.package.Package objects
-
- '''
-
-Example of a longer docstring:
-
-::
-
- @classmethod
- def search_by_name(cls, search_term, vocab_id_or_name=None):
- '''Return all tags whose names contain a given string.
-
- By default only free tags (tags which do not belong to any vocabulary)
- are returned. If the optional argument ``vocab_id_or_name`` is given
- then only tags from that vocabulary are returned.
-
- :param search_term: the string to search for in the tag names
- :type search_term: string
- :param vocab_id_or_name: the id or name of the vocabulary to look in
- (optional, default: None)
- :type vocab_id_or_name: string
-
- :returns: a list of tags that match the search term
- :rtype: list of ckan.model.tag.Tag objects
-
- '''
-
-
-The phrases that follow ``:param foo:``, ``:type foo:``, or ``:returns:``
-should not start with capital letters or end with full stops. These should be
-short phrases and not full sentences. If more detail is required put it in the
-function description instead.
-
-Indicate optional arguments by ending their descriptions with (optional) in
-brackets. Where relevant also indicate the default value: (optional, default:
-5). It's also helpful to list all required parameters before optional ones.
-
-.. _Sphinx directives: http://sphinx.pocoo.org/markup/desc.html#info-field-lists
-
-You can also use a little inline `reStructuredText markup`_ in docstrings, e.g.
-``*stars for emphasis*`` or ````double-backticks for literal text````
-
-.. _reStructuredText markup: http://docutils.sourceforge.net/docs/user/rst/quickref.html#inline-markup
-
-CKAN Action API Docstrings
-``````````````````````````
-
-Docstrings from CKAN's action API are processed with `autodoc`_ and
-included in the API chapter of CKAN's documentation. The intended audience of
-these docstrings is users of the CKAN API and not (just) CKAN core developers.
-
-In the Python source each API function has the same two arguments (``context``
-and ``data_dict``), but the docstrings should document the keys that the
-functions read from ``data_dict`` and not ``context`` and ``data_dict``
-themselves, as this is what the user has to POST in the JSON dict when calling
-the API.
-
-Where practical, it's helpful to give examples of param and return values in
-API docstrings.
-
-CKAN datasets used to be called packages and the old name still appears in the
-source, e.g. in function names like package_list(). When documenting functions
-like this write dataset not package, but the first time you do this put package
-after it in brackets to avoid any confusion, e.g.
-
-::
-
- def package_show(context, data_dict):
- '''Return the metadata of a dataset (package) and its resources.
-
-Example of a ckan.logic.action API docstring:
-
-::
-
- def vocabulary_create(context, data_dict):
- '''Create a new tag vocabulary.
-
- You must be a sysadmin to create vocabularies.
-
- :param name: the name of the new vocabulary, e.g. ``'Genre'``
- :type name: string
- :param tags: the new tags to add to the new vocabulary, for the format of
- tag dictionaries see ``tag_create()``
- :type tags: list of tag dictionaries
-
- :returns: the newly-created vocabulary
- :rtype: dictionary
-
- '''
-
-.. _Autodoc: http://sphinx.pocoo.org/ext/autodoc.html
-
-Tools
------
-
-Running the `PEP 8 style guide checker`_ is good for checking adherence to `PEP
-8`_ formatting. As mentioned above, only perform style clean-ups on master to
-help avoid spurious merge conflicts.
-
-`PyLint`_ is a useful tool for analysing python source code for errors and signs of poor quality.
-
-`pyflakes`_ is another useful tool for passive analysis of python source code.
-There's also a `pyflakes vim plugin`_ which will highlight unused variables,
-undeclared variables, syntax errors and unused imports.
-
-.. _PEP 8 style guide checker: http://pypi.python.org/pypi/pep8
-.. _PyLint: http://www.logilab.org/857
-.. _pyflakes: http://pypi.python.org/pypi/pyflakes
-.. _pyflakes vim plugin: http://www.vim.org/scripts/script.php?script_id=2441
-
-CKAN Code Areas
-===============
-
-This section describes some guidelines for making changes in particular areas
-of the codebase, as well as general concepts particular to CKAN.
-
-General
--------
-
-Some rules to adhere to when making changes to the codebase in general.
-
-.. todo:: Is there anything to include in this 'General' section?
-
-Domain Models
--------------
-
-This section describes things to bear in mind when making changes to the domain
-models. For more information about CKAN's domain models, see
-:doc:`domain-model`.
-
-The structure of the CKAN data is described in the 'model'. This is in the code
-at `ckan/model`.
-
-Many of the domain objects are Revisioned and some are Stateful. These are
-concepts introduced by `vdm`_.
-
-.. _vdm: http://okfn.org/projects/vdm/
-.. _sqlalchemy migrate: http://code.google.com/p/sqlalchemy-migrate SQLAlchemy Migrate
-
-Migration
-`````````
-When edits are made to the model code, then before the code can be used on a
-CKAN instance with existing data, the existing data has to be migrated. This is
-achieved with a migration script.
-
-CKAN currently uses to manage these scripts. When you deploy new code to a
-CKAN instance, as part of the process you run any required migration scripts
-with: ::
-
- paster --plugin=ckan db upgrade --config={.ini file}
-
-The scripts give their model version numbers in their filenames and are stored
-in ``ckan/migration/versions/``.
-
-The current version the database is migrated to is also stored in the database.
-When you run the upgrade, as each migration script is run it prints to the
-console something like ``11->12``. If no upgrade is required because it is up
-to date, then nothing is printed.
-
-Creating a new migration script
-```````````````````````````````
-A migration script should be checked into CKAN at the same time as the model
-changes it is related to. Before pushing the changes, ensure the tests pass
-when running against the migrated model, which requires the
-``--ckan-migration`` setting.
-
-To create a new migration script, create a python file in
-``ckan/migration/versions/`` and name it with a prefix numbered one higher than
-the previous one and some words describing the change.
-
-You need to use the special engine provided by the SqlAlchemy Migrate. Here is
-the standard header for your migrate script: ::
-
- from sqlalchemy import *
- from migrate import *
-
-The migration operations go in the upgrade function: ::
-
- def upgrade(migrate_engine):
- metadata = MetaData()
- metadata.bind = migrate_engine
-
-The following process should be followed when doing a migration. This process
-is here to make the process easier and to validate if any mistakes have been
-made:
-
-1. Get a dump of the database schema before you add your new migrate scripts. ::
-
- paster --plugin=ckan db clean --config={.ini file}
- paster --plugin=ckan db upgrade --config={.ini file}
- pg_dump -h host -s -f old.sql dbname
-
-2. Get a dump of the database as you have specified it in the model. ::
-
- paster --plugin=ckan db clean --config={.ini file}
-
- #this makes the database as defined in the model
- paster --plugin=ckan db create-from-model -config={.ini file}
- pg_dump -h host -s -f new.sql dbname
-
-3. Get agpdiff (apt-get it). It produces sql it thinks that you need to run on
- the database in order to get it to the updated schema. ::
-
- apgdiff old.sql new.sql > upgrade.diff
-
-(or if you don't want to install java use http://apgdiff.startnet.biz/diff_online.php)
-
-4. The upgrade.diff file created will have all the changes needed in sql.
- Delete the drop index lines as they are not created in the model.
-
-5. Put the resulting sql in your migrate script, e.g. ::
-
- migrate_engine.execute('''update table .........; update table ....''')
-
-6. Do a dump again, then a diff again to see if the the only thing left are drop index statements.
-
-7. run nosetests with ``--ckan-migration`` flag.
-
-It's that simple. Well almost.
-
-* If you are doing any table/field renaming adding that to your new migrate
- script first and use this as a base for your diff (i.e add a migrate script
- with these renaming before 1). This way the resulting sql won't try to drop and
- recreate the field/table!
-
-* It sometimes drops the foreign key constraints in the wrong order causing an
- error so you may need to rearrange the order in the resulting upgrade.diff.
-
-* If you need to do any data transfer in the migrations then do it between the
- dropping of the constraints and adding of new ones.
-
-* May need to add some tests if you are doing data migrations.
-
-An example of a script doing it this way is ``034_resource_group_table.py``.
-This script copies the definitions of the original tables in order to do the
-renaming the tables/fields.
-
-In order to do some basic data migration testing extra assertions should be
-added to the migration script. Examples of this can also be found in
-``034_resource_group_table.py`` for example.
-
-This statement is run at the top of the migration script to get the count of
-rows: ::
-
- package_count = migrate_engine.execute('''select count(*) from package''').first()[0]
-
-And the following is run after to make sure that row count is the same: ::
-
- resource_group_after = migrate_engine.execute('''select count(*) from resource_group''').first()[0]
- assert resource_group_after == package_count
-
-The Action Layer
-----------------
-
-When making changes to the action layer, found in the four modules
-``ckan/logic/action/{create,delete,get,update}`` there are a few things to bear
-in mind.
-
-Server Errors
-`````````````
-
-When writing action layer code, bear in mind that the input provided in the
-``data_dict`` may be user-provided. This means that required fields should be
-checked for existence and validity prior to use. For example, code such as ::
-
- id = data_dict['id']
-
-will raise a ``KeyError`` if the user hasn't provided an ``id`` field in their
-data dict. This results in a 500 error, and no message to explain what went
-wrong. The correct response by the action function would be to raise a
-``ValidationError`` instead, as this will be caught and will provide the user
-with a `bad request` response, alongside an error message explaining the issue.
-
-To this end, there's a helper function, ``logic.get_or_bust()`` which can be
-used to safely retrieve a value from a dict: ::
-
- id = _get_or_bust(data_dict, "id")
-
-Function visibility
-```````````````````
-
-**All** publicly visible functions in the
-``ckan.logic.action.{create,delete,get,update}`` namespaces will be exposed
-through the :doc:`apiv3`. **This includes functions imported** by those
-modules, **as well as any helper functions** defined within those modules. To
-prevent inadvertent exposure of non-action functions through the action api,
-care should be taken to:
-
-1. Import modules correctly (see `Imports`_). For example: ::
-
- import ckan.lib.search as search
-
- search.query_for(...)
-
-2. Hide any locally defined helper functions: ::
-
- def _a_useful_helper_function(x, y, z):
- '''This function is not exposed because it is marked as private```
- return x+y+z
-
-3. Bring imported convenience functions into the module namespace as private
- members: ::
-
- _get_or_bust = logic.get_or_bust
-
-Documentation
-`````````````
-
-Please refer to `CKAN Action API Docstrings`_ for information about writing
-docstrings for the action functions. It is **very** important that action
-functions are documented as they are not only consumed by CKAN developers but
-by CKAN users.
-
-Controllers
------------
-
-Guidelines when writing controller actions:
-
-- Use ``get_action``, rather than calling the action directly; and rather than
- calling the action directly, as this allows extensions to overide the action's
- behaviour. ie use ::
-
- ckan.logic.get_action('group_activity_list_html')(...)
-
- Instead of ::
-
- ckan.logic.action.get.group_activity_list_html(...)
-
-- Controllers have access to helper functions in ``ckan.lib.helpers``.
- When developing for ckan core, only use the helper functions found in
- ``ckan.lib.helpers.__allowed_functions__``.
-
-.. todo:: Anything else for controllers?
-
-Templating
-----------
-
-Helper Functions
-````````````````
-
-Templates have access to a set of helper functions in ``ckan.lib.helpers``.
-When developing for ckan core, only use the helper functions found in
-``ckan.lib.helpers.__allowed_functions__``.
-
-.. todo:: Jinja2 templates
-
-Testing
--------
-
-- Functional tests which test the behaviour of the web user interface, and the
- APIs should be placed within ``ckan/tests/functional``. These tests can be a
- lot slower to run that unit tests which don't access the database or solr. So
- try to bear that in mind, and attempt to cover just what is neccessary, leaving
- what can be tested via unit-testing in unit-tests.
-
-- ``nose.tools.assert_in`` and ``nose.tools.assert_not_in`` are only available
- in Python>=2.7. So import them from ``ckan.tests``, which will provide
- alternatives if they're not available.
-
-- the `mock`_ library can be used to create and interrogate mock objects.
-
-See :doc:`test` for further information on testing in CKAN.
-
-.. _mock: http://pypi.python.org/pypi/mock
-
-Writing Extensions
-------------------
-
-Please see :doc:`writing-extensions` for information about writing ckan
-extensions, including details on the API available to extensions.
-
-Deprecation
------------
-
-- Anything that may be used by extensions (see :doc:`writing-extensions`) needs
- to maintain backward compatibility at call-site. ie - template helper
- functions and functions defined in the plugins toolkit.
-
-- The length of time of deprecation is evaluated on a function-by-function
- basis. At minimum, a function should be marked as deprecated during a point
- release.
-
-- To mark a helper function, use the ``deprecated`` decorator found in
- ``ckan.lib.maintain`` eg: ::
-
-
- @deprecated()
- def facet_items(*args, **kwargs):
- """
- DEPRECATED: Use the new facet data structure, and `unselected_facet_items()`
- """
- # rest of function definition.
-
-Javascript Coding Standards
-===========================
-
-Formatting
-----------
-
-.. _OKFN Coding Standards: http://wiki.okfn.org/Coding_Standards#Javascript
-.. _idiomatic.js: https://github.com/rwldrn/idiomatic.js/
-.. _Douglas Crockford's: http://javascript.crockford.com/code.html
-
-All JavaScript documents must use **two spaces** for indentation and files
-should have no trailing whitespace. This is contrary to the `OKFN Coding
-Standards`_ but matches what's in use in the current code base.
-
-Coding style must follow the `idiomatic.js`_ style but with the following
-exceptions.
-
-.. note:: Idiomatic is heavily based upon `Douglas Crockford's`_ style
- guide which is recommended by the `OKFN Coding Standards`_.
-
-White Space
-```````````
-
-Two spaces must be used for indentation at all times. Unlike in idiomatic
-whitespace must not be used _inside_ parentheses between the parentheses
-and their Contents. ::
-
- // BAD: Too much whitespace.
- function getUrl( full ) {
- var url = '/styleguide/javascript/';
- if ( full ) {
- url = 'http://okfn.github.com/ckan' + url;
- }
- return url;
- }
-
- // GOOD:
- function getUrl(full) {
- var url = '/styleguide/javascript/';
- if (full) {
- url = 'http://okfn.github.com/ckan' + url;
- }
- return url;
- }
-
-.. note:: See section 2.D.1.1 of idiomatic for more examples of this syntax.
-
-Quotes
-``````
-
-Single quotes should be used everywhere unless writing JSON or the string
-contains them. This makes it easier to create strings containing HTML. ::
-
- jQuery('').appendTo('body');
-
-Object properties need not be quoted unless required by the interpreter. ::
-
- var object = {
- name: 'bill',
- 'class': 'user-name'
- };
-
-Variable declarations
-`````````````````````
-
-One ``var`` statement must be used per variable assignment. These must be
-declared at the top of the function in which they are being used. ::
-
- // GOOD:
- var good = "string";
- var alsoGood = "another;
-
- // GOOD:
- var good = "string";
- var okay = [
- "hmm", "a bit", "better"
- ];
-
- // BAD:
- var good = "string",
- iffy = [
- "hmm", "not", "great"
- ];
-
-Declare variables at the top of the function in which they are first used. This
-avoids issues with variable hoisting. If a variable is not assigned a value
-until later in the function then it it okay to define more than one per
-statement. ::
-
- // BAD: contrived example.
- function lowercaseNames(names) {
- var names = [];
-
- for (var index = 0, length = names.length; index < length; index += 1) {
- var name = names[index];
- names.push(name.toLowerCase());
- }
-
- var sorted = names.sort();
- return sorted;
- }
-
- // GOOD:
- function lowercaseNames(names) {
- var names = [];
- var index, sorted, name;
-
- for (index = 0, length = names.length; index < length; index += 1) {
- name = names[index];
- names.push(names[index].toLowerCase());
- }
-
- sorted = names.sort();
- return sorted;
- }
-
-Naming
-------
-
-All properties, functions and methods must use lowercase camelCase: ::
-
- var myUsername = 'bill';
- var methods = {
- getSomething: function () {}
- };
-
-Constructor functions must use uppercase CamelCase: ::
-
- function DatasetSearchView() {
- }
-
-Constants must be uppercase with spaces delimited by underscores: ::
-
- var env = {
- PRODUCTION: 'production',
- DEVELOPMENT: 'development',
- TESTING: 'testing'
- };
-
-Event handlers and callback functions should be prefixed with "on": ::
-
- function onDownloadClick(event) {}
-
- jQuery('.download').click(onDownloadClick);
-
-Boolean variables or methods returning boolean functions should prefix
-the variable name with "is": ::
-
- function isAdmin() {}
-
- var canEdit = isUser() && isAdmin();
-
-
-.. note:: Alternatives are "has", "can" and "should" if they make more sense
-
-Private methods should be prefixed with an underscore: ::
-
- View.extend({
- "click": "_onClick",
- _onClick: function (event) {
- }
- });
-
-Functions should be declared as named functions rather than assigning an
-anonymous function to a variable. ::
-
- // GOOD:
- function getName() {
- }
-
- // BAD:
- var getName = function () {
- };
-
-Named functions are generally easier to debug as they appear named in the
-debugger.
-
-Comments
---------
-
-Comments should be used to explain anything that may be unclear when you return
-to it in six months time. Single line comments should be used for all inline
-comments that do not form part of the documentation. ::
-
- // Export the function to either the exports or global object depending
- // on the current environment. This can be either an AMD module, CommonJS
- // module or a browser.
- if (typeof module.define === 'function' && module.define.amd) {
- module.define('broadcast', function () {
- return Broadcast;
- });
- } else if (module.exports) {
- module.exports = Broadcast;
- } else {
- module.Broadcast = Broadcast;
- }
-
-JSHint
-------
-
-All JavaScript should pass `JSHint`_ before being committed. This can
-be installed using ``npm`` (which is bundled with `node`_) by running: ::
-
- $ npm -g install jshint
-
-Each project should include a jshint.json file with appropriate configuration
-options for the tool. Most text editors can also be configured to read from
-this file.
-
-.. _node: http://nodejs.org
-.. _jshint: http://www.jshint.com
-
-Documentation
--------------
-
-For documentation we use a simple markup format to document all methods. The
-documentation should provide enough information to show the reader what the
-method does, arguments it accepts and a general example of usage. Also
-for API's and third party libraries, providing links to external documentation
-is encouraged.
-
-The formatting is as follows::
-
- /* My method description. Should describe what the method does and where
- * it should be used.
- *
- * param1 - The method params, one per line (default: null)
- * param2 - A default can be provided in brackets at the end.
- *
- * Example
- *
- * // Indented two spaces. Should give a common example of use.
- * client.getTemplate('index.html', {limit: 1}, function (html) {
- * module.el.html(html);
- * });
- *
- * Returns describes what the object returns.
- */
-
-For example::
-
- /* Loads an HTML template from the CKAN snippet API endpoint. Template
- * variables can be passed through the API using the params object.
- *
- * Optional success and error callbacks can be provided or these can
- * be attached using the returns jQuery promise object.
- *
- * filename - The filename of the template to load.
- * params - An optional object containing key/value arguments to be
- * passed into the template.
- * success - An optional success callback to be called on load. This will
- * recieve the HTML string as the first argument.
- * error - An optional error callback to be called if the request fails.
- *
- * Example
- *
- * client.getTemplate('index.html', {limit: 1}, function (html) {
- * module.el.html(html);
- * });
- *
- * Returns a jqXHR promise object that can be used to attach callbacks.
- */
-
-Testing
--------
-
-For unit testing we use the following libraries.
-
-- `Mocha`_: As a BDD unit testing framework.
-- `Sinon`_: Provides spies, stubs and mocks for methods and functions.
-- `Chai`_: Provides common assertions.
-
-.. _Mocha: http://visionmedia.github.com/mocha/
-.. _Sinon: http://chaijs.com/
-.. _Chai: http://sinonjs.org/docs/
-
-Tests are run from the test/index.html directory. We use the BDD interface
-(``describe()``, ``it()`` etc.) provided by mocha and the assert interface
-provided by chai.
-
-Generally we try and have the core functionality of all libraries and modules
-unit tested.
-
-Best Practices
---------------
-
-Forms
-`````
-
-All forms should work without JavaScript enabled. This means that they must
-submit ``application/x-www-form-urlencoded`` data to the server and receive an appropriate
-response. The server should check for the ``X-Requested-With: XMLHTTPRequest``
-header to determine if the request is an ajax one. If so it can return an
-appropriate format, otherwise it should issue a 303 redirect.
-
-The one exception to this rule is if a form or button is injected with
-JavaScript after the page has loaded. It's then not part of the HTML document
-and can submit any data format it pleases.
-
-Ajax
-````````
-
-Ajax requests can be used to improve the experience of submitting forms and
-other actions that require server interactions. Nearly all requests will
-go through the following states.
-
-1. User clicks button.
-2. JavaScript intercepts the click and disables the button (add ``disabled``
- attr).
-3. A loading indicator is displayed (add class ``.loading`` to button).
-4. The request is made to the server.
-5. a) On success the interface is updated.
- b) On error a message is displayed to the user if there is no other way to
- resolve the issue.
-6. The loading indicator is removed.
-7. The button is re-enabled.
-
-Here's a possible example for submitting a search form using jQuery. ::
-
- jQuery('#search-form').submit(function (event) {
- var form = $(this);
- var button = form.find('[type=submit]');
-
- // Prevent the browser submitting the form.
- event.preventDefault();
-
- button.prop('disabled', true).addClass('loading');
-
- jQuery.ajax({
- type: this.method,
- data: form.serialize(),
- success: function (results) {
- updatePageWithResults(results);
- },
- error: function () {
- showSearchError('Sorry we were unable to complete this search');
- },
- complete: function () {
- button.prop('disabled', false).removeClass('loading');
- }
- });
- });
-
-This covers possible issues that might arise from submitting the form as well
-as providing the user with adequate feedback that the page is doing something.
-Disabling the button prevents the form being submitted twice and the error
-feedback should hopefully offer a solution for the error that occurred.
-
-Event Handlers
-``````````````
-
-When using event handlers to listen for browser events it's a common
-requirement to want to cancel the default browser action. This should be
-done by calling the ``event.preventDefault()`` method: ::
-
- jQuery('button').click(function (event) {
- event.preventDefault();
- });
-
-It is also possible to return ``false`` from the callback function. Avoid doing
-this as it also calls the ``event.stopPropagation()`` method which prevents the
-event from bubbling up the DOM tree. This prevents other handlers listening
-for the same event. For example an analytics click handler attached to the
-```` element.
-
-Also jQuery (1.7+) now provides the `.on()`_ and `.off()`_ methods as
-alternatives to ``.bind()``, ``.unbind()``, ``.delegate()`` and
-``.undelegate()`` and they should be preferred for all tasks.
-
-.. _.on(): http://api.jquery.com/on/
-.. _.off(): http://api.jquery.com/off/
-
-Templating
-``````````
-
-Small templates that will not require customisation by the instance can be
-placed inline. If you need to create multi-line templates use an array rather
-than escaping newlines within a string::
-
- var template = [
- '
',
- '',
- '
'
- ].join('');
-
-Always localise text strings within your templates. If you are including them
-inline this can always be done with jQuery::
-
- jQuery(template).find('span').text(_('This is my text string'));
-
-Larger templates can be loaded in using the CKAN snippet API. Modules get
-access to this functionality via the ``sandbox.client`` object::
-
- initialize: function () {
- var el = this.el;
- this.sandbox.client.getTemplate('dataset.html', function (html) {
- el.html(html);
- });
- }
-
-The primary benefits of this is that the localisation can be done by the server
-and it keeps the JavaScript modules free from large strings.
-
-HTML Coding Standards
-=====================
-
-Formatting
-----------
-
-All HTML documents must use **two spaces** for indentation and there should be
-no trailing whitespace. XHTML syntax must be used (this is more a Genshi
-requirement) and all attributes must use double quotes around attributes. ::
-
-
-
-
-HTML5 elements should be used where appropriate reserving ``
`` and ````
-elements for situations where there is no semantic value (such as wrapping
-elements to provide styling hooks).
-
-Doctype and layout
-------------------
-
-All documents must be using the HTML5 doctype and the ```` element should
-have a ``"lang"`` attribute. The ```` should also at a minimum include
-``"viewport"`` and ``"charset"`` meta tags. ::
-
-
-
-
-
-
- Example Site
-
-
-
-
-Forms
------
-
-Form fields must always include a ``