Merge pull request #5 from wildcatzita/master
Added more features. Fixed bug with rating shown
This commit is contained in:
commit
6a94f97837
30
README.rst
30
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=<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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
|
@ -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}
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
Loading…
Reference in New Issue