Merge pull request #5 from wildcatzita/master

Added more features. Fixed bug with rating shown
This commit is contained in:
Jari Voutilainen 2017-02-13 10:45:21 +02:00 committed by GitHub
commit 6a94f97837
14 changed files with 308 additions and 99 deletions

View File

@ -2,12 +2,12 @@
CKAN Rating extension 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. 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:: The stars can also be added to any desired view by adding the following code to the desired template::
{% snippet "rating/stars.html", package=<YOUR_PACKAGE> %} {% snippet "rating/snippets/stars.html", package=<YOUR_PACKAGE> %}
The amount of ratings submitted can also be displayed with:: 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 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 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 Development Installation
------------------------ ------------------------
@ -62,13 +76,3 @@ do::
cd ckanext-rating cd ckanext-rating
python setup.py develop python setup.py develop
pip install -r dev-requirements.txt 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

View File

@ -2,45 +2,56 @@ import ckan.plugins as p
import ckan.model as model import ckan.model as model
import ckan.logic as logic import ckan.logic as logic
from ckan.lib.base import h from ckan.lib.base import h
from ckan.controllers.package import PackageController
from ckan.common import request
c = p.toolkit.c c = p.toolkit.c
flatten_to_string_key = logic.flatten_to_string_key flatten_to_string_key = logic.flatten_to_string_key
class RatingController(p.toolkit.BaseController): class RatingController(p.toolkit.BaseController):
def submit_package_rating(self, package, rating): def submit_package_rating(self, package, rating):
p.toolkit.get_action('rating_package_create')( context = {'model': model, 'user': c.user or c.author}
context = {'model': model, data_dict = {'package': package, 'rating': rating}
'user': c.user or c.author}, if p.toolkit.check_access('check_access_user', context, data_dict):
data_dict={'package': package, p.toolkit.get_action('rating_package_create')(context, data_dict)
'rating': rating} h.redirect_to(str('/dataset/' + package))
) return p.toolkit.render('package/read.html')
h.redirect_to(str('/dataset/' + package))
return p.toolkit.render('package/read.html')
def submit_showcase_rating(self, package, rating): def submit_showcase_rating(self, package, rating):
p.toolkit.get_action('rating_package_create')( context = {'model': model, 'user': c.user or c.author}
context = {'model': model, data_dict = {'package': package, 'rating': rating}
'user': c.user or c.author}, if p.toolkit.check_access('check_access_user', context, data_dict):
data_dict={'package': package, p.toolkit.get_action('rating_package_create')(context, data_dict)
'rating': rating} h.redirect_to(str('/showcase/' + package))
) return p.toolkit.render('showcase/showcase_info.html')
h.redirect_to(str('/showcase/' + package))
return p.toolkit.render('showcase/showcase_info.html')
def submit_ajax_package_rating(self, package, rating): def submit_ajax_package_rating(self, package, rating):
try: context = {'model': model, 'user': c.user or c.author}
p.toolkit.get_action('rating_package_create')( data_dict = {'package': package, 'rating': rating}
context = {'model': model, if p.toolkit.check_access('check_access_user', context, data_dict):
'user': c.user or c.author}, try:
data_dict={'package': package, p.toolkit.get_action('rating_package_create')(
'rating': rating} context, data_dict)
) except Exception, ex:
except Exception, ex: errors = ex
errors = ex else:
else: data['success'] = True
data['success'] = True
data = flatten_to_string_key({ 'data': data, 'errors': errors }), data = flatten_to_string_key({'data': data, 'errors': errors}),
response.headers['Content-Type'] = 'application/json;charset=utf-8' response.headers['Content-Type'] = 'application/json;charset=utf-8'
return h.json.dumps(data) 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

View File

@ -1,19 +1,19 @@
from ckanext.rating.model import Rating from ckanext.rating.model import Rating
from ckan.plugins import toolkit from ckan.plugins import toolkit
from pylons import config from pylons import config
import ckan.model as model
c = toolkit.c c = toolkit.c
def get_user_rating(package_id): def get_user_rating(package_id):
context = {'model': model, 'user': c.user} if not c.userobj:
from ckan.model import User
if not isinstance(context.get('user'), User):
user = toolkit.request.environ.get('REMOTE_ADDR') user = toolkit.request.environ.get('REMOTE_ADDR')
else:
user = c.userobj
user_rating = Rating.get_user_package_rating(user, package_id).first() user_rating = Rating.get_user_package_rating(user, package_id).first()
return user_rating.rating if user_rating is not None else None return user_rating.rating if user_rating is not None else None
def show_rating_in_type(type): def show_rating_in_type(type):
return type in config.get('ckanext.rating.enabled_dataset_types', ['dataset']) return type in config.get('ckanext.rating.enabled_dataset_types',
['dataset'])

View File

@ -6,6 +6,7 @@ from ckan.plugins import toolkit
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def rating_package_create(context, data_dict): def rating_package_create(context, data_dict):
'''Review a dataset (package). '''Review a dataset (package).
:param package: the name or id of the dataset to rate :param package: the name or id of the dataset to rate
@ -27,7 +28,7 @@ def rating_package_create(context, data_dict):
error = None error = None
if not package_ref: if not package_ref:
error = _('You must supply a package id or name ' error = _('You must supply a package id or name '
'(parameter "package").') '(parameter "package").')
elif not rating: elif not rating:
error = _('You must supply a rating (parameter "rating").') error = _('You must supply a rating (parameter "rating").')
else: else:

View File

@ -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

View File

@ -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}

View File

@ -16,9 +16,11 @@ __all__ = ['MIN_RATING', 'MAX_RATING']
MIN_RATING = 1.0 MIN_RATING = 1.0
MAX_RATING = 5.0 MAX_RATING = 5.0
def make_uuid(): def make_uuid():
return unicode(uuid.uuid4()) return unicode(uuid.uuid4())
class Rating(Base): class Rating(Base):
__tablename__ = 'review' __tablename__ = 'review'
@ -43,7 +45,7 @@ class Rating(Base):
existing_rating = cls.get_user_package_rating(ip_or_user, package_id) existing_rating = cls.get_user_package_rating(ip_or_user, package_id)
if (existing_rating.first()): if (existing_rating.first()):
existing_rating.update({ 'rating': rating }) existing_rating.update({'rating': rating})
model.repo.commit() model.repo.commit()
log.info('Review updated for package') log.info('Review updated for package')
else: else:
@ -55,10 +57,10 @@ class Rating(Base):
ip_or_user = None ip_or_user = None
review = Rating( review = Rating(
user_id = user_id, user_id=user_id,
rater_ip = ip_or_user, rater_ip=ip_or_user,
package_id = package_id, package_id=package_id,
rating = rating rating=rating
) )
model.Session.add(review) model.Session.add(review)
@ -71,7 +73,8 @@ class Rating(Base):
.filter(cls.package_id == package_id) \ .filter(cls.package_id == package_id) \
.all() .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 { return {
'rating': round(average, 2), 'rating': round(average, 2),
'ratings_count': len(ratings) 'ratings_count': len(ratings)
@ -87,11 +90,14 @@ class Rating(Base):
user_id = ip_or_user.id user_id = ip_or_user.id
ip_or_user = None ip_or_user = None
rating = model.Session.query(cls) \ rating = model.Session.query(cls).filter(
.filter(cls.package_id == package_id, cls.user_id == user_id, cls.rater_ip == ip_or_user) cls.package_id == package_id,
cls.user_id == user_id,
cls.rater_ip == ip_or_user)
return rating return rating
def init_tables(engine): def init_tables(engine):
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
log.info('Rating database tables are set-up') log.info('Rating database tables are set-up')

View File

@ -1,12 +1,57 @@
import ckan.plugins as plugins import ckan.plugins as plugins
import ckan.plugins.toolkit as toolkit 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.logic import action
from ckanext.rating import helpers 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): class RatingPlugin(plugins.SingletonPlugin):
plugins.implements(plugins.IConfigurer) plugins.implements(plugins.IConfigurer)
plugins.implements(plugins.IActions) plugins.implements(plugins.IActions)
plugins.implements(plugins.ITemplateHelpers) plugins.implements(plugins.ITemplateHelpers)
plugins.implements(plugins.IAuthFunctions)
plugins.implements(plugins.IPackageController, inherit=True)
plugins.implements(plugins.IRoutes, inherit=True) plugins.implements(plugins.IRoutes, inherit=True)
# IConfigurer # IConfigurer
@ -21,14 +66,14 @@ class RatingPlugin(plugins.SingletonPlugin):
# IActions # IActions
def get_actions(self): def get_actions(self):
return { return {
'rating_package_create': action.rating_package_create, 'rating_package_create': action.rating_package_create,
'rating_package_get': action.rating_package_get, 'rating_package_get': action.rating_package_get,
'rating_showcase_create': action.rating_package_create, 'rating_showcase_create': action.rating_package_create,
'rating_showcase_get': action.rating_package_get 'rating_showcase_get': action.rating_package_get
} }
## ITemplateHelpers # ITemplateHelpers
def get_helpers(self): def get_helpers(self):
return { return {
@ -37,6 +82,32 @@ class RatingPlugin(plugins.SingletonPlugin):
'show_rating_in_type': helpers.show_rating_in_type '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 # IRoutes
def before_map(self, map): def before_map(self, map):
@ -48,4 +119,11 @@ class RatingPlugin(plugins.SingletonPlugin):
controller='ckanext.rating.controller:RatingController', controller='ckanext.rating.controller:RatingController',
action='submit_showcase_rating') action='submit_showcase_rating')
return map map.connect(
'/dataset',
controller='ckanext.rating.controller:RatingPackageController',
action='search',
highlight_actions='index search'
)
return map

View File

@ -22,4 +22,13 @@ a.rating-star-hover:hover {
.rating-description { .rating-description {
color: #7c7c82; color: #7c7c82;
font-size: 14px; font-size: 14px;
} }
.dataset-raiting {
float: right;
display: inline-block;
}
.dataset-raiting span {
font-size: 14px;
}

View File

@ -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 %}

View File

@ -0,0 +1,16 @@
{% ckan_extends %}
{% block package_info %}
{% if pkg %}
<section class="module module-narrow">
<div class="module context-info">
<div class="module-content">
{% block package_info_inner %}
{{ super() }}
{% endblock %}
{% snippet "rating/snippets/rating.html", package=pkg %}
</div>
</div>
</section>
{% endif %}
{% endblock %}

View File

@ -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 package - The package for which the rating is displayed
{% snippet "rating/snippets/rating.html", package=pkg %} {% snippet "rating/snippets/rating.html", package=pkg %}
#} #}
{% resource "rating_css/rating.css" %}
{% if h.show_rating_in_type(package.type) %} {% if h.show_rating_in_type(package.type) %}
<div class="rating"> <div class="rating">
{% block general_rating %} {% block general_rating %}
<h2 class="heading">{{ _('Rating') }}</h2> <h2 class="heading">{{ _('Rating') }}</h2>
<div class="rating-container"> <div class="rating-container">
{% snippet "rating/snippets/stars_inactive.html", package=package %} {% snippet "rating/snippets/stars_inactive.html", package=package %}
</div> </div>
{% endblock %} {% endblock %}
{% block user_rating %} {% block user_rating %}
<h2 class="heading">{{ _('Your rating') }}</h2> {% if h.check_access('check_access_user') %}
<div class="rating-container"> <h2 class="heading">{{ _('Your rating') }}</h2>
{%- snippet "rating/snippets/stars.html", package=package -%}<br> <div class="rating-container">
<span class="rating-details"> {%- snippet "rating/snippets/stars.html", package=package -%}
{%- snippet "rating/snippets/rating_description.html", rating=h.get_user_rating(package.id) -%} {% block user_rating_br %}<br>{% endblock %}
</span> <span class="rating-details">
</div> {%- snippet "rating/snippets/rating_description.html", rating=h.get_user_rating(package.id) -%}
{% endblock %} </span>
</div> </div>
{% endif %} {% else %}
<div class="login-rating-details">
<a href="{{ h.url_for('login') }}">{{ _('Login') }}</a> {{ _('to leave a rating') }}
</div>
{% endif %}
{% endblock %}
</div>
{% endif %}

View File

@ -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. 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 ratings_count = h.package_rating(None, {'package_id' : package.id} ).ratings_count%}
{% set stars = h.package_rating(None, {'package_id' : package.id} ).rating %} {% set stars = h.package_rating(None, {'package_id' : package.id} ).rating %}
<span class="star-rating{% if stars == 0 %} no-stars{% endif %}"> {% if stars|int < stars %}
<span class="star-rating-stars"> {% set half_star = 1 %}
{%- for index in range(stars|int) -%} {% else %}
<span class="icon icon-star rating-star"></span> {% set half_star = 0 %}
{%- endfor -%} {% endif %}
{%- for index in range(stars|int, 5) -%}
<span class="icon icon-star-empty rating-star"></span>
{%- endfor -%}
</span>
</span>
<br>
<span class="rating-description">
{{ ratings_count }} {{ _('rating') if ratings_count == 1 else _('ratings') }}
</span>
{% block main_star_rating %}
<span class="star-rating{% if stars == 0 %} no-stars{% endif %}">
<span class="star-rating-stars">
{%- for index in range(stars|int) -%}
<span class="icon icon-star"></span>
{%- endfor -%}
{%- if half_star == 1 -%}
<span class="icon icon-star-half-empty"></span>
{%- endif -%}
{%- for index in range(stars|int + half_star, 5) -%}
<span class="icon icon-star-empty"></span>
{%- endfor -%}
</span>
</span>
{% endblock %}
{% block main_star_rating_br %}
<br>
{% endblock %}
{% block star_rating_description %}
<span class="rating-description">{{ ratings_count }} {{ _('rating') if ratings_count == 1 else _('ratings') }}</span>
{% endblock %}

View File

@ -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 %}
<div class="dataset-raiting">
<span>{{rating|round(1)}}</span>
<i class="user-rating-star icon icon-star"></i>
</div>
{% endif%}
{% endblock %}