diff --git a/README.rst b/README.rst index eb72a05..c454a4d 100644 --- a/README.rst +++ b/README.rst @@ -2,12 +2,12 @@ CKAN Rating extension ============= -This is a simple rating extension for CKAN datasets (packages) and showcases. The extension adds a list of clickable stars to the side navigation +This is a simple rating extension for CKAN datasets (packages) and showcases. The extension adds a list of clickable stars to the side navigation in the dataset and showcacse templates similar to ckanext-qa. In showcase the stars are also displayed in the showcase listing, but are not clickable. The stars can also be added to any desired view by adding the following code to the desired template:: - {% snippet "rating/stars.html", package= %} + {% snippet "rating/snippets/stars.html", package= %} The amount of ratings submitted can also be displayed with:: @@ -20,7 +20,7 @@ Rating is identified with client IP if the user is not authenticated. User ID is Requirements ------------ -This extension works with CKAN version 2.5 or later. +This extension works with CKAN version 2.5 or later. ------------ @@ -51,6 +51,20 @@ To install ckanext-rating: 6. If you want to use this extension for ckanext-showcase, install it into your environment by following the instructions at https://github.com/ckan/ckanext-showcase +--------------- +Config Settings +--------------- + +Rating is enabled or disabled for unauthenticated users:: + + rating.enabled_for_unauthenticated_users = true or false + +Optional:: + + # List of dataset types for which the rating will be shown (defaults to ['dataset']) + ckanext.rating.enabled_dataset_types + + ------------------------ Development Installation ------------------------ @@ -62,13 +76,3 @@ do:: cd ckanext-rating python setup.py develop pip install -r dev-requirements.txt - - ---------------- -Config Settings ---------------- - -Optional:: - - # List of dataset types for which the rating will be shown (defaults to ['dataset']) - ckanext.rating.enabled_dataset_types diff --git a/ckanext/rating/controller.py b/ckanext/rating/controller.py index 25a4725..bd84135 100644 --- a/ckanext/rating/controller.py +++ b/ckanext/rating/controller.py @@ -2,45 +2,56 @@ import ckan.plugins as p import ckan.model as model import ckan.logic as logic from ckan.lib.base import h +from ckan.controllers.package import PackageController +from ckan.common import request c = p.toolkit.c flatten_to_string_key = logic.flatten_to_string_key + class RatingController(p.toolkit.BaseController): - def submit_package_rating(self, package, rating): - p.toolkit.get_action('rating_package_create')( - context = {'model': model, - 'user': c.user or c.author}, - data_dict={'package': package, - 'rating': rating} - ) - h.redirect_to(str('/dataset/' + package)) - return p.toolkit.render('package/read.html') + def submit_package_rating(self, package, rating): + context = {'model': model, 'user': c.user or c.author} + data_dict = {'package': package, 'rating': rating} + if p.toolkit.check_access('check_access_user', context, data_dict): + p.toolkit.get_action('rating_package_create')(context, data_dict) + h.redirect_to(str('/dataset/' + package)) + return p.toolkit.render('package/read.html') - def submit_showcase_rating(self, package, rating): - p.toolkit.get_action('rating_package_create')( - context = {'model': model, - 'user': c.user or c.author}, - data_dict={'package': package, - 'rating': rating} - ) - h.redirect_to(str('/showcase/' + package)) - return p.toolkit.render('showcase/showcase_info.html') + def submit_showcase_rating(self, package, rating): + context = {'model': model, 'user': c.user or c.author} + data_dict = {'package': package, 'rating': rating} + if p.toolkit.check_access('check_access_user', context, data_dict): + p.toolkit.get_action('rating_package_create')(context, data_dict) + h.redirect_to(str('/showcase/' + package)) + return p.toolkit.render('showcase/showcase_info.html') def submit_ajax_package_rating(self, package, rating): - try: - p.toolkit.get_action('rating_package_create')( - context = {'model': model, - 'user': c.user or c.author}, - data_dict={'package': package, - 'rating': rating} - ) - except Exception, ex: - errors = ex - else: - data['success'] = True + context = {'model': model, 'user': c.user or c.author} + data_dict = {'package': package, 'rating': rating} + if p.toolkit.check_access('check_access_user', context, data_dict): + try: + p.toolkit.get_action('rating_package_create')( + context, data_dict) + except Exception, ex: + errors = ex + else: + data['success'] = True - data = flatten_to_string_key({ 'data': data, 'errors': errors }), - response.headers['Content-Type'] = 'application/json;charset=utf-8' - return h.json.dumps(data) \ No newline at end of file + data = flatten_to_string_key({'data': data, 'errors': errors}), + response.headers['Content-Type'] = 'application/json;charset=utf-8' + return h.json.dumps(data) + + +class RatingPackageController(PackageController): + + def search(self): + cur_page = request.params.get('page') + if cur_page is not None: + c.current_page = self._get_page_number(request.params) + else: + c.current_page = 1 + c.pkg_type = 'dataset' + result = super(RatingPackageController, self).search() + return result diff --git a/ckanext/rating/helpers.py b/ckanext/rating/helpers.py index a858f07..17862e3 100644 --- a/ckanext/rating/helpers.py +++ b/ckanext/rating/helpers.py @@ -1,19 +1,19 @@ from ckanext.rating.model import Rating from ckan.plugins import toolkit from pylons import config -import ckan.model as model c = toolkit.c + def get_user_rating(package_id): - context = {'model': model, 'user': c.user} - - from ckan.model import User - if not isinstance(context.get('user'), User): + if not c.userobj: user = toolkit.request.environ.get('REMOTE_ADDR') - + else: + user = c.userobj user_rating = Rating.get_user_package_rating(user, package_id).first() return user_rating.rating if user_rating is not None else None + def show_rating_in_type(type): - return type in config.get('ckanext.rating.enabled_dataset_types', ['dataset']) \ No newline at end of file + return type in config.get('ckanext.rating.enabled_dataset_types', + ['dataset']) diff --git a/ckanext/rating/logic/action.py b/ckanext/rating/logic/action.py index 0f20b79..c35acc4 100644 --- a/ckanext/rating/logic/action.py +++ b/ckanext/rating/logic/action.py @@ -6,6 +6,7 @@ from ckan.plugins import toolkit log = logging.getLogger(__name__) + def rating_package_create(context, data_dict): '''Review a dataset (package). :param package: the name or id of the dataset to rate @@ -27,7 +28,7 @@ def rating_package_create(context, data_dict): error = None if not package_ref: error = _('You must supply a package id or name ' - '(parameter "package").') + '(parameter "package").') elif not rating: error = _('You must supply a rating (parameter "rating").') else: diff --git a/ckanext/rating/logic/auth/__init__.py b/ckanext/rating/logic/auth/__init__.py new file mode 100644 index 0000000..9297378 --- /dev/null +++ b/ckanext/rating/logic/auth/__init__.py @@ -0,0 +1,7 @@ +from .create import rating_create_auth + + +def get_rating_auth_dict(): + rating_auth = dict() + rating_auth.update(rating_create_auth()) + return rating_auth diff --git a/ckanext/rating/logic/auth/create.py b/ckanext/rating/logic/auth/create.py new file mode 100644 index 0000000..22f6d29 --- /dev/null +++ b/ckanext/rating/logic/auth/create.py @@ -0,0 +1,20 @@ +import pylons.config as config +from ckan.plugins import toolkit +import ckan.logic as logic +c = toolkit.c + + +def rating_create_auth(): + return { + 'check_access_user': check_access_user, + } + + +@logic.auth_allow_anonymous_access +def check_access_user(context, data_dict): + if c.user: + return {'success': True} + else: + allow_rating = toolkit.asbool( + config.get('rating.enabled_for_unauthenticated_users', True)) + return {'success': allow_rating} diff --git a/ckanext/rating/model.py b/ckanext/rating/model.py index 24eda29..3c22133 100644 --- a/ckanext/rating/model.py +++ b/ckanext/rating/model.py @@ -16,9 +16,11 @@ __all__ = ['MIN_RATING', 'MAX_RATING'] MIN_RATING = 1.0 MAX_RATING = 5.0 + def make_uuid(): return unicode(uuid.uuid4()) + class Rating(Base): __tablename__ = 'review' @@ -43,7 +45,7 @@ class Rating(Base): existing_rating = cls.get_user_package_rating(ip_or_user, package_id) if (existing_rating.first()): - existing_rating.update({ 'rating': rating }) + existing_rating.update({'rating': rating}) model.repo.commit() log.info('Review updated for package') else: @@ -55,10 +57,10 @@ class Rating(Base): ip_or_user = None review = Rating( - user_id = user_id, - rater_ip = ip_or_user, - package_id = package_id, - rating = rating + user_id=user_id, + rater_ip=ip_or_user, + package_id=package_id, + rating=rating ) model.Session.add(review) @@ -71,7 +73,8 @@ class Rating(Base): .filter(cls.package_id == package_id) \ .all() - average = sum(r.rating for r in ratings) / float(len(ratings)) if len(ratings) > 0 else 0 + average = sum(r.rating for r in ratings) / float(len(ratings)) if ( + len(ratings) > 0) else 0 return { 'rating': round(average, 2), 'ratings_count': len(ratings) @@ -87,11 +90,14 @@ class Rating(Base): user_id = ip_or_user.id ip_or_user = None - rating = model.Session.query(cls) \ - .filter(cls.package_id == package_id, cls.user_id == user_id, cls.rater_ip == ip_or_user) + rating = model.Session.query(cls).filter( + cls.package_id == package_id, + cls.user_id == user_id, + cls.rater_ip == ip_or_user) return rating + def init_tables(engine): Base.metadata.create_all(engine) log.info('Rating database tables are set-up') diff --git a/ckanext/rating/plugin.py b/ckanext/rating/plugin.py index 6f07067..cc208a9 100644 --- a/ckanext/rating/plugin.py +++ b/ckanext/rating/plugin.py @@ -1,12 +1,57 @@ import ckan.plugins as plugins import ckan.plugins.toolkit as toolkit +from ckan.common import request, c, g +import sqlalchemy +import ckan.model as model + from ckanext.rating.logic import action from ckanext.rating import helpers +import ckanext.rating.logic.auth as rating_auth +from ckanext.rating.model import Rating + + +def sort_by_rating(sort): + limit = g.datasets_per_page + if c.current_page: + page = c.current_page + else: + page = 1 + offset = (page - 1) * limit + c.count_pkg = model.Session.query( + sqlalchemy.func.count(model.Package.id)).\ + filter(model.Package.type == 'dataset').\ + filter(model.Package.private == False).\ + filter(model.Package.state == 'active').scalar() + query = model.Session.query( + model.Package.id, model.Package.title, + sqlalchemy.func.avg( + sqlalchemy.func.coalesce(Rating.rating, 0)). + label('rating_avg')).\ + outerjoin(Rating, Rating.package_id == model.Package.id).\ + filter(model.Package.type == 'dataset').\ + filter(model.Package.private == False).\ + filter(model.Package.state == 'active').\ + group_by(model.Package.id).\ + distinct() + if sort == 'rating desc': + query = query.order_by(sqlalchemy.desc('rating_avg')) + else: + query = query.order_by(sqlalchemy.asc('rating_avg')) + res = query.offset(offset).limit(limit) + c.qr = q = [id[0] for id in res] + tmp = 'id:(' + for id in q: + tmp += id + ' OR ' + q = tmp[:-4] + ')' + return q + class RatingPlugin(plugins.SingletonPlugin): plugins.implements(plugins.IConfigurer) plugins.implements(plugins.IActions) plugins.implements(plugins.ITemplateHelpers) + plugins.implements(plugins.IAuthFunctions) + plugins.implements(plugins.IPackageController, inherit=True) plugins.implements(plugins.IRoutes, inherit=True) # IConfigurer @@ -21,14 +66,14 @@ class RatingPlugin(plugins.SingletonPlugin): # IActions def get_actions(self): - return { + return { 'rating_package_create': action.rating_package_create, 'rating_package_get': action.rating_package_get, 'rating_showcase_create': action.rating_package_create, 'rating_showcase_get': action.rating_package_get } - ## ITemplateHelpers + # ITemplateHelpers def get_helpers(self): return { @@ -37,6 +82,32 @@ class RatingPlugin(plugins.SingletonPlugin): 'show_rating_in_type': helpers.show_rating_in_type } + # IAuthFunctions + + def get_auth_functions(self): + return rating_auth.get_rating_auth_dict() + + # IPackageController + + def before_search(self, search_params): + sort = request.params.get('sort', '') + if sort in ['rating desc', 'rating asc']: + search_params['q'] = sort_by_rating(sort) + search_params['start'] = 0 + return search_params + + def after_search(self, search_results, search_params): + sort = search_params.get('sort', '') + if sort in ['rating desc', 'rating asc']: + tmp = [] + for id in c.qr: + for pkg in search_results['results']: + if id == pkg['id']: + tmp.append(pkg) + search_results['results'] = tmp + search_results['count'] = c.count_pkg + return search_results + # IRoutes def before_map(self, map): @@ -48,4 +119,11 @@ class RatingPlugin(plugins.SingletonPlugin): controller='ckanext.rating.controller:RatingController', action='submit_showcase_rating') - return map \ No newline at end of file + map.connect( + '/dataset', + controller='ckanext.rating.controller:RatingPackageController', + action='search', + highlight_actions='index search' + ) + + return map diff --git a/ckanext/rating/public/css/rating.css b/ckanext/rating/public/css/rating.css index 3e54fb1..03a816d 100644 --- a/ckanext/rating/public/css/rating.css +++ b/ckanext/rating/public/css/rating.css @@ -22,4 +22,13 @@ a.rating-star-hover:hover { .rating-description { color: #7c7c82; font-size: 14px; -} \ No newline at end of file +} + +.dataset-raiting { + float: right; + display: inline-block; +} + +.dataset-raiting span { + font-size: 14px; +} diff --git a/ckanext/rating/templates/package/search.html b/ckanext/rating/templates/package/search.html new file mode 100644 index 0000000..27e761a --- /dev/null +++ b/ckanext/rating/templates/package/search.html @@ -0,0 +1,21 @@ +{% ckan_extends %} + +{% block form %} + {% set facets = { + 'fields': c.fields_grouped, + 'search': c.search_facets, + 'titles': c.facet_titles, + 'translated_fields': c.translated_fields, + 'remove_field': c.remove_field } + %} + {% set sorting = [ + (_('Relevance'), 'score desc, metadata_modified desc'), + (_('Name Ascending'), 'title_string asc'), + (_('Name Descending'), 'title_string desc'), + (_('Rating Ascending'), 'rating asc') if h.show_rating_in_type(c.pkg_type) else (false, false), + (_('Rating Descending'), 'rating desc') if h.show_rating_in_type(c.pkg_type) else (false, false), + (_('Last Modified'), 'metadata_modified desc'), + (_('Popular'), 'views_recent desc') if g.tracking_enabled else (false, false) ] + %} + {% snippet 'snippets/search_form.html', form_id='dataset-search-form', type='dataset', query=c.q, sorting=sorting, sorting_selected=c.sort_by_selected, count=c.page.item_count, facets=facets, show_empty=request.params, error=c.query_error, fields=c.fields %} +{% endblock %} diff --git a/ckanext/rating/templates/package/snippets/info.html b/ckanext/rating/templates/package/snippets/info.html new file mode 100644 index 0000000..f6dc7ff --- /dev/null +++ b/ckanext/rating/templates/package/snippets/info.html @@ -0,0 +1,16 @@ +{% ckan_extends %} + +{% block package_info %} + {% if pkg %} +
+
+
+ {% block package_info_inner %} + {{ super() }} + {% endblock %} + {% snippet "rating/snippets/rating.html", package=pkg %} +
+
+
+ {% endif %} +{% endblock %} diff --git a/ckanext/rating/templates/rating/snippets/rating.html b/ckanext/rating/templates/rating/snippets/rating.html index 9da9f54..e9cf621 100644 --- a/ckanext/rating/templates/rating/snippets/rating.html +++ b/ckanext/rating/templates/rating/snippets/rating.html @@ -1,27 +1,35 @@ {# -Renders a complete block of rating snippets with an option to rate the dataset +Renders a complete block of rating snippets package - The package for which the rating is displayed {% snippet "rating/snippets/rating.html", package=pkg %} #} +{% resource "rating_css/rating.css" %} {% if h.show_rating_in_type(package.type) %} -
- {% block general_rating %} -

{{ _('Rating') }}

-
- {% snippet "rating/snippets/stars_inactive.html", package=package %} -
- {% endblock %} - {% block user_rating %} -

{{ _('Your rating') }}

-
- {%- snippet "rating/snippets/stars.html", package=package -%}
- - {%- snippet "rating/snippets/rating_description.html", rating=h.get_user_rating(package.id) -%} - -
- {% endblock %} -
-{% endif %} \ No newline at end of file +
+ {% block general_rating %} +

{{ _('Rating') }}

+
+ {% snippet "rating/snippets/stars_inactive.html", package=package %} +
+ {% endblock %} + {% block user_rating %} + {% if h.check_access('check_access_user') %} +

{{ _('Your rating') }}

+
+ {%- snippet "rating/snippets/stars.html", package=package -%} + {% block user_rating_br %}
{% endblock %} + + {%- snippet "rating/snippets/rating_description.html", rating=h.get_user_rating(package.id) -%} + +
+ {% else %} + + {% endif %} + {% endblock %} +
+{% endif %} diff --git a/ckanext/rating/templates/rating/snippets/stars_inactive.html b/ckanext/rating/templates/rating/snippets/stars_inactive.html index 8c2473c..db82591 100644 --- a/ckanext/rating/templates/rating/snippets/stars_inactive.html +++ b/ckanext/rating/templates/rating/snippets/stars_inactive.html @@ -1,5 +1,5 @@ {# -Renders a set of stars which are not clickable +Renders a set of stars stars - The number of stars to be displayed. @@ -9,18 +9,32 @@ stars - The number of stars to be displayed. {% set ratings_count = h.package_rating(None, {'package_id' : package.id} ).ratings_count%} {% set stars = h.package_rating(None, {'package_id' : package.id} ).rating %} - - - {%- for index in range(stars|int) -%} - - {%- endfor -%} - {%- for index in range(stars|int, 5) -%} - - {%- endfor -%} - - -
- - {{ ratings_count }} {{ _('rating') if ratings_count == 1 else _('ratings') }} - +{% if stars|int < stars %} + {% set half_star = 1 %} +{% else %} + {% set half_star = 0 %} +{% endif %} +{% block main_star_rating %} + + + {%- for index in range(stars|int) -%} + + {%- endfor -%} + {%- if half_star == 1 -%} + + {%- endif -%} + {%- for index in range(stars|int + half_star, 5) -%} + + {%- endfor -%} + + +{% endblock %} + +{% block main_star_rating_br %} +
+{% endblock %} + +{% block star_rating_description %} + {{ ratings_count }} {{ _('rating') if ratings_count == 1 else _('ratings') }} +{% endblock %} diff --git a/ckanext/rating/templates/snippets/package_item.html b/ckanext/rating/templates/snippets/package_item.html new file mode 100644 index 0000000..617cd15 --- /dev/null +++ b/ckanext/rating/templates/snippets/package_item.html @@ -0,0 +1,14 @@ +{% resource "rating_css/rating.css" %} + +{% ckan_extends %} + +{% block heading_title %} + {{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }} + {% set rating = h.package_rating(None, {'package_id' : package.id} ).rating|float %} + {% if rating != 0 %} +
+ {{rating|round(1)}} + +
+ {% endif%} +{% endblock %}