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
=============
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=<YOUR_PACKAGE> %}
{% snippet "rating/snippets/stars.html", package=<YOUR_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

View File

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

View File

@ -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'])
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__)
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:

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
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')

View File

@ -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
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 {
color: #7c7c82;
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
{% snippet "rating/snippets/rating.html", package=pkg %}
#}
{% resource "rating_css/rating.css" %}
{% if h.show_rating_in_type(package.type) %}
<div class="rating">
{% block general_rating %}
<h2 class="heading">{{ _('Rating') }}</h2>
<div class="rating-container">
{% snippet "rating/snippets/stars_inactive.html", package=package %}
</div>
{% endblock %}
{% block user_rating %}
<h2 class="heading">{{ _('Your rating') }}</h2>
<div class="rating-container">
{%- snippet "rating/snippets/stars.html", package=package -%}<br>
<span class="rating-details">
{%- snippet "rating/snippets/rating_description.html", rating=h.get_user_rating(package.id) -%}
</span>
</div>
{% endblock %}
</div>
{% endif %}
<div class="rating">
{% block general_rating %}
<h2 class="heading">{{ _('Rating') }}</h2>
<div class="rating-container">
{% snippet "rating/snippets/stars_inactive.html", package=package %}
</div>
{% endblock %}
{% block user_rating %}
{% if h.check_access('check_access_user') %}
<h2 class="heading">{{ _('Your rating') }}</h2>
<div class="rating-container">
{%- snippet "rating/snippets/stars.html", package=package -%}
{% block user_rating_br %}<br>{% endblock %}
<span class="rating-details">
{%- snippet "rating/snippets/rating_description.html", rating=h.get_user_rating(package.id) -%}
</span>
</div>
{% 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.
@ -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 %}
<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 rating-star"></span>
{%- endfor -%}
{%- 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>
{% if stars|int < stars %}
{% set half_star = 1 %}
{% else %}
{% set half_star = 0 %}
{% endif %}
{% 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 %}