diff --git a/.gitignore b/.gitignore index bd799524..3264fb95 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,5 @@ coverage.xml # Sphinx documentation docs/_build/ + +pgadmin4-env/ \ No newline at end of file diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 00000000..b70f6a1e --- /dev/null +++ b/alembic.ini @@ -0,0 +1,105 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = postgresql://ckan_default:123698745@localhost/ckan_default + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/README b/alembic/README new file mode 100644 index 00000000..98e4f9c4 --- /dev/null +++ b/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 00000000..b783e0a2 --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,79 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context +from ckan.model.meta import metadata + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 00000000..55df2863 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/ckanext/showcase/assets/js/showcase-ckeditor.js b/ckanext/showcase/assets/js/showcase-ckeditor.js index dbeda388..e4c94b48 100644 --- a/ckanext/showcase/assets/js/showcase-ckeditor.js +++ b/ckanext/showcase/assets/js/showcase-ckeditor.js @@ -15,6 +15,8 @@ ckan.module('showcase-ckeditor', function ($) { }, _onReady: function(){ + var editorId = this.el.data('editor'); + var lang = this.el.data('lang'); var config = {}; config.toolbar = [ 'heading', @@ -39,7 +41,7 @@ ckan.module('showcase-ckeditor', function ($) { styles: ['alignLeft', 'full', 'alignRight'] } - config.language = 'en' + config.language = lang var csrf_field = $('meta[name=csrf_field_name]').attr('content'); var csrf_token = $('meta[name='+ csrf_field +']').attr('content'); @@ -48,14 +50,16 @@ ckan.module('showcase-ckeditor', function ($) { headers: {'X-CSRFToken': csrf_token} } - ClassicEditor - .create( - document.querySelector('#editor'), - config - ) - .catch( error => { - console.error( error.stack ); - } ); + if (editorId) { + ClassicEditor + .create( + document.querySelector('#'+ editorId), + config + ) + .catch( error => { + console.error( error.stack ); + } ); + } } diff --git a/ckanext/showcase/assets/webassets.yml b/ckanext/showcase/assets/webassets.yml index 796470ab..20da6fb8 100644 --- a/ckanext/showcase/assets/webassets.yml +++ b/ckanext/showcase/assets/webassets.yml @@ -19,3 +19,21 @@ ckeditor-content-css: - ckeditor-content-style.css output: showcase/%(version)s_ckeditor-content-style.css filter: cssrewrite + +showcase-actions: + filter: rjsmin + output: ckanext-showcase/%(version)s-showcase-actions.js + contents: + - js/showcase-actions.js + extra: + preload: + - base/main + +showcase-conditional-fields: + filter: rjsmin + output: ckanext-showcase/%(version)s-showcase-conditional-fields.js + contents: + - js/showcase-conditional-fields.js + extra: + preload: + - base/main \ No newline at end of file diff --git a/ckanext/showcase/data/constants.py b/ckanext/showcase/data/constants.py new file mode 100644 index 00000000..f8da4829 --- /dev/null +++ b/ckanext/showcase/data/constants.py @@ -0,0 +1,33 @@ +from enum import Enum +from ckan.plugins.toolkit import _ + +class ApprovalStatus(Enum): + PENDING = 'a' + NEEDS_REVISION = 'b' + REJECTED = 'c' + APPROVED = 'd' + + +SHOWCASE_STATUS_OPTIONS = { + 'a' : _(u"Pending"), + 'b': _(u"Needs Revision"), + 'c': _(u"Rejected"), + 'd': _(u"Approved"), +} + + +class ReuseCaseType(Enum): + MOBILE_APPLICATION = 'a' + WEB_APPLICATION = 'b' + ARCTICLE = 'c' + SCIENTIFIC_RESEARCH = 'd' + DEVELOPMENT_API = 'e' + + +REUSE_CASE_TYPE_OPTIONS = { + 'a': _(u"Mobile Application"), + 'b': _(u"Web Application"), + 'c': _(u"Writing a newspaper article, blog article, or research paper"), + 'd': _(u"Conducting scientific research or analysis"), + 'e':_(u"Development API"), +} diff --git a/ckanext/showcase/logic/action/__init__.py b/ckanext/showcase/logic/action/__init__.py index 43ab2d14..600d7fec 100644 --- a/ckanext/showcase/logic/action/__init__.py +++ b/ckanext/showcase/logic/action/__init__.py @@ -1,36 +1,32 @@ -import ckanext.showcase.logic.action.create -import ckanext.showcase.logic.action.delete -import ckanext.showcase.logic.action.update -import ckanext.showcase.logic.action.get +import ckanext.showcase.logic.action.create as CREATE +import ckanext.showcase.logic.action.delete as DELETE +import ckanext.showcase.logic.action.update as UPDATE +import ckanext.showcase.logic.action.get as GET def get_actions(): action_functions = { - 'ckanext_showcase_create': - ckanext.showcase.logic.action.create.showcase_create, - 'ckanext_showcase_update': - ckanext.showcase.logic.action.update.showcase_update, - 'ckanext_showcase_delete': - ckanext.showcase.logic.action.delete.showcase_delete, - 'ckanext_showcase_show': - ckanext.showcase.logic.action.get.showcase_show, - 'ckanext_showcase_list': - ckanext.showcase.logic.action.get.showcase_list, - 'ckanext_showcase_package_association_create': - ckanext.showcase.logic.action.create.showcase_package_association_create, - 'ckanext_showcase_package_association_delete': - ckanext.showcase.logic.action.delete.showcase_package_association_delete, - 'ckanext_showcase_package_list': - ckanext.showcase.logic.action.get.showcase_package_list, - 'ckanext_package_showcase_list': - ckanext.showcase.logic.action.get.package_showcase_list, - 'ckanext_showcase_admin_add': - ckanext.showcase.logic.action.create.showcase_admin_add, - 'ckanext_showcase_admin_remove': - ckanext.showcase.logic.action.delete.showcase_admin_remove, - 'ckanext_showcase_admin_list': - ckanext.showcase.logic.action.get.showcase_admin_list, - 'ckanext_showcase_upload': - ckanext.showcase.logic.action.create.showcase_upload, + # CREATE + 'ckanext_showcase_create': CREATE.showcase_create, + 'ckanext_showcase_package_association_create': CREATE.showcase_package_association_create, + 'ckanext_showcase_upload': CREATE.showcase_upload, + + # UPDATE + 'ckanext_showcase_update': UPDATE.showcase_update, + 'ckanext_showcase_status_update': UPDATE.status_update, + + # GET + 'ckanext_showcase_show': GET.showcase_show, + 'ckanext_showcase_list': GET.showcase_list, + 'ckanext_showcase_search': GET.showcase_filtered, + 'ckanext_showcase_package_list': GET.showcase_package_list, + 'ckanext_package_showcase_list': GET.package_showcase_list, + 'ckanext_showcase_statics': GET.showcase_statics, + 'ckanext_showcase_status_show': GET.status_show, + + # DELETE + 'ckanext_showcase_package_association_delete': DELETE.showcase_package_association_delete, + 'ckanext_showcase_delete': DELETE.showcase_delete, } + return action_functions diff --git a/ckanext/showcase/logic/action/action_decorator.py b/ckanext/showcase/logic/action/action_decorator.py new file mode 100644 index 00000000..6a78f068 --- /dev/null +++ b/ckanext/showcase/logic/action/action_decorator.py @@ -0,0 +1,26 @@ +import functools + +def notify_after_action(notification_func): + """ + A decorator that calls a notification function after the main action completes. + + :param notification_func: The notification function to call after the action. + """ + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + # Execute the original function and capture the result + result = func(*args, **kwargs) + + # Extract the request id (assuming it's in the result) + showcase_id = result.get('id') if result else None + + # Call the notification function if a valid request id is found + if showcase_id: + notification_func(showcase_id) + + # Return the original result + return result + + return wrapper + return decorator \ No newline at end of file diff --git a/ckanext/showcase/logic/action/create.py b/ckanext/showcase/logic/action/create.py index 0b4dbc68..c747fa30 100644 --- a/ckanext/showcase/logic/action/create.py +++ b/ckanext/showcase/logic/action/create.py @@ -2,26 +2,28 @@ import ckan.lib.uploader as uploader import ckan.lib.helpers as h -import ckan.plugins.toolkit as toolkit -from ckan.logic.converters import convert_user_name_or_id_to_id +import ckan.plugins.toolkit as tk from ckan.lib.navl.dictization_functions import validate +from ckanext.showcase.logic import notifiy +from ckanext.showcase.logic.action.action_decorator import notify_after_action import ckanext.showcase.logic.converters as showcase_converters import ckanext.showcase.logic.schema as showcase_schema -from ckanext.showcase.model import ShowcasePackageAssociation, ShowcaseAdmin +from ckanext.showcase.model import ShowcasePackageAssociation convert_package_name_or_id_to_title_or_name = \ showcase_converters.convert_package_name_or_id_to_title_or_name showcase_package_association_create_schema = \ showcase_schema.showcase_package_association_create_schema -showcase_admin_add_schema = showcase_schema.showcase_admin_add_schema log = logging.getLogger(__name__) +@notify_after_action(notifiy.showcase_create) def showcase_create(context, data_dict): '''Upload the image and continue with package creation.''' + tk.check_access('ckanext_showcase_create',context, data_dict) # force type to 'showcase' data_dict['type'] = 'showcase' upload = uploader.get_uploader('showcase') @@ -31,7 +33,18 @@ def showcase_create(context, data_dict): upload.upload(uploader.get_max_image_size()) - pkg = toolkit.get_action('package_create')(context, data_dict) + + site_user = tk.get_action("get_site_user")({"ignore_auth": True}, {}) + updated_context = {'ignore_auth': True, 'user':site_user['name']} + pkg = tk.get_action('package_create')( + context.copy().update(updated_context), + data_dict + ) + + tk.get_action('ckanext_showcase_status_update')( + context.copy().update(updated_context), + {"showcase_id": pkg.get("id",pkg.get("name", '')) } + ) return pkg @@ -46,7 +59,7 @@ def showcase_package_association_create(context, data_dict): :type package_id: string ''' - toolkit.check_access('ckanext_showcase_package_association_create', + tk.check_access('ckanext_showcase_package_association_create', context, data_dict) # validate the incoming data_dict @@ -54,57 +67,26 @@ def showcase_package_association_create(context, data_dict): data_dict, showcase_package_association_create_schema(), context) if errors: - raise toolkit.ValidationError(errors) + raise tk.ValidationError(errors) - package_id, showcase_id = toolkit.get_or_bust(validated_data_dict, + package_id, showcase_id = tk.get_or_bust(validated_data_dict, ['package_id', 'showcase_id']) if ShowcasePackageAssociation.exists(package_id=package_id, showcase_id=showcase_id): - raise toolkit.ValidationError("ShowcasePackageAssociation with package_id '{0}' and showcase_id '{1}' already exists.".format(package_id, showcase_id), + raise tk.ValidationError("ShowcasePackageAssociation with package_id '{0}' and showcase_id '{1}' already exists.".format(package_id, showcase_id), error_summary=u"The dataset, {0}, is already in the showcase".format(convert_package_name_or_id_to_title_or_name(package_id, context))) # create the association return ShowcasePackageAssociation.create(package_id=package_id, showcase_id=showcase_id) - -def showcase_admin_add(context, data_dict): - '''Add a user to the list of showcase admins. - - :param username: name of the user to add to showcase user admin list - :type username: string - ''' - - toolkit.check_access('ckanext_showcase_admin_add', context, data_dict) - - # validate the incoming data_dict - validated_data_dict, errors = validate( - data_dict, showcase_admin_add_schema(), context) - - username = toolkit.get_or_bust(validated_data_dict, 'username') - try: - user_id = convert_user_name_or_id_to_id(username, context) - except toolkit.Invalid: - raise toolkit.ObjectNotFound - - if errors: - raise toolkit.ValidationError(errors) - - if ShowcaseAdmin.exists(user_id=user_id): - raise toolkit.ValidationError("ShowcaseAdmin with user_id '{0}' already exists.".format(user_id), - error_summary=u"User '{0}' is already a Showcase Admin.".format(username)) - - # create showcase admin entry - return ShowcaseAdmin.create(user_id=user_id) - - def showcase_upload(context, data_dict): ''' Uploads images to be used in showcase content. ''' - toolkit.check_access('ckanext_showcase_upload', context, data_dict) + tk.check_access('ckanext_showcase_upload', context, data_dict) upload = uploader.get_uploader('showcase_image') diff --git a/ckanext/showcase/logic/action/delete.py b/ckanext/showcase/logic/action/delete.py index 8bcf89ab..a78d7056 100644 --- a/ckanext/showcase/logic/action/delete.py +++ b/ckanext/showcase/logic/action/delete.py @@ -1,12 +1,11 @@ import logging -import ckan.plugins.toolkit as toolkit -from ckan.logic.converters import convert_user_name_or_id_to_id +import ckan.plugins.toolkit as tk import ckan.lib.navl.dictization_functions from ckanext.showcase.logic.schema import ( showcase_package_association_delete_schema, - showcase_admin_remove_schema) + ) from ckanext.showcase.model import ShowcasePackageAssociation, ShowcaseAdmin @@ -24,14 +23,14 @@ def showcase_delete(context, data_dict): ''' model = context['model'] - id = toolkit.get_or_bust(data_dict, 'id') + id = tk.get_or_bust(data_dict, 'id') entity = model.Package.get(id) if entity is None: - raise toolkit.ObjectNotFound + raise tk.ObjectNotFound - toolkit.check_access('ckanext_showcase_delete', context, data_dict) + tk.check_access('ckanext_showcase_delete', context, data_dict) entity.purge() model.repo.commit() @@ -49,7 +48,7 @@ def showcase_package_association_delete(context, data_dict): model = context['model'] - toolkit.check_access('ckanext_showcase_package_association_delete', + tk.check_access('ckanext_showcase_package_association_delete', context, data_dict) # validate the incoming data_dict @@ -57,9 +56,9 @@ def showcase_package_association_delete(context, data_dict): data_dict, showcase_package_association_delete_schema(), context) if errors: - raise toolkit.ValidationError(errors) + raise tk.ValidationError(errors) - package_id, showcase_id = toolkit.get_or_bust(validated_data_dict, + package_id, showcase_id = tk.get_or_bust(validated_data_dict, ['package_id', 'showcase_id']) @@ -67,39 +66,8 @@ def showcase_package_association_delete(context, data_dict): package_id=package_id, showcase_id=showcase_id) if showcase_package_association is None: - raise toolkit.ObjectNotFound("ShowcasePackageAssociation with package_id '{0}' and showcase_id '{1}' doesn't exist.".format(package_id, showcase_id)) + raise tk.ObjectNotFound("ShowcasePackageAssociation with package_id '{0}' and showcase_id '{1}' doesn't exist.".format(package_id, showcase_id)) # delete the association showcase_package_association.delete() model.repo.commit() - - -def showcase_admin_remove(context, data_dict): - '''Remove a user to the list of showcase admins. - - :param username: name of the user to remove from showcase user admin list - :type username: string - ''' - - model = context['model'] - - toolkit.check_access('ckanext_showcase_admin_remove', context, data_dict) - - # validate the incoming data_dict - validated_data_dict, errors = validate(data_dict, - showcase_admin_remove_schema(), - context) - - if errors: - raise toolkit.ValidationError(errors) - - username = toolkit.get_or_bust(validated_data_dict, 'username') - user_id = convert_user_name_or_id_to_id(username, context) - - showcase_admin_to_remove = ShowcaseAdmin.get(user_id=user_id) - - if showcase_admin_to_remove is None: - raise toolkit.ObjectNotFound("ShowcaseAdmin with user_id '{0}' doesn't exist.".format(user_id)) - - showcase_admin_to_remove.delete() - model.repo.commit() diff --git a/ckanext/showcase/logic/action/get.py b/ckanext/showcase/logic/action/get.py index aa6b7de0..aa2294cc 100644 --- a/ckanext/showcase/logic/action/get.py +++ b/ckanext/showcase/logic/action/get.py @@ -1,50 +1,114 @@ -import ckan.plugins.toolkit as toolkit +import ckan.plugins.toolkit as tk import ckan.lib.dictization.model_dictize as model_dictize from ckan.lib.navl.dictization_functions import validate - +from ckan.logic import validate as validate_decorator +from ckanext.showcase import utils +from sqlalchemy import or_ +from ckanext.showcase.data.constants import * +import ckan.authz as authz +import datetime from ckanext.showcase.logic.schema import (showcase_package_list_schema, - package_showcase_list_schema) -from ckanext.showcase.model import ShowcasePackageAssociation, ShowcaseAdmin + package_showcase_list_schema, + showcase_search_schema) +from ckanext.showcase.model import ShowcasePackageAssociation, ShowcaseApprovalStatus +from sqlalchemy import Column, ForeignKey, types, or_, func +import ckan.lib.dictization.model_dictize as md + import logging log = logging.getLogger(__name__) -@toolkit.side_effect_free +@tk.side_effect_free def showcase_show(context, data_dict): - '''Return the pkg_dict for a showcase (package). + tk.check_access('ckanext_showcase_show', context, data_dict) - :param id: the id or name of the showcase - :type id: string - ''' + pkg_dict = tk.get_action('package_show')(context, data_dict) + + approval_status = ShowcaseApprovalStatus.get_status_for_showcase(pkg_dict.get('id')) + + if approval_status: + approval_status = approval_status.as_dict() + else: + approval_status = ShowcaseApprovalStatus.update_status(pkg_dict.get('id')) - toolkit.check_access('ckanext_showcase_show', context, data_dict) + pkg_dict['approval_status'] = approval_status - pkg_dict = toolkit.get_action('package_show')(context, data_dict) + model = context["model"] + user = context.get('user') + user_obj = model.User.get(user) + pkg_dict['creator'] = md.user_dictize(user_obj, context) return pkg_dict -@toolkit.side_effect_free +@tk.side_effect_free def showcase_list(context, data_dict): '''Return a list of all showcases in the site.''' - toolkit.check_access('ckanext_showcase_list', context, data_dict) + tk.check_access('ckanext_showcase_list', context, data_dict) + + # model = context["model"] + # user = context.get('user') + # user_obj = model.User.get(user) + + offset = data_dict.pop('page', 1) - 1 + limit = data_dict.pop('limit', 20) + + data_dict['status'] = ApprovalStatus.APPROVED.value + q = ShowcaseApprovalStatus.filter_showcases(**data_dict) + total = q.count() + + if limit != -1: + q = q.offset(offset * limit).limit(limit) + + showcase_ids_list = [] + for pkg in q.all(): + showcase_ids_list.append( + pkg.id + ) + + return { + 'items': showcase_ids_list, + 'total': total + } + +@tk.side_effect_free +@validate_decorator(showcase_search_schema) +def showcase_filtered(context, data_dict): + '''Return a list of all showcases in the site.''' + tk.check_access('ckanext_showcase_list', context, data_dict) model = context["model"] + user = context.get('user') + user_obj = model.User.get(user) + + offset = data_dict.pop('page', 1) - 1 + limit = data_dict.pop('limit', 20) + - q = model.Session.query(model.Package) \ - .filter(model.Package.type == 'showcase') \ - .filter(model.Package.state == 'active') + if not authz.is_authorized_boolean('is_portal_admin', context): + data_dict['creator_user_id'] = user_obj.id + + q = ShowcaseApprovalStatus.filter_showcases(**data_dict) + + total = q.count() + if limit != -1: + q = q.offset(offset * limit).limit(limit) showcase_list = [] for pkg in q.all(): - showcase_list.append(model_dictize.package_dictize(pkg, context)) + showcase_list.append( + tk.get_action('ckanext_showcase_show')(context, {'id': pkg.id}) + ) - return showcase_list + return { + 'items': showcase_list, + 'total': total + } -@toolkit.side_effect_free +@tk.side_effect_free def showcase_package_list(context, data_dict): '''List packages associated with a showcase. @@ -54,7 +118,7 @@ def showcase_package_list(context, data_dict): :rtype: list of dictionaries ''' - toolkit.check_access('ckanext_showcase_package_list', context, data_dict) + tk.check_access('ckanext_showcase_package_list', context, data_dict) # validate the incoming data_dict validated_data_dict, errors = validate(data_dict, @@ -62,7 +126,7 @@ def showcase_package_list(context, data_dict): context) if errors: - raise toolkit.ValidationError(errors) + raise tk.ValidationError(errors) # get a list of package ids associated with showcase id pkg_id_list = ShowcasePackageAssociation.get_package_ids_for_showcase( @@ -76,14 +140,14 @@ def showcase_package_list(context, data_dict): for pkg_id in pkg_id_list: id_list.append(pkg_id[0]) q = 'id:(' + ' OR '.join(['{0}'.format(x) for x in id_list]) + ')' - _pkg_list = toolkit.get_action('package_search')( + _pkg_list = tk.get_action('package_search')( context, {'q': q, 'rows': 100}) pkg_list = _pkg_list['results'] return pkg_list -@toolkit.side_effect_free +@tk.side_effect_free def package_showcase_list(context, data_dict): '''List showcases associated with a package. @@ -93,7 +157,7 @@ def package_showcase_list(context, data_dict): :rtype: list of dictionaries ''' - toolkit.check_access('ckanext_package_showcase_list', context, data_dict) + tk.check_access('ckanext_package_showcase_list', context, data_dict) # validate the incoming data_dict validated_data_dict, errors = validate(data_dict, @@ -101,11 +165,12 @@ def package_showcase_list(context, data_dict): context) if errors: - raise toolkit.ValidationError(errors) + raise tk.ValidationError(errors) # get a list of showcase ids associated with the package id showcase_id_list = ShowcasePackageAssociation.get_showcase_ids_for_package( - validated_data_dict['package_id']) + validated_data_dict['package_id'] + ) showcase_list = [] q = '' @@ -116,7 +181,7 @@ def package_showcase_list(context, data_dict): id_list.append(showcase_id[0]) fq = 'dataset_type:showcase' q = 'id:(' + ' OR '.join(['{0}'.format(x) for x in id_list]) + ')' - _showcase_list = toolkit.get_action('package_search')( + _showcase_list = tk.get_action('package_search')( context, {'q': q, 'fq': fq, 'rows': 100}) showcase_list = _showcase_list['results'] @@ -124,29 +189,28 @@ def package_showcase_list(context, data_dict): return showcase_list -@toolkit.side_effect_free -def showcase_admin_list(context, data_dict): - ''' - Return a list of dicts containing the id and name of all active showcase - admin users. +@tk.side_effect_free +def status_show(context, data_dict): + tk.check_access('ckanext_showcase_status_show', context, data_dict) - :rtype: list of dictionaries - ''' + showcase_id = data_dict.get('showcase_id', None) + feedback_instance = ShowcaseApprovalStatus.get_status_for_showcase(showcase_id=showcase_id).as_dict() - toolkit.check_access('ckanext_showcase_admin_list', context, data_dict) + return feedback_instance - model = context["model"] - user_ids = ShowcaseAdmin.get_showcase_admin_ids() - if user_ids: - q = model.Session.query(model.User) \ - .filter(model.User.state == 'active') \ - .filter(model.User.id.in_(user_ids)) +@tk.side_effect_free +def showcase_statics(context, data_dict): + tk.check_access('ckanext_showcase_list', context, data_dict) - showcase_admin_list = [] - for user in q.all(): - showcase_admin_list.append({'name': user.name, 'id': user.id}) - return showcase_admin_list - - return [] + model = context["model"] + user = context.get('user') + user_obj = model.User.get(user) + + if authz.is_authorized_boolean('is_portal_admin', context): + return ShowcaseApprovalStatus.generate_statistics() + else: + return ShowcaseApprovalStatus.generate_statistics( + creator_user_id=user_obj.id + ) \ No newline at end of file diff --git a/ckanext/showcase/logic/action/update.py b/ckanext/showcase/logic/action/update.py index 783d76ab..c55dca80 100644 --- a/ckanext/showcase/logic/action/update.py +++ b/ckanext/showcase/logic/action/update.py @@ -1,13 +1,20 @@ import logging import ckan.lib.uploader as uploader -import ckan.plugins.toolkit as toolkit - +import ckan.plugins.toolkit as tk +from ckanext.showcase.logic import notifiy +from ckanext.showcase.logic.action.action_decorator import notify_after_action +from ckanext.showcase.model import ShowcaseApprovalStatus +from ckan.common import _ +import ckanext.showcase.logic.schema as showcase_schema +from ckanext.showcase.data.constants import * +from ckan.logic import validate as validate_decorator log = logging.getLogger(__name__) def showcase_update(context, data_dict): + tk.check_access('ckanext_showcase_update',context, data_dict) upload = uploader.get_uploader('showcase', data_dict['image_url']) @@ -16,6 +23,36 @@ def showcase_update(context, data_dict): upload.upload(uploader.get_max_image_size()) - pkg = toolkit.get_action('package_update')(context, data_dict) + site_user = tk.get_action("get_site_user")({"ignore_auth": True}, {}) + updated_context = {'ignore_auth': True, 'user':site_user['name']} + pkg = tk.get_action('package_update')( + context.copy().update(updated_context), + data_dict + ) + + tk.get_action('ckanext_showcase_status_update')( + context.copy().update(updated_context), + {"showcase_id": pkg.get("id",pkg.get("name", '')) } + ) return pkg + + +@validate_decorator(showcase_schema.showcase_status_update_schema) +@notify_after_action(notifiy.status_update) +def status_update(context, data_dict): + tk.check_access('ckanext_showcase_status_update',context, data_dict) + + showcase_id = data_dict.get('showcase_id') + status = data_dict.get('status', ApprovalStatus.PENDING) + feedback = data_dict.get('feedback','') if status == ApprovalStatus.NEEDS_REVISION else '' + + update_status = ShowcaseApprovalStatus.update_status( + showcase_id, + feedback, + status + ) + + return update_status + + diff --git a/ckanext/showcase/logic/auth.py b/ckanext/showcase/logic/auth.py index 86e58cf2..b32a1f51 100644 --- a/ckanext/showcase/logic/auth.py +++ b/ckanext/showcase/logic/auth.py @@ -1,7 +1,12 @@ -import ckan.plugins.toolkit as toolkit +import ckan.plugins.toolkit as tk import ckan.model as model +from ckan.common import _ +from ckanext.showcase import utils +from sqlalchemy import or_ -from ckanext.showcase.model import ShowcaseAdmin +from ckanext.showcase.model import ShowcaseApprovalStatus +from ckanext.showcase.data.constants import * +import ckan.authz as authz import logging log = logging.getLogger(__name__) @@ -18,53 +23,79 @@ def get_auth_functions(): 'ckanext_showcase_package_association_delete': package_association_delete, 'ckanext_showcase_package_list': showcase_package_list, 'ckanext_package_showcase_list': package_showcase_list, - 'ckanext_showcase_admin_add': add_showcase_admin, - 'ckanext_showcase_admin_remove': remove_showcase_admin, - 'ckanext_showcase_admin_list': showcase_admin_list, 'ckanext_showcase_upload': showcase_upload, + + # Approval Workflow + 'ckanext_showcase_status_show': status_show, + 'ckanext_showcase_status_update': status_update, } -def _is_showcase_admin(context): - ''' - Determines whether user in context is in the showcase admin list. - ''' - user = context.get('user', '') - userobj = model.User.get(user) - return ShowcaseAdmin.is_user_showcase_admin(userobj) +def _is_user_the_creator(context, data_dict, key='id'): + if authz.auth_is_anon_user(context): return False + user = context.get('user') + model = context['model'] + user_obj = model.User.get(user) -def create(context, data_dict): - '''Create a Showcase. + q = model.Session.query(model.Package) \ + .filter(model.Package.type == utils.DATASET_TYPE_NAME)\ + .filter(model.Package.creator_user_id == user_obj.id)\ + .filter(or_( + model.Package.id == data_dict.get(key,''), + model.Package.name == data_dict.get(key,''), + )) + + return bool(q.count()) - Only sysadmin or users listed as Showcase Admins can create a Showcase. - ''' - return {'success': _is_showcase_admin(context)} + +def create(context, data_dict): + return {'success': (not authz.auth_is_anon_user(context))} def delete(context, data_dict): - '''Delete a Showcase. - - Only sysadmin or users listed as Showcase Admins can delete a Showcase. - ''' - return {'success': _is_showcase_admin(context)} + return { + 'success': False, + 'msg': _('User not authorized to delete a submitted Reuse') + } def update(context, data_dict): - '''Update a Showcase. - - Only sysadmin or users listed as Showcase Admins can update a Showcase. - ''' - return {'success': _is_showcase_admin(context)} + if _is_user_the_creator(context, data_dict): + return {'success': True} + else: + return { + 'success': False, + 'msg': _('User not authorized to delete a submitted Reuse') + } -@toolkit.auth_allow_anonymous_access +@tk.auth_allow_anonymous_access def show(context, data_dict): '''All users can access a showcase show''' - return {'success': True} + showcase_id = data_dict.get('id','') + + showcase = model.Session.query(model.Package) \ + .filter(model.Package.id == showcase_id)\ + .filter(model.Package.type == utils.DATASET_TYPE_NAME)\ + .first() + + + if not showcase: + return {'success': False, 'msg': _('Reuse does not exist')} + + + status_obj = ShowcaseApprovalStatus.get(showcase_id=showcase_id).as_dict() or ShowcaseApprovalStatus.update_status(showcase_id,'') + if status_obj['status'] == ApprovalStatus.APPROVED.value \ + or _is_user_the_creator(context, data_dict) \ + or authz.is_authorized_boolean('is_portal_admin', context): + return {'success': True} + else: + return {'success': False, 'msg': _('User not authorized to view this Reuse')} -@toolkit.auth_allow_anonymous_access + +@tk.auth_allow_anonymous_access def showcase_list(context, data_dict): '''All users can access a showcase list''' return {'success': True} @@ -76,7 +107,7 @@ def package_association_create(context, data_dict): Only sysadmins or user listed as Showcase Admins can create a package/showcase association. ''' - return {'success': _is_showcase_admin(context)} + return {'success': _is_user_the_creator(context, data_dict, 'showcase_id')} def package_association_delete(context, data_dict): @@ -85,36 +116,52 @@ def package_association_delete(context, data_dict): Only sysadmins or user listed as Showcase Admins can delete a package/showcase association. ''' - return {'success': _is_showcase_admin(context)} + return {'success': _is_user_the_creator(context, data_dict, 'showcase_id')} -@toolkit.auth_allow_anonymous_access +@tk.auth_allow_anonymous_access def showcase_package_list(context, data_dict): '''All users can access a showcase's package list''' - return {'success': True} + showcase_id = data_dict.get('showcase_id', None) + return authz.is_authorized( + 'ckanext_showcase_show', + context, {**data_dict, "id":showcase_id} + ) -@toolkit.auth_allow_anonymous_access +@tk.auth_allow_anonymous_access def package_showcase_list(context, data_dict): '''All users can access a packages's showcase list''' return {'success': True} -def add_showcase_admin(context, data_dict): - '''Only sysadmins can add users to showcase admin list.''' - return {'success': False} +def showcase_upload(context, data_dict): + return {'success': (not authz.auth_is_anon_user(context))} + +def status_show(context, data_dict): + showcase_id = data_dict.get('id','') + showcase = tk.get_action('ckanext_showcase_show')( + context, + {'id': data_dict['id']} + ) -def remove_showcase_admin(context, data_dict): - '''Only sysadmins can remove users from showcase admin list.''' - return {'success': False} + if not showcase: + return {'success': False, 'msg': _('Reuse does not exist')} + status_obj = ShowcaseApprovalStatus.get(showcase_id=showcase_id).as_dict() \ + or ShowcaseApprovalStatus.update_status(showcase_id,'') -def showcase_admin_list(context, data_dict): - '''Only sysadmins can list showcase admin users.''' - return {'success': False} + if status_obj['status'] == ApprovalStatus.APPROVED.value \ + or _is_user_the_creator(context, data_dict) \ + or authz.is_authorized_boolean('is_portal_admin', context): + return {'success': True} + else: + return {'success': False, 'msg': _('User not authorized to view the status')} -def showcase_upload(context, data_dict): - '''Only sysadmins can upload images.''' - return {'success': _is_showcase_admin(context)} +def status_update(context, data_dict): + if authz.is_authorized_boolean('is_portal_admin', context): + return {'success': True} + + return {'success': False, 'msg': _('User not authorized to update Reuse status')} \ No newline at end of file diff --git a/ckanext/showcase/logic/helpers.py b/ckanext/showcase/logic/helpers.py index 57c3ef4b..881aec3e 100644 --- a/ckanext/showcase/logic/helpers.py +++ b/ckanext/showcase/logic/helpers.py @@ -1,5 +1,22 @@ import ckan.lib.helpers as h from ckan.plugins import toolkit as tk +from ckanext.showcase.data.constants import REUSE_CASE_TYPE_OPTIONS, SHOWCASE_STATUS_OPTIONS, ApprovalStatus + + + +def get_helpers(): + return { + 'facet_remove_field': facet_remove_field, + 'get_site_statistics': get_site_statistics, + 'showcase_get_wysiwyg_editor': showcase_get_wysiwyg_editor, + 'showcase_status_options': showcase_status_options, + 'showcase_status_filter_options': showcase_status_filter_options, + 'ckanext_showcase_metatdata': ckanext_showcase_metatdata, + 'ckanext_showcase_types': ckanext_showcase_types, + } + + +############################################# def facet_remove_field(key, value=None, replace=None): @@ -34,3 +51,122 @@ def get_site_statistics(): def showcase_get_wysiwyg_editor(): return tk.config.get('ckanext.showcase.editor', '') + + +def showcase_status_options(): + return [ + {'text': value, 'value':key} + for key, value in SHOWCASE_STATUS_OPTIONS.items() + ] + + + +def showcase_status_filter_options(): + return [ + {'text': tk._("Select Status"), 'value': ''} + ] + showcase_status_options() + +_ = tk._ +from ckan.common import _, config + +def ckanext_showcase_metatdata(showcase, showcase_datasets, user_info): + return [ + { + 'label': _("Title En"), + 'value': showcase.get('title', ''), + 'type': 'text', + }, + { + 'label': _("Title Ar"), + 'value': showcase.get('title_ar', ''), + 'type': 'text', + }, + { + 'label': _("Slug"), + 'value': showcase.get('name', ''), + 'type': 'text', + }, + { + 'label': _("Description En"), + 'value': h.render_markdown(showcase.get('notes', '')), + 'type': 'text', + }, + { + 'label': _("Description Ar"), + 'value': h.render_markdown(showcase.get('notes_ar', '')), + 'type': 'text', + }, + { + 'label': _("Date Created"), + 'value': showcase.get('metadata_created', ''), + 'type': 'date', + }, + { + 'label': _("Reuse Case Type"), + 'value': [REUSE_CASE_TYPE_OPTIONS[reuse_type] for reuse_type in showcase.get('reuse_type',[])], + 'type': 'list', + }, + { + 'label': _("The User"), + 'value': user_info['user_dict']['fullname'] or user_info['user_dict']['name'], + 'type': 'text', + }, + { + 'label': _("Status"), + 'value': showcase.get('approval_status', {})['display_status'], + 'type': 'text', + }, + { + 'label': _("Admin Feedback"), + 'value': showcase.get('approval_status', {}).get('feedback',''), + 'type': 'text', + }, + { + 'label': _("Last Status Update"), + 'value': showcase.get('approval_status', {}).get('status_modified',''), + 'type': 'date', + }, + { + 'label': _("Submitted Author Name"), + 'value': showcase.get('author', ''), + 'type': 'text', + }, + { + 'label': _("Submitted Author Email"), + 'value': showcase.get('author_email', ''), + 'type': 'email', + }, + { + 'label': _("Image Url"), + 'value': showcase.get('image_display_url', ''), + 'type': 'link', + 'url': showcase.get('image_display_url', ''), + }, + { + 'label': _("External Link"), + 'value': showcase.get('url', ''), + 'type': 'link', + 'url': showcase.get('url', ''), + }, + { + 'label': _("Associated Datasets"), + 'value': "".join([ + f" {dataset.get('display_name', dataset.get('title', '') )}" + for dataset in showcase_datasets + ]), + 'type': 'markup', + }, + { + 'label': _("Reuse Case Public Display"), + 'value': "Link", + 'type': 'link', + 'url': h.url_for('showcase_blueprint.read', id=showcase.get('id', '')), + }, + ] + + +def ckanext_showcase_types(): + return [ + {'text': value, 'value':key} + for key, value in REUSE_CASE_TYPE_OPTIONS.items() + ] diff --git a/ckanext/showcase/logic/notifiy.py b/ckanext/showcase/logic/notifiy.py new file mode 100644 index 00000000..3d7b9f57 --- /dev/null +++ b/ckanext/showcase/logic/notifiy.py @@ -0,0 +1,146 @@ +from ckan.lib.mailer import mail_user +import ckan.plugins.toolkit as tk +import ckan.lib.helpers as h +from ckan.common import config +from ckan import model +from ckan.lib.jobs import enqueue as enqueue_job +from ckanext.showcase.templates.emails import EmailTemplates, NotificationTemplates, SubjectTemplates + +_ = tk._ + +import logging +log = logging.getLogger(__name__) + +site_title = config.get('ckan.site_title') +site_url = config.get('ckan.site_url') + + +def _get_notification_context(): + return { + 'model': model, + 'user': tk.g.user or tk.g.author, + 'session': model.Session, + 'auth_user_obj': tk.g.userobj, + 'ignore_auth': True + } + +def _get_all_portal_admins(): + context = { + 'model': model, + 'session': model.Session, + 'user': tk.g.user or tk.g.author, + 'auth_user_obj': tk.g.userobj, + 'ignore_auth': True + } + members = tk.get_action('member_list')(context, {'id':'portal_administrator', 'type': 'user'}) + members = [member[0] for member in members] + + return members + + + +def showcase_create(showcase_id): + showcase = tk.get_action('ckanext_showcase_show')(_get_notification_context(), {'id': showcase_id}) + + body_vars = {'showcase': showcase} + action_url = site_url + h.url_for('showcase_blueprint.read', id = showcase['id']) + + # Requester + requster = model.User.get(showcase.get('creator_user_id')) + body_vars['user_name'] = requster.fullname or requster.name + body_vars['opening_word'] = 'Your' + + + _notify_user(requster, body_vars, 'get_showcase_create', action_url) + + + body_vars['opening_word'] = 'A' + + # admins + portal_admins = _get_all_portal_admins() + body_vars['role'] = 'admin' + + for admin in portal_admins: + user = model.User.get(admin) + body_vars['user_name'] = user.fullname or user.name + _notify_user(user, body_vars, 'get_showcase_create', action_url) + + +def status_update(showcase_id): + showcase = tk.get_action('ckanext_showcase_show')(_get_notification_context(), {'id': showcase_id}) + showcase_status = tk.get_action('ckanext_showcase_status_show')(_get_notification_context(), {'showcase_id': showcase_id}) + + showcase = {**showcase, **showcase_status} + body_vars = {'showcase': showcase} + action_url = site_url + h.url_for('showcase_blueprint.read', id = showcase['id']) + + # Requester + requster = model.User.get(showcase.get('creator_user_id')) + body_vars['user_name'] = requster.fullname or requster.name + body_vars['opening_word'] = 'Your' + + _notify_user(user, body_vars, 'get_status_update', action_url) + + + # admins + portal_admins = _get_all_portal_admins() + body_vars['role'] = 'admin' + + for admin in portal_admins: + user = model.User.get(admin) + body_vars['user_name'] = user.fullname or user.name + _notify_user(user, body_vars, 'get_status_update', action_url) + + + +def _notify_user(user, body_vars, template_name, action_url): + + subject_method = getattr(SubjectTemplates, template_name, None) + + # Check if method exists + if subject_method: + subject = subject_method(body_vars) + else: + raise ValueError(f"Template method '{template_name}' not found.") + + notification_method = getattr(NotificationTemplates, template_name, None) + + # Check if method exists + if notification_method: + notification = notification_method(body_vars) + else: + raise ValueError(f"Template method '{template_name}' not found.") + + + body_method = getattr(EmailTemplates, template_name, None) + + # Check if method exists + if body_method: + body = body_method(body_vars) + else: + raise ValueError(f"Template method '{template_name}' not found.") + + + tk.get_action('generate_notification')(_get_notification_context(), { + 'user_id': user.id, + 'subject': subject, + 'body': notification, + 'action_url': action_url, + }) + + queue_email_job(user, subject, body) + + +def _send_email(user, subject, body): + if user.email: + try: + log.info(f'ISSUE_EMAIL_LOG1 {user.email}') + mail_user(user, subject, body) + log.info(f'ISSUE_EMAIL_LOG2 {user.email}') + except: + log.critical('Email sending failed.') + + +def queue_email_job(user, subject, body): + """Queue the email job to be processed in the background.""" + enqueue_job(_send_email, args=[user, subject, body], title='Send Email',rq_kwargs={'timeout': 300}) \ No newline at end of file diff --git a/ckanext/showcase/logic/schema.py b/ckanext/showcase/logic/schema.py index c78d3b9f..fe7cd625 100644 --- a/ckanext/showcase/logic/schema.py +++ b/ckanext/showcase/logic/schema.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -import ckan.plugins.toolkit as toolkit +import ckan.plugins.toolkit as tk from ckan.logic.schema import (default_tags_schema, default_extras_schema, @@ -7,27 +7,40 @@ from ckanext.showcase.logic.validators import ( convert_package_name_or_id_to_id_for_type_dataset, - convert_package_name_or_id_to_id_for_type_showcase) - -if toolkit.check_ckan_version("2.10"): - unicode_safe = toolkit.get_validator("unicode_safe") + convert_package_name_or_id_to_id_for_type_showcase, + is_valid_filter_status, + is_valid_status, + validate_reuse_types, + validate_status_feedback + ) + +if tk.check_ckan_version("2.10"): + unicode_safe = tk.get_validator("unicode_safe") else: unicode_safe = str -not_empty = toolkit.get_validator("not_empty") -empty = toolkit.get_validator("empty") -if_empty_same_as = toolkit.get_validator("if_empty_same_as") -ignore_missing = toolkit.get_validator("ignore_missing") -ignore = toolkit.get_validator("ignore") -keep_extras = toolkit.get_validator("keep_extras") +not_empty = tk.get_validator("not_empty") +empty = tk.get_validator("empty") +if_empty_same_as = tk.get_validator("if_empty_same_as") +ignore_missing = tk.get_validator("ignore_missing") +ignore = tk.get_validator("ignore") +keep_extras = tk.get_validator("keep_extras") + +package_id_not_changed = tk.get_validator("package_id_not_changed") +name_validator = tk.get_validator("name_validator") +user_id_or_name_exists = tk.get_validator("user_id_or_name_exists") +package_name_validator = tk.get_validator("package_name_validator") +tag_string_convert = tk.get_validator("tag_string_convert") +ignore_not_package_admin = tk.get_validator("ignore_not_package_admin") +url_validator = tk.get_validator("url_validator") +convert_to_extras = tk.get_validator("convert_to_extras") +convert_from_extras = tk.get_validator("convert_from_extras") +ignore_empty = tk.get_validator("ignore_empty") +isodate = tk.get_validator("isodate") +natural_number_validator = tk.get_validator("natural_number_validator") +int_validator = tk.get_validator("int_validator") + -package_id_not_changed = toolkit.get_validator("package_id_not_changed") -name_validator = toolkit.get_validator("name_validator") -user_id_or_name_exists = toolkit.get_validator("user_id_or_name_exists") -package_name_validator = toolkit.get_validator("package_name_validator") -tag_string_convert = toolkit.get_validator("tag_string_convert") -ignore_not_package_admin = toolkit.get_validator("ignore_not_package_admin") -url_validator = toolkit.get_validator("url_validator") def showcase_base_schema(): @@ -35,10 +48,13 @@ def showcase_base_schema(): 'id': [empty], 'revision_id': [ignore], 'name': [not_empty, name_validator, package_name_validator], - 'title': [if_empty_same_as("name"), unicode_safe], + 'title': [not_empty, unicode_safe], + 'title_ar': [convert_to_extras, not_empty, unicode_safe], 'author': [ignore_missing, unicode_safe], 'author_email': [ignore_missing, unicode_safe], - 'notes': [ignore_missing, unicode_safe], + 'notes': [not_empty, unicode_safe], + 'notes_ar': [convert_to_extras, not_empty, unicode_safe], + 'reuse_type': [convert_to_extras, not_empty, validate_reuse_types], 'url': [ignore_missing, url_validator], 'state': [ignore_not_package_admin, ignore_missing], 'type': [ignore_missing, unicode_safe], @@ -50,8 +66,8 @@ def showcase_base_schema(): 'extras': default_extras_schema(), 'save': [ignore], 'return_to': [ignore], - 'image_url': [toolkit.get_validator('ignore_missing'), - toolkit.get_converter('convert_to_extras')] + 'image_url': [tk.get_validator('ignore_missing'), + tk.get_converter('convert_to_extras')] } return schema @@ -89,6 +105,9 @@ def showcase_show_schema(): schema.update({ 'state': [ignore_missing], + 'title_ar': [convert_from_extras, ignore_missing, unicode_safe], + 'notes_ar': [convert_from_extras, ignore_missing, unicode_safe], + 'reuse_type': [convert_from_extras, ignore_missing, unicode_safe], }) # Remove validators for several keys from the schema so validation doesn't @@ -109,11 +128,11 @@ def showcase_show_schema(): schema['tracking_summary'] = [ignore_missing] schema.update({ - 'image_url': [toolkit.get_converter('convert_from_extras'), - toolkit.get_validator('ignore_missing')], + 'image_url': [tk.get_converter('convert_from_extras'), + tk.get_validator('ignore_missing')], 'original_related_item_id': [ - toolkit.get_converter('convert_from_extras'), - toolkit.get_validator('ignore_missing')] + tk.get_converter('convert_from_extras'), + tk.get_validator('ignore_missing')] }) return schema @@ -149,12 +168,32 @@ def package_showcase_list_schema(): return schema -def showcase_admin_add_schema(): + +def showcase_status_update_schema(): schema = { - 'username': [not_empty, user_id_or_name_exists, unicode_safe], + 'showcase_id': [ + not_empty, + unicode_safe, + convert_package_name_or_id_to_id_for_type_showcase + ], + "status": [ + ignore_missing, + is_valid_status + ], + "feedback": [ignore_missing, unicode_safe], + '__after': [validate_status_feedback], } return schema +def showcase_search_schema(): + schema = { + 'q':[ignore_missing, unicode_safe], + "created_start": [ignore_missing, isodate], + "created_end": [ignore_missing, isodate], + 'status': [ignore_empty, is_valid_filter_status], + 'page': [ignore_empty, natural_number_validator], + 'limit': [ignore_empty, int_validator], + 'sort': [ignore_missing, unicode_safe], + } -def showcase_admin_remove_schema(): - return showcase_admin_add_schema() + return schema \ No newline at end of file diff --git a/ckanext/showcase/logic/validators.py b/ckanext/showcase/logic/validators.py index b27885a9..05c872a1 100644 --- a/ckanext/showcase/logic/validators.py +++ b/ckanext/showcase/logic/validators.py @@ -1,8 +1,8 @@ from ckan.plugins import toolkit as tk - +from ckanext.showcase.data.constants import * _ = tk._ Invalid = tk.Invalid - +import datetime def convert_package_name_or_id_to_id_for_type(package_name_or_id, context, package_type='dataset'): @@ -42,3 +42,51 @@ def convert_package_name_or_id_to_id_for_type_showcase(package_name_or_id, return convert_package_name_or_id_to_id_for_type(package_name_or_id, context, package_type='showcase') + + +def is_valid_status(applied_status, context): + status_map = {status.value: status.name for status in ApprovalStatus} + if applied_status not in status_map: + raise Invalid(_(f"Invalid status: {applied_status}. Must be one of: {list(status_map.keys())}")) + + return ApprovalStatus[status_map[applied_status]] + +def is_valid_filter_status(applied_status, context): + status_map = {status.value: status.name for status in ApprovalStatus} + if applied_status not in status_map: + raise Invalid(_(f"Invalid status: {applied_status}. Must be one of: {list(status_map.keys())}")) + + return applied_status + + +def validate_status_feedback(key, flattened_data, errors, context): + key1 = ('feedback',) + feedback = flattened_data.get(key1) + status = flattened_data.get(('status',)) + + + if status == ApprovalStatus.NEEDS_REVISION and not feedback: + errors.get(key1, []).append( + _("Feedback must be provided with the selected status") + ) + else: + flattened_data[key1] = feedback or '' + + +def validate_reuse_types(applied_types, context): + if isinstance(applied_types, str): + applied_types = [applied_types] + + if not isinstance(applied_types, list): + raise Invalid(_("Invalid input: Reuse types must be a list")) + + status_map = {status.value: status.name for status in ReuseCaseType} + selected_types = [] + for c_type in applied_types: + if c_type in status_map: + selected_types.append(c_type) + else: + raise Invalid(_(f"Invalid type: {c_type}. Must be one of: {list(status_map.keys())}")) + + return selected_types + diff --git a/ckanext/showcase/migration/showcase/alembic.ini b/ckanext/showcase/migration/showcase/alembic.ini index bf93ad50..955fc558 100644 --- a/ckanext/showcase/migration/showcase/alembic.ini +++ b/ckanext/showcase/migration/showcase/alembic.ini @@ -35,7 +35,7 @@ script_location = %(here)s # are written from script.py.mako # output_encoding = utf-8 -sqlalchemy.url = driver://user:pass@localhost/dbname +sqlalchemy.url = postgresql://ckan_default:123698745@localhost/ckan_default # Logging configuration diff --git a/ckanext/showcase/migration/showcase/versions/02b006cb222c_add_ckanext_showcase_tables.py b/ckanext/showcase/migration/showcase/versions/02b006cb222c_add_ckanext_showcase_tables.py index d82a90d5..d5e23903 100644 --- a/ckanext/showcase/migration/showcase/versions/02b006cb222c_add_ckanext_showcase_tables.py +++ b/ckanext/showcase/migration/showcase/versions/02b006cb222c_add_ckanext_showcase_tables.py @@ -7,7 +7,7 @@ """ from alembic import op import sqlalchemy as sa - +from ckanext.showcase.data.constants import * # revision identifiers, used by Alembic. revision = "02b006cb222c" @@ -49,8 +49,31 @@ def upgrade(): nullable=False, ), ) + + if "showcase_approval" not in tables: + op.create_table( + "showcase_approval", + sa.Column( + "showcase_id", + sa.UnicodeText, + sa.ForeignKey("package.id", ondelete="CASCADE", onupdate="CASCADE"), + primary_key=True, + nullable=False, + ), + sa.Column("feedback", sa.UnicodeText, nullable=True), + sa.Column( + "status", + sa.Enum( + ApprovalStatus, + name="status_enum" + ), + nullable=False, + default=ApprovalStatus.PENDING + ) + ) def downgrade(): op.drop_table("showcase_package_association") op.drop_table("showcase_admin") + op.drop_table("showcase_approval") diff --git a/ckanext/showcase/migration/showcase/versions/09f4781de341_latest_migrations.py b/ckanext/showcase/migration/showcase/versions/09f4781de341_latest_migrations.py new file mode 100644 index 00000000..6933e4b5 --- /dev/null +++ b/ckanext/showcase/migration/showcase/versions/09f4781de341_latest_migrations.py @@ -0,0 +1,41 @@ +"""Latest Migrations + +Revision ID: 09f4781de341 +Revises: 444c55f4d42c +Create Date: 2024-08-04 10:50:59.928825 + +""" +from alembic import op +from ckanext.showcase.data.constants import ApprovalStatus +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '09f4781de341' +down_revision = '444c55f4d42c' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('showcase_approval') + op.create_table('showcase_approval', + sa.Column('showcase_id', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('feedback', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column( + 'status', + sa.types.Enum(ApprovalStatus, name="showcase_status_enum"), + nullable=False, + default=ApprovalStatus.PENDING + ), + sa.ForeignKeyConstraint(['showcase_id'], ['package.id'], name='showcase_approval_showcase_id_fkey', onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('showcase_id', name='showcase_approval_pkey') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/ckanext/showcase/migration/showcase/versions/22fe3db8aba8_add_showcase_approval_status_table.py b/ckanext/showcase/migration/showcase/versions/22fe3db8aba8_add_showcase_approval_status_table.py new file mode 100644 index 00000000..aeea0f79 --- /dev/null +++ b/ckanext/showcase/migration/showcase/versions/22fe3db8aba8_add_showcase_approval_status_table.py @@ -0,0 +1,79 @@ +"""Add showcase approval status table + +Revision ID: 22fe3db8aba8 +Revises: 02b006cb222c +Create Date: 2024-07-21 10:49:16.978530 + +""" +from alembic import op +import sqlalchemy as sa +from ckanext.showcase.data.constants import * + +# revision identifiers, used by Alembic. +revision = '22fe3db8aba8' +down_revision = '02b006cb222c' +branch_labels = None +depends_on = None + + +def upgrade(): + engine = op.get_bind() + inspector = sa.inspect(engine) + tables = inspector.get_table_names() + if "showcase_package_association" not in tables: + op.create_table( + "showcase_package_association", + sa.Column( + "package_id", + sa.UnicodeText, + sa.ForeignKey("package.id", ondelete="CASCADE", onupdate="CASCADE"), + primary_key=True, + nullable=False, + ), + sa.Column( + "showcase_id", + sa.UnicodeText, + sa.ForeignKey("package.id", ondelete="CASCADE", onupdate="CASCADE"), + primary_key=True, + nullable=False, + ), + ) + if "showcase_package_association" not in tables: + op.create_table( + "showcase_admin", + sa.Column( + "user_id", + sa.UnicodeText, + sa.ForeignKey("user.id", ondelete="CASCADE", onupdate="CASCADE"), + primary_key=True, + nullable=False, + ), + ) + + if "showcase_approval" not in tables: + op.create_table( + "showcase_approval", + sa.Column( + "showcase_id", + sa.UnicodeText, + sa.ForeignKey("package.id", ondelete="CASCADE", onupdate="CASCADE"), + primary_key=True, + nullable=False, + ), + sa.Column("feedback", sa.UnicodeText, nullable=True), + sa.Column( + "status", + sa.Enum( + ApprovalStatus, + name="status_enum" + ), + nullable=False, + default=ApprovalStatus.PENDING + ) + ) + + +def downgrade(): + op.drop_table("showcase_package_association") + op.drop_table("showcase_admin") + op.drop_table("showcase_approval") diff --git a/ckanext/showcase/migration/showcase/versions/444c55f4d42c_description_of_migration.py b/ckanext/showcase/migration/showcase/versions/444c55f4d42c_description_of_migration.py new file mode 100644 index 00000000..644aef5a --- /dev/null +++ b/ckanext/showcase/migration/showcase/versions/444c55f4d42c_description_of_migration.py @@ -0,0 +1,41 @@ +"""description of migration + +Revision ID: 444c55f4d42c +Revises: 22fe3db8aba8 +Create Date: 2024-08-04 10:36:46.093276 + +""" +from alembic import op +from ckanext.showcase.data.constants import ApprovalStatus +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '444c55f4d42c' +down_revision = '22fe3db8aba8' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('showcase_approval', + sa.Column('showcase_id', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('feedback', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column( + 'status', + sa.types.Enum(ApprovalStatus, name="showcase_status_enum"), + nullable=False, + default=ApprovalStatus.PENDING + ), + sa.ForeignKeyConstraint(['showcase_id'], ['package.id'], name='showcase_approval_showcase_id_fkey', onupdate='CASCADE', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('showcase_id', name='showcase_approval_pkey') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('showcase_approval') + + # ### end Alembic commands ### diff --git a/ckanext/showcase/migration/showcase/versions/87bec6583583_date_added_in_approval_table.py b/ckanext/showcase/migration/showcase/versions/87bec6583583_date_added_in_approval_table.py new file mode 100644 index 00000000..dc238e0d --- /dev/null +++ b/ckanext/showcase/migration/showcase/versions/87bec6583583_date_added_in_approval_table.py @@ -0,0 +1,30 @@ +"""Date added in Approval table + +Revision ID: 87bec6583583 +Revises: 09f4781de341 +Create Date: 2024-08-04 10:57:45.897601 + +""" +import datetime +from alembic import op +from ckanext.showcase.data.constants import ApprovalStatus +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '87bec6583583' +down_revision = '09f4781de341' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('showcase_approval', sa.Column('status_modified', sa.types.DateTime, nullable=True, default=datetime.datetime.utcnow)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('ckanext_issues', 'status_modified') diff --git a/ckanext/showcase/model/__init__.py b/ckanext/showcase/model/__init__.py index 120a1ad0..a2e582b9 100644 --- a/ckanext/showcase/model/__init__.py +++ b/ckanext/showcase/model/__init__.py @@ -1,10 +1,25 @@ -from sqlalchemy import Column, ForeignKey, types - +from sqlalchemy import Column, ForeignKey, types, or_, func +import datetime from ckan.model.domain_object import DomainObject from ckan.model.meta import Session +from ckanext.showcase.data.constants import * +from ckan.model.meta import Session +from ckan import model +from ckanext.showcase import utils import logging +from six import text_type +from sqlalchemy.orm import class_mapper +try: + from sqlalchemy.engine import Row +except ImportError: + try: + from sqlalchemy.engine.result import RowProxy as Row + except ImportError: + from sqlalchemy.engine.base import RowProxy as Row + + try: from ckan.plugins.toolkit import BaseModel except ImportError: @@ -71,11 +86,12 @@ def get_package_ids_for_showcase(cls, showcase_id): @classmethod def get_showcase_ids_for_package(cls, package_id): - """ - Return a list of showcase ids associated with the passed package_id. - """ showcase_package_association_list = ( - Session.query(cls.showcase_id).filter_by(package_id=package_id).all() + Session.query(cls.showcase_id) \ + .filter_by(package_id=package_id) \ + .join(ShowcaseApprovalStatus, cls.showcase_id == ShowcaseApprovalStatus.showcase_id) + .filter(ShowcaseApprovalStatus.status == ApprovalStatus.PENDING) + .all() ) return showcase_package_association_list @@ -104,3 +120,201 @@ def is_user_showcase_admin(cls, user): Determine whether passed user is in the showcase admin list. """ return user.id in cls.get_showcase_admin_ids() + +class ShowcaseApprovalStatus(ShowcaseBaseModel, BaseModel): + __tablename__ = "showcase_approval" + + # TODO Check if date fields are required + + showcase_id = Column( + types.UnicodeText, + ForeignKey("package.id", ondelete="CASCADE", onupdate="CASCADE"), + nullable=False, + primary_key=True, + ) + feedback = Column(types.UnicodeText, nullable=True) + status = Column( + types.Enum( + ApprovalStatus, + name="status_enum" + ), + nullable=False, + default=ApprovalStatus.PENDING + ) + status_modified = Column(types.DateTime, default=datetime.datetime.utcnow) + + + @classmethod + def get_status_for_showcase(cls, showcase_id): + return cls.filter(showcase_id=showcase_id).first() + + @classmethod + def update_status(cls, showcase_id, feedback='', status=ApprovalStatus.PENDING): + feedback_instance = cls.get(showcase_id=showcase_id) + if feedback_instance: + feedback_instance.feedback = feedback + feedback_instance.status = status + feedback_instance.status_modified = datetime.datetime.now() + Session.commit() + return feedback_instance.as_dict() + else: + return cls.create( + showcase_id=showcase_id, + feedback = feedback, + status = status + ) + + + def as_dict(self): + result_dict = {} + + if isinstance(self, Row): + fields = self.keys() + else: + ModelClass = self.__class__ + table = class_mapper(ModelClass).mapped_table + fields = [field.name for field in table.c] + + for name in fields: + value = getattr(self, name) + + if value is None\ + or isinstance(value, dict)\ + or isinstance(value, int)\ + or isinstance(value, list): + result_dict[name] = value + elif isinstance(value, Enum): + result_dict[name] = value.value + if name == 'status': + result_dict['display_status'] = SHOWCASE_STATUS_OPTIONS[value.value] + + elif isinstance(value, datetime.datetime): + result_dict[name] = value.isoformat() + else: + result_dict[name] = text_type(value) + + return result_dict + + + @classmethod + def generate_statistics(cls, creator_user_id=None): + session = Session() + + # Base query + query = session.query(cls)\ + .join(model.Package, model.Package.id == cls.showcase_id)\ + .filter(model.Package.type == utils.DATASET_TYPE_NAME)\ + .filter(model.Package.state == 'active') + + if creator_user_id: + query = query.filter(model.Package.creator_user_id == creator_user_id) + + total_count = query.count() + + # Breakdown by status + status_breakdown = query.with_entities( + cls.status, func.count(cls.showcase_id) + ).group_by(cls.status).all() + + status_dict = {status.value: count for status, count in status_breakdown} + + statistics = { + 'total': total_count, + 'status_breakdown': status_dict, + } + + return statistics + + @classmethod + def filter_showcases(cls, **kwargs): + query = Session.query(model.Package.id) \ + .filter(model.Package.type == utils.DATASET_TYPE_NAME) \ + .filter(model.Package.state == 'active') \ + .join(cls, model.Package.id == ShowcaseApprovalStatus.showcase_id) + # TODO do we need an outer join + + query = cls.filter_by_search_query(query, kwargs.pop('q', '')) + + query = cls.filter_by_date( + query, + created_start=kwargs.pop('created_start', None), + created_end=kwargs.pop('created_end', None), + ) + + sort_fields = kwargs.pop('sort', 'metadata_created desc').split() + sort_field, sort_order = sort_fields[0], sort_fields[1] + + query = cls.filter_by_status( + query, + status = kwargs.pop('status', '') + ) + + query = cls.filter_by_creator_user_id( + query, + creator_user_id=kwargs.pop('creator_user_id', '') + ) + + + if sort_field: + if hasattr(model.Package, sort_field): + required_class = model.Package + elif hasattr(cls, sort_field): + required_class = cls + if required_class: + if sort_order == 'desc': + query = query.order_by(getattr(required_class, sort_field).desc()) + else: + query = query.order_by(getattr(required_class, sort_field).asc()) + + return query + + @classmethod + def filter_by_search_query(cls, query, search_query): + search_terms = search_query.split() + + if search_terms: + title_conditions = [model.Package.title.ilike(f'%{word}%') for word in search_terms] + notes_conditions = [model.Package.notes.ilike(f'%{word}%') for word in search_terms] + name_conditions = [model.Package.name.ilike(f'%{word}%') for word in search_terms] + + combined_conditions = or_(*title_conditions, *notes_conditions, *name_conditions) + query = query.filter(combined_conditions) + + return query + + + @classmethod + def filter_by_status(cls, query, status = None): + if not status: return query + + status = cls.convert_status_to_enum(status) + query = query.filter(ShowcaseApprovalStatus.status==status) + return query + + + @classmethod + def convert_status_to_enum(cls, status=None): + if not status: return ApprovalStatus.PENDING + showcase_status_map = {status_item.value: status_item.name for status_item in ApprovalStatus} + return ApprovalStatus[showcase_status_map[status]] + + + @classmethod + def filter_by_date(cls, query, created_start=None, created_end=None): + if created_start: + created_start = created_start.date() + query = query.filter(func.date(model.Package.metadata_created) >= created_start) + + if created_end: + created_end = created_end.date() + query = query.filter(func.date(model.Package.metadata_created) <= created_end) + + return query + + @classmethod + def filter_by_creator_user_id(cls, query, creator_user_id=None): + if creator_user_id: + query = query \ + .filter(model.Package.creator_user_id == creator_user_id) + + return query diff --git a/ckanext/showcase/plugin.py b/ckanext/showcase/plugin.py index 5797acbd..3165e2a7 100644 --- a/ckanext/showcase/plugin.py +++ b/ckanext/showcase/plugin.py @@ -18,6 +18,7 @@ import ckanext.showcase.logic.schema as showcase_schema import ckanext.showcase.logic.helpers as showcase_helpers +from ckan.common import request _ = tk._ @@ -90,11 +91,8 @@ def show_package_schema(self): # ITemplateHelpers def get_helpers(self): - return { - 'facet_remove_field': showcase_helpers.facet_remove_field, - 'get_site_statistics': showcase_helpers.get_site_statistics, - 'showcase_get_wysiwyg_editor': showcase_helpers.showcase_get_wysiwyg_editor, - } + return showcase_helpers.get_helpers() + # IFacets @@ -102,7 +100,9 @@ def dataset_facets(self, facets_dict, package_type): '''Only show tags for Showcase search list.''' if package_type != DATASET_TYPE_NAME: return facets_dict - return OrderedDict({'tags': _('Tags')}) + return OrderedDict( + # {'tags': _('Tags')} + ) # IAuthFunctions @@ -119,7 +119,7 @@ def get_actions(self): def _add_to_pkg_dict(self, context, pkg_dict): '''Add key/values to pkg_dict and return it.''' - if pkg_dict['type'] != 'showcase': + if pkg_dict['type'] != DATASET_TYPE_NAME: return pkg_dict # Add a display url for the Showcase image to the pkg dict so template @@ -139,12 +139,35 @@ def _add_to_pkg_dict(self, context, pkg_dict): tk.get_action('ckanext_showcase_package_list')( context, {'showcase_id': pkg_dict['id']})) + # ADD EXTRAS TEMP SOLUTION + for extra in pkg_dict.get('extras', {}): + pkg_dict[extra['key']] = extra['value'] + # Rendered notes if showcase_helpers.showcase_get_wysiwyg_editor() == 'ckeditor': pkg_dict['showcase_notes_formatted'] = pkg_dict['notes'] + pkg_dict['showcase_notes_formatted_ar'] = pkg_dict.get('notes_ar','') else: pkg_dict['showcase_notes_formatted'] = \ h.render_markdown(pkg_dict['notes']) + pkg_dict['showcase_notes_formatted_ar'] = \ + h.render_markdown(pkg_dict.get('notes_ar','')) + + + + current_lang = request.environ.get('CKAN_LANG', 'en') + if current_lang == 'ar': + display_title = pkg_dict.get('title_ar', '') or pkg_dict.get('title', '') or pkg_dict.get('name', '') + display_notes = pkg_dict.get('notes_ar', '') or pkg_dict.get('notes', '') + display_notes_formatted = pkg_dict.get('showcase_notes_formatted_ar', '') or pkg_dict.get('showcase_notes_formatted', '') + else: + display_title = pkg_dict.get('title', '') or pkg_dict.get('name', '') + display_notes = pkg_dict.get('notes', '') + display_notes_formatted = pkg_dict.get('showcase_notes_formatted', '') + + pkg_dict['display_title'] = display_title + pkg_dict['display_notes'] = display_notes + pkg_dict['display_notes_formatted'] = display_notes_formatted return pkg_dict @@ -169,6 +192,16 @@ def before_dataset_search(self, search_params): filter = 'dataset_type:{0}'.format(DATASET_TYPE_NAME) if filter not in fq: search_params.update({'fq': fq + " -" + filter}) + + if "+" + filter in fq: + approved_ids = utils.get_approved_showcase_ids() + q = 'id:(' + ' OR '.join(['{0}'.format(x) for x in approved_ids]) + ')' + + applied_query = search_params.get('q') + if applied_query: q+=(' AND ' + applied_query) + + search_params.update({'q':q}) + return search_params # CKAN < 2.10 (Remove when dropping support for 2.9) diff --git a/ckanext/showcase/public/images/r1.png b/ckanext/showcase/public/images/r1.png new file mode 100644 index 00000000..038e63c2 Binary files /dev/null and b/ckanext/showcase/public/images/r1.png differ diff --git a/ckanext/showcase/public/images/r2.png b/ckanext/showcase/public/images/r2.png new file mode 100644 index 00000000..45d36ce5 Binary files /dev/null and b/ckanext/showcase/public/images/r2.png differ diff --git a/ckanext/showcase/public/images/r3.png b/ckanext/showcase/public/images/r3.png new file mode 100644 index 00000000..c010a22b Binary files /dev/null and b/ckanext/showcase/public/images/r3.png differ diff --git a/ckanext/showcase/public/images/r4.png b/ckanext/showcase/public/images/r4.png new file mode 100644 index 00000000..c8168c21 Binary files /dev/null and b/ckanext/showcase/public/images/r4.png differ diff --git a/ckanext/showcase/public/images/r5.png b/ckanext/showcase/public/images/r5.png new file mode 100644 index 00000000..a2d6f117 Binary files /dev/null and b/ckanext/showcase/public/images/r5.png differ diff --git a/ckanext/showcase/public/js/showcase-actions.js b/ckanext/showcase/public/js/showcase-actions.js new file mode 100644 index 00000000..d29f832b --- /dev/null +++ b/ckanext/showcase/public/js/showcase-actions.js @@ -0,0 +1,81 @@ +ckan.module("showcase-actions", function ($) { + "use strict"; + + return { + options: { + showcaseId: null, + ajaxReload: false, + }, + + initialize: function () { + $.proxyAll(this, /_on/); + + // Attach event handlers for approve and reject buttons + this.$(".showcase-actions .approve-showcase").on( + "click", + this._onApproveShowcase + ); + this.$(".showcase-actions .reject-showcase").on( + "click", + this._onRejectShowcase + ); + }, + + _showConfirmation: function (message, callback) { + // Set the message in the modal + $('#confirmation-message').text(message); + + // Show the modal + $('#custom-confirmation-modal').modal('show'); + + // Set the callback for the confirm button + $('#confirm-button').off('click').on('click', function () { + callback(); + $('#custom-confirmation-modal').modal('hide'); // Hide the modal after confirmation + }); + }, + + _onApproveShowcase: function (e) { + var self = this; + var id = e.currentTarget.dataset.id; + var message = e.currentTarget.getAttribute("data-module-content"); + + // Show confirmation dialog + this._showConfirmation(message, function () { + self._updateShowcaseStatus(id, 'd'); + }); + }, + + _onRejectShowcase: function (e) { + var self = this; + var id = e.currentTarget.dataset.id; + var message = e.currentTarget.getAttribute("data-module-content"); + + // Show confirmation dialog + this._showConfirmation(message, function () { + self._updateShowcaseStatus(id, 'c'); + }); + }, + + _updateShowcaseStatus: function (id, status) { + var ajaxReload = this.options.ajaxReload; + + this.sandbox.client.call( + "POST", + "ckanext_showcase_status_update", + { + showcase_id: id, + status: status, + }, + function () { + if (ajaxReload) { + $(document).trigger("showcase:statusChanged"); + } else { + window.location.reload(); + } + } + ); + }, + }; + }); + \ No newline at end of file diff --git a/ckanext/showcase/public/js/showcase-conditional-fields.js b/ckanext/showcase/public/js/showcase-conditional-fields.js new file mode 100644 index 00000000..28b1d06a --- /dev/null +++ b/ckanext/showcase/public/js/showcase-conditional-fields.js @@ -0,0 +1,25 @@ +this.ckan.module('showcase-conditional-fields', function ($) { + return { + initialize: function () { + jQuery.proxyAll(this, /_on/); + this.el.ready(this._onReady); + }, + + _onReady: function () { + var statusField = $('#field-status'); + var feedback = $('#field-feedback').closest('.control-full'); + + function toggleFields(status) { + if (status === 'b' ) feedback.show(); + else feedback.hide(); + } + + toggleFields(statusField.val()); + + // On status change, show/hide fields accordingly + statusField.change(function () { + toggleFields($(this).val()); + }); + } + }; + }); \ No newline at end of file diff --git a/ckanext/showcase/templates/admin/base.html b/ckanext/showcase/templates/admin/base.html deleted file mode 100644 index eca21e2c..00000000 --- a/ckanext/showcase/templates/admin/base.html +++ /dev/null @@ -1,6 +0,0 @@ -{% ckan_extends %} - -{% block content_primary_nav %} - {{ super() }} - {{ h.build_nav_icon('showcase_blueprint.admins', _('Showcase Config'), icon='trophy') }} -{% endblock %} diff --git a/ckanext/showcase/templates/admin/confirm_remove_showcase_admin.html b/ckanext/showcase/templates/admin/confirm_remove_showcase_admin.html index 2f8c95fe..82044d75 100644 --- a/ckanext/showcase/templates/admin/confirm_remove_showcase_admin.html +++ b/ckanext/showcase/templates/admin/confirm_remove_showcase_admin.html @@ -9,7 +9,7 @@ {% block form %} - {{ _('Are you sure you want to remove this user as a Showcase Admin - {name}?').format(name=c.user_dict.name) }} + {{ _('Are you sure you want to remove this user as a Reuse Admin - {name}?').format(name=c.user_dict.name) }} {{ h.csrf_input() if 'csrf_input' in h }} diff --git a/ckanext/showcase/templates/admin/manage_showcase_admins.html b/ckanext/showcase/templates/admin/manage_showcase_admins.html index 19127533..d8a876eb 100644 --- a/ckanext/showcase/templates/admin/manage_showcase_admins.html +++ b/ckanext/showcase/templates/admin/manage_showcase_admins.html @@ -8,7 +8,7 @@ {% block primary_content_inner %} - {% block page_heading %}{{ _('Manage Showcase Admins') }}{% endblock %} + {% block page_heading %}{{ _('Manage Reuse Admins') }}{% endblock %} {% block form %} @@ -19,7 +19,7 @@ {{ _('Add an Existing User') }} - {{ _('To make an existing user a Showcase Admin, search for their username below.') }} + {{ _('To make an existing user a Reuse Admin, search for their username below.') }} @@ -38,7 +38,7 @@ {% endblock %} - {{ _('Showcase Admins') }} + {{ _('Reuse Admins') }} {% if c.showcase_admins %} @@ -51,7 +51,7 @@ {{ _('Showcase Admins') }} {{ h.linked_user(user_dict['id'], maxlength=20) }} - {% set locale = h.dump_json({'content': _('Are you sure you want to remove this user from the Showcase Admin list?')}) %} + {% set locale = h.dump_json({'content': _('Are you sure you want to remove this user from the Reuse Admin list?')}) %} {% block delete_button_text %} {{ _('Remove') }}{% endblock %} @@ -61,7 +61,7 @@ {{ _('Showcase Admins') }} {% else %} - {{ _('There are currently no Showcase Admins.') }} + {{ _('There are currently no Reuse Admins.') }} {% endif %} {% endblock %} @@ -70,7 +70,7 @@ {{ _('Showcase Admins') }} {% trans %} - Showcase Admin: Can create and remove showcases. Can add and remove datasets from showcases. + Reuse Admin: Can create and remove reuses. Can add and remove datasets from reuses. {% endtrans %} diff --git a/ckanext/showcase/templates/emails.py b/ckanext/showcase/templates/emails.py new file mode 100644 index 00000000..f6e12d0e --- /dev/null +++ b/ckanext/showcase/templates/emails.py @@ -0,0 +1,106 @@ +from ckan.common import _, config +import ckan.lib.helpers as h + + +class EmailTemplates(): + @classmethod + def site_title(cls): + return config.get('ckan.site_title') + + + @classmethod + def site_url(cls): + return config.get('ckan.site_url') + + + @classmethod + def footer_lines(cls): + return [ + _("Have a nice day."), + "---", + _(f"Message sent by { cls.site_title() } ({ cls.site_url() })") + ] + + @classmethod + def compose_email_body(cls, lines, body_vars): + user_name = body_vars['user_name'] + + text = "\n\n".join( + [_(f"Dear {user_name},")] + lines + cls.footer_lines() + ) + return text + + + @classmethod + def get_showcase_create(cls, body_vars): + showcase = body_vars['showcase'] + opening_word = body_vars['opening_word'] + action_url = h.url_for('showcase_blueprint.read', id = showcase['id']), + + + lines = [ + _(f"{opening_word} resuse case was submitted to Portal Supervisor for review."), + _(f"You can check the current status of your resuse case at {cls.site_url() + action_url[0]}"), + ] + + return cls.compose_email_body(lines, body_vars) + + + @classmethod + def get_status_update(cls, body_vars): + showcase = body_vars['showcase'] + action_url = h.url_for('showcase_blueprint.read', id = showcase['id']), + + lines = [ + _(f"Status of reuse case \'{showcase['display_title']}\' was updated to {showcase['status']}."), + _(f"You can check the current status of resuse at {cls.site_url() + action_url[0]}"), + ] + + return cls.compose_email_body(lines, body_vars) + + +class SubjectTemplates(): + @classmethod + def get_showcase_create(cls, body_vars): + showcase = body_vars['showcase'] + + return _(f"Reuse case \'{showcase['display_title']}\' was submitted for review.") + + + @classmethod + def get_status_update(cls, body_vars): + showcase = body_vars['showcase'] + + return _(f"Reuse case \'{showcase['display_title']}\' status was updated."), + + + +class NotificationTemplates(): + @classmethod + def compose_email_body(cls, lines, body_vars): + text = "\n\n".join(lines) + return text + + + @classmethod + def get_showcase_create(cls, body_vars): + showcase = body_vars['showcase'] + opening_word = body_vars['opening_word'] + + + lines = [ + _(f"{opening_word} resuse case was submitted to the Portal Supervisor for review."), + ] + + return cls.compose_email_body(lines, body_vars) + + @classmethod + def get_status_update(cls, body_vars): + showcase = body_vars['showcase'] + + lines = [ + _(f"Status of reuse case \'{showcase['display_title']}\' was updated to {showcase['status']}."), + ] + + return cls.compose_email_body(lines, body_vars) + diff --git a/ckanext/showcase/templates/header.html b/ckanext/showcase/templates/header.html index 5637a587..2ed06c52 100644 --- a/ckanext/showcase/templates/header.html +++ b/ckanext/showcase/templates/header.html @@ -4,5 +4,5 @@ {% block header_site_navigation_tabs %} {{ super() }} - {{ h.build_nav_main((showcase_route, _('Showcases'))) }} + {{ h.build_nav_main((showcase_route, _('Reuses'))) }} {% endblock %} diff --git a/ckanext/showcase/templates/package/dataset_showcase_list.html b/ckanext/showcase/templates/package/dataset_showcase_list.html index b06c6e5f..54193292 100644 --- a/ckanext/showcase/templates/package/dataset_showcase_list.html +++ b/ckanext/showcase/templates/package/dataset_showcase_list.html @@ -1,9 +1,9 @@ {% extends "package/read_base.html" %} -{% block subtitle %}{{ _('Showcases') }} - {{ super() }}{% endblock %} +{% block subtitle %}{{ _('Reuses') }} - {{ super() }}{% endblock %} {% block primary_content_inner %} - {% if h.check_access('ckanext_showcase_update') and c.showcase_dropdown %} + {% if h.check_access('ckanext_showcase_delete') and c.showcase_dropdown %} {{ h.csrf_input() if 'csrf_input' in h }} @@ -11,16 +11,16 @@ {{ option[1] }} {% endfor %} - {{ _('Add to showcase') }} + {{ _('Add to reuse') }} {% endif %} - {% block page_heading %}{{ _('Showcases featuring {dataset_name}').format(dataset_name=h.dataset_display_name(c.pkg_dict)) }}{% endblock %} + {% block page_heading %}{{ _('Reuses featuring {dataset_name}').format(dataset_name=h.dataset_display_name(c.pkg_dict)) }}{% endblock %} {% block showcase_list %} {% if c.showcase_list %} - {% snippet "showcase/snippets/showcase_list.html", packages=c.showcase_list, pkg_id=c.pkg_dict.name, show_remove=h.check_access('ckanext_showcase_update') %} + {% snippet "showcase/snippets/showcase_list.html", packages=c.showcase_list, pkg_id=c.pkg_dict.name, show_remove=h.check_access('ckanext_showcase_delete') %} {% else %} - {{ _('There are no showcases that feature this dataset') }} + {{ _('There are no reuses that feature this dataset') }} {% endif %} {% endblock %} {% endblock %} diff --git a/ckanext/showcase/templates/package/read_base.html b/ckanext/showcase/templates/package/read_base.html index a72655cc..d2ccaf79 100644 --- a/ckanext/showcase/templates/package/read_base.html +++ b/ckanext/showcase/templates/package/read_base.html @@ -3,5 +3,5 @@ {% block content_primary_nav %} {{ super() }} - {{ h.build_nav_icon(showcase_dataset_showcase_list_route, _('Showcases'), id=pkg.name, icon='trophy') }} + {{ h.build_nav_icon(showcase_dataset_showcase_list_route, _('Reuses'), id=pkg.name, icon='trophy') }} {% endblock %} diff --git a/ckanext/showcase/templates/showcase/add_datasets.html b/ckanext/showcase/templates/showcase/add_datasets.html index 7a4d0fef..12b6627a 100644 --- a/ckanext/showcase/templates/showcase/add_datasets.html +++ b/ckanext/showcase/templates/showcase/add_datasets.html @@ -1,6 +1,6 @@ {% extends 'showcase/edit_base.html' %} -{% block subtitle %}{{ _('Showcases - Add datasets') }}{% endblock %} +{% block subtitle %}{{ _('Reuses - Add datasets') }}{% endblock %} {% block wrapper_class %} ckanext-showcase-edit-wrapper{% endblock %} @@ -28,7 +28,7 @@ {% endblock %} {% block page_heading %} - {{ _('Datasets available to add to this showcase') }} + {{ _('Datasets available to add to this reuse') }} {% endblock %} {% block package_search_results_list %} @@ -46,7 +46,7 @@ - {{ _('Add to Showcase') }} + {{ _('Add to Reuse') }} diff --git a/ckanext/showcase/templates/showcase/confirm_delete.html b/ckanext/showcase/templates/showcase/confirm_delete.html index 82369a4b..e81baeb2 100644 --- a/ckanext/showcase/templates/showcase/confirm_delete.html +++ b/ckanext/showcase/templates/showcase/confirm_delete.html @@ -13,7 +13,7 @@ {% block form %} - {{ _('Are you sure you want to delete showcase - {showcase_name}?').format(showcase_name=pkg.name) }} + {{ _('Are you sure you want to delete reuse - {showcase_name}?').format(showcase_name=pkg.name) }} {{ h.csrf_input() if 'csrf_input' in h }} diff --git a/ckanext/showcase/templates/showcase/dashboard/actions.html b/ckanext/showcase/templates/showcase/dashboard/actions.html new file mode 100644 index 00000000..839ffc3e --- /dev/null +++ b/ckanext/showcase/templates/showcase/dashboard/actions.html @@ -0,0 +1,175 @@ +{% import 'macros/autoform.html' as autoform %} +{% extends "page.html" %} + +{% import 'macros/form.html' as form %} + +{% set showcase_list_route = 'showcase_blueprint.dashboard_index' %} +{% set showcase_read_route = 'showcase_blueprint.dashboard_read' %} +{% set showcase_update_route = 'showcase_blueprint.dashboard_update' %} + +{% block styles %} + {{super()}} + +{% endblock %} + +{% block subtitle %}{{ _('Reuse Case Management') }}{% endblock %} + +{% block breadcrumb_content %} +{% link_for _('Reuse Cases'), named_route=showcase_list_route %} + {% if showcase %} + {% set ShowcaseTitle = showcase.display_title %} + {% link_for ShowcaseTitle|truncate(30), named_route=showcase_read_route, id=showcase.id %} + {% endif %} +{% endblock %} + +{% block page_header %} + {{ super() }} +{% endblock %} + +{% block content_action %} + {% link_for _('View'), named_route=showcase_read_route, id=showcase.id, class_='btn btn-default', icon='eye' %} +{% endblock %} + +{% block content_primary_nav %} + {{ h.build_nav_icon(showcase_update_route, _('Reuse Case Actions'), id=showcase.id, icon='wrench') }} +{% endblock %} + + +{% block primary_content_inner %} + +{{ _('Manage Reuse Case') }} + + {{ h.csrf_input() if 'csrf_input' in h }} + + {% block errors %}{{ form.errors(error_summary) }}{% endblock %} + + {{ form.input('title', id='field-title', label=_('Title En'), value=showcase.title, classes=["control-full"], attrs={"disabled": "disabled"}) }} + {{ form.input('title_ar', id='field-title_ar', label=_('Title Ar'), value=showcase.title_ar, classes=["control-full"], attrs={"disabled": "disabled"}) }} + + {{ form.markdown('notes', id='field-notes', label=_('Description English'), value=showcase.notes, error=errors.notes, attrs={"disabled": "disabled"}) }} + {{ form.markdown('notes_ar', id='field-notes_ar', label=_('Description Arabic'), value=showcase.notes_ar, error=errors.notes_ar, attrs={"disabled": "disabled"}) }} + + + {{ form.input('author', label=_('Submitted By'), id='field-author', value=showcase.author, error=errors.author, classes=['control-full'], attrs={"disabled": "disabled"}) }} + {{ form.input('author_email', label=_('Submitter Email'), id='field-author-email', value=showcase.author_email, error=errors.author_email, classes=['control-full'], attrs={"disabled": "disabled"}) }} + + + + + {{ form.input('url', label=_('External link'), id='field-url', value=showcase.url, error=errors.url, classes=['control-full'], attrs={"disabled": "disabled"}) }} + + + + {{ _('Visit External Link') }} + + + + + {% if showcase_datasets %} + + {{_('Associated Datasets')}} + {% for dataset in showcase_datasets %} + + + + + + + + + + + {{ _('Go to Dataset') }} + + + + {% endfor %} + + {% endif %} + + + {% if showcase.image_display_url %} + + {{_('Reuse Case Image')}} + + + + + + + + + {% endif %} + + + + + + {{ _("Reuse Type") }} + + + + {% set existing_types = showcase.get('reuse_type', []) %} + {% set missing_error = _('Missing value') %} + {% set empty_group = _('Please select the Reuse Type') %} + {{ empty_group }} + {% for reuse_type in h.ckanext_showcase_types() %} + + {{ reuse_type.text }} + + {% endfor %} + + + + + {% if h.check_access('ckanext_showcase_status_show', {'id': showcase.id}) %} + {{ form.select('status_old', id='field-status_old', label=_('Reuse Case Status'), options=h.showcase_status_options(), selected=showcase_status.status, classes=["control-full"], attrs={"disabled": "disabled"}) }} + + {% if showcase_status.feedback %} + {{ form.textarea('feedback_old', id='field-feedback_old', label=_('Admin feedback'), placeholder=_('Admin feedback'), value=showcase_status.feedback, classes=["control-full"], attrs={"disabled": "disabled"}) }} + {% endif %} + + {% endif %} + + + + + +{{_('Reuse Case Actions')}} +{% if h.check_access('ckanext_showcase_status_update', {'id': data.id}) %} +{% asset 'showcase/showcase-conditional-fields' %} + + {{ form.select('status', id='field-status', label=_('Reuse Case Status'), options=h.showcase_status_options(), selected=data.status, classes=["control-full"], is_required=true) }} + {{ form.textarea('feedback', id='field-feedback', label=_('Admin feedback'), placeholder=_('Admin feedback'), value=data.feedback, classes=["control-full"], is_required=true) }} + + + {{ _('Update Status') }} + + +{% endif %} + + + + {{ _('Back') }} + + +{% endblock %} + + +{% block scripts %} + {{ super() }} +{% endblock %} + +{% block secondary_content %} + {% snippet "showcase/dashboard/snippets/user_info.html", user_info = user_info, submitted_date=showcase.metadata_created %} +{% endblock %} \ No newline at end of file diff --git a/ckanext/showcase/templates/showcase/dashboard/detail.html b/ckanext/showcase/templates/showcase/dashboard/detail.html new file mode 100644 index 00000000..ce25f998 --- /dev/null +++ b/ckanext/showcase/templates/showcase/dashboard/detail.html @@ -0,0 +1,79 @@ +{% extends "page.html" %} + +{% set showcase_list_route = 'showcase_blueprint.dashboard_index' %} +{% set showcase_read_route = 'showcase_blueprint.dashboard_read' %} +{% set showcase_update_route = 'showcase_blueprint.dashboard_update' %} +{% set editor = h.showcase_get_wysiwyg_editor() %} +{% set display_title = showcase.display_title %} +{% set display_notes = showcase.display_notes %} +{% set display_notes_formatted = showcase.display_notes_formatted %} + +{% block styles %} + {{super()}} + +{% endblock %} + +{% block subtitle %}{{ _('Reuse Case Management') }}{% endblock %} + +{% block breadcrumb_content %} +{% link_for _('Reuse Cases'), named_route=showcase_list_route %} + {% if showcase %} + {% set ShowcaseTitle = showcase.display_title %} + {% link_for ShowcaseTitle|truncate(30), named_route=showcase_read_route, id=showcase.id %} + {% endif %} +{% endblock %} + +{% block content_action %} + {% if h.check_access('ckanext_showcase_status_show', {'id': showcase.id}) %} + {% link_for _('Take Action'), named_route=showcase_update_route, id=showcase.id, class_='btn btn-default', icon='wrench' %} + {% endif %} +{% endblock %} + +{% block content_primary_nav %} + {{ h.build_nav_icon(showcase_read_route, _('Reuse Case Details'), id=showcase.id, icon='wrench') }} +{% endblock %} + + +{% block primary_content_inner %} + + + {{ showcase.approval_status.display_status }} + + + {% block page_heading %} + {{ display_title }} + {% if showcase.state.startswith('draft') %} + [{{ _('Draft') }}] + {% endif %} + {% endblock %} + + + {% if showcase.image_display_url %} + + + + {% endif %} + + + {% block package_notes %} + {% if display_notes_formatted and editor == 'ckeditor' %} + + {{ display_notes_formatted|safe }} + + {% elif display_notes_formatted %} + + {{ display_notes_formatted }} + + {% endif %} + {% endblock %} + + + {% set metadata = h.ckanext_showcase_metatdata(showcase, showcase_datasets, user_info) %} + {% snippet 'package/snippets/metadata.html', extras = metadata, table_title = _('Showcase Metadata') %} + +{% endblock %} + + +{% block secondary_content %} + {% snippet "showcase/dashboard/snippets/user_info.html", user_info = user_info, submitted_date=showcase.metadata_created %} +{% endblock %} diff --git a/ckanext/showcase/templates/showcase/dashboard/search.html b/ckanext/showcase/templates/showcase/dashboard/search.html new file mode 100644 index 00000000..e0b6d827 --- /dev/null +++ b/ckanext/showcase/templates/showcase/dashboard/search.html @@ -0,0 +1,69 @@ +{% extends "package/search.html" %} +{% import 'macros/form.html' as form %} + +{% set showcase_list_route = 'showcase_blueprint.dashboard_index' %} +{% set showcase_read_route = 'showcase_blueprint.read' %} +{% set showcase_update_route = 'showcase_blueprint.dashboard_update' %} +{% set showcase_new_route = 'showcase_blueprint.new' %} + +{% block styles %} + {{super()}} + +{% endblock %} + + +{% block subtitle %}{{ _('Reuse Cases') }}{% endblock %} + +{% block breadcrumb_content %} + {{ _('User Dashboard') }} + {% link_for _('Reuse Cases'), named_route=showcase_list_route %} +{% endblock %} + +{% block page_primary_action %} + {% if h.check_access('ckanext_showcase_create') %} + + {% link_for _('Submit Reuse Case'), named_route=showcase_new_route, class_='btn btn-primary', icon='plus-square' %} + + {% endif %} + + {% snippet 'showcase/dashboard/snippets/stats-indicators.html', statistics=statistics %} +{% endblock %} + +{% block form %} + {% set sorting = [ + (_('Name Ascending'), 'title asc'), + (_('Name Descending'), 'title desc'), + (_('Last Created'), 'metadata_created desc'), + (_('Last Status Update'), 'status_modified desc'), + ] + %} + {% + snippet 'showcase/dashboard/snippets/showcase_search_form.html', + placeholder=_('Search reuse cases...'), + query=data_dict.q, + sorting=sorting, + sorting_selected=selected_sorting, + count=count, + show_empty=request.args, + error=data_dict.errors, + no_bottom_border=true, + status_options=h.showcase_status_filter_options(), + created_start=data_dict.created_start or '', + created_end=data_dict.created_end or '', + status_selected=data_dict.status or '' + %} + +{% endblock %} + +{% block package_search_results_list %} + {{ h.snippet('showcase/dashboard/snippets/list.html', showcases=showcases) }} +{% endblock %} + + +{% block package_search_results_api %} +{% endblock %} + + +{% block secondary_content %} + {% snippet "showcase/dashboard/snippets/helper.html" %} +{% endblock %} \ No newline at end of file diff --git a/ckanext/showcase/templates/showcase/dashboard/snippets/confirmation-modal.html b/ckanext/showcase/templates/showcase/dashboard/snippets/confirmation-modal.html new file mode 100644 index 00000000..b8435d9d --- /dev/null +++ b/ckanext/showcase/templates/showcase/dashboard/snippets/confirmation-modal.html @@ -0,0 +1,19 @@ + + + + + {{_('Confirmation')}} + + × + + + + + + + + + \ No newline at end of file diff --git a/ckanext/showcase/templates/showcase/dashboard/snippets/helper.html b/ckanext/showcase/templates/showcase/dashboard/snippets/helper.html new file mode 100644 index 00000000..7a9d7f93 --- /dev/null +++ b/ckanext/showcase/templates/showcase/dashboard/snippets/helper.html @@ -0,0 +1,13 @@ + + + + {{ _('What are Data Requests?') }} + + + + {% trans %} + The "Data Request" feature allows users to request datasets that are not yet available in the portal. Whether you're looking for specific data to support your research, project, or decision-making process, you can submit a request directly to the data providers. This feature enables collaboration between the public and data publishers, helping to expand the portal’s dataset offerings. Simply submit your request and track its progress as data providers work to make the requested information available. + {% endtrans %} + + + \ No newline at end of file diff --git a/ckanext/showcase/templates/showcase/dashboard/snippets/list.html b/ckanext/showcase/templates/showcase/dashboard/snippets/list.html new file mode 100644 index 00000000..20dd3980 --- /dev/null +++ b/ckanext/showcase/templates/showcase/dashboard/snippets/list.html @@ -0,0 +1,46 @@ + +{% block showcases_list %} +{% if showcases %} +{% asset 'showcase/showcase-actions' %} + + + + + + {{_('Date')}} + {{_('Use Case Title')}} + {{_('The User')}} + {{_('Status')}} + {{_('Actions')}} + + + + + {% block showcase_list_inner %} + {% for showcase in showcases %} + {% + snippet + 'showcase/dashboard/snippets/showcase_item.html', + showcase=showcase, + item_class=item_class, + truncate=truncate, + truncate_title=truncate_title, + show_remove=show_remove + %} + {% endfor %} + {% endblock %} + + + + {% if show_more %} + + {{_('View All')}} + + {% endif %} + + +{% endif %} +{% endblock %} + + +{% snippet 'showcase/dashboard/snippets/confirmation-modal.html' %} diff --git a/ckanext/showcase/templates/showcase/dashboard/snippets/showcase_item.html b/ckanext/showcase/templates/showcase/dashboard/snippets/showcase_item.html new file mode 100644 index 00000000..d499c6c8 --- /dev/null +++ b/ckanext/showcase/templates/showcase/dashboard/snippets/showcase_item.html @@ -0,0 +1,51 @@ +{% set lang = h.lang() %} +{% set showcase_list_route = 'showcase_blueprint.dashboard_index' %} +{% set showcase_read_route = 'showcase_blueprint.read' %} +{% set showcase_update_route = 'showcase_blueprint.dashboard_update' %} +{% set showcase_approve_route = 'showcase_blueprint.approve' %} +{% set showcase_reject_route = 'showcase_blueprint.reject' %} + + + + {{h.render_datetime(showcase.metadata_created, date_format='%d/%m/%Y')}} + {{ showcase.display_title }} + {{ showcase.creator.fullname }} + {{ showcase.approval_status.display_status }} + + + + {% if showcase.approval_status.status != 'd' %} + + + + {% endif %} + + {% if showcase.approval_status.status != 'c' %} + + + + {% endif %} + + + + + + + + {{_('More Details')}} + + + + \ No newline at end of file diff --git a/ckanext/showcase/templates/showcase/dashboard/snippets/showcase_search_form.html b/ckanext/showcase/templates/showcase/dashboard/snippets/showcase_search_form.html new file mode 100644 index 00000000..58820f1d --- /dev/null +++ b/ckanext/showcase/templates/showcase/dashboard/snippets/showcase_search_form.html @@ -0,0 +1,168 @@ +{% extends "snippets/search_form.html" %} + +{% block search_title %} + {% if not no_title %} + {% snippet 'showcase/dashboard/snippets/showcase_search_result_text.html', query=query, count=count %} + {% endif %} +{% endblock %} + + + +{% block search_input %} + {{ _('Reuse Cases') }} + {{ super() }} +{% endblock %} + + +{% block search_sortby %} +{% set created_start_formatted = created_start.strftime('%Y-%m-%d') if created_start else '' %} +{% set created_end_formatted = created_end.strftime('%Y-%m-%d') if created_end else '' %} + + + + + {{_('Filter By')}} + + + + {% if status_selected %} + + + {{ _('Clear Status') }} + + + + + + {% endif %} + + {% if created_start %} + + + {{ _('Clear Start Date') }} + + + + + + {% endif %} + + {% if created_end %} + + + {{ _('Clear End Date') }} + + + + + + {% endif %} + + + + + {% if status_selected or created_start or created_end %} + + + {{_('Clear Filters')}} + + + + + + {% endif %} + + + + + + {% if status_options %} + + {{_('Status')}} + + + + {% for status in status_options %} + + {{ status.text|truncate(30) }} + + {% endfor %} + + + + {% endif %} + + + {{_('From Date')}} + + + + + + + {{_('To Date')}} + + + + + + + {% if sorting %} + + {{_('Order By')}} + + + {% for label, value in sorting %} + {% if label and value %} + + {{ label }} + + {% endif %} + {% endfor %} + + + + + {% endif %} + + + + + +{% endblock %} \ No newline at end of file diff --git a/ckanext/showcase/templates/showcase/dashboard/snippets/showcase_search_result_text.html b/ckanext/showcase/templates/showcase/dashboard/snippets/showcase_search_result_text.html new file mode 100644 index 00000000..31ebde92 --- /dev/null +++ b/ckanext/showcase/templates/showcase/dashboard/snippets/showcase_search_result_text.html @@ -0,0 +1,19 @@ + +{% set text_query = ungettext("{number} reuse case found for '{query}'", "{number} reuse cases found for '{query}'", count) %} +{% set text_query_none = _("No reuse cases found for '{query}'") %} +{% set text_no_query = ungettext("{number} reuse case found", "{number} reuse cases found", count) %} +{% set text_no_query_none = _("No reuse cases found") %} + +{% if query %} + {%- if count -%} + {{ text_query.format(number=h.localised_number(count), query=query) }} + {%- else -%} + {{ text_query_none.format(query=query) }} + {%- endif -%} +{%- else -%} + {%- if count -%} + {{ text_no_query.format(number=h.localised_number(count)) }} + {%- else -%} + {{ text_no_query_none }} + {%- endif -%} +{%- endif -%} diff --git a/ckanext/showcase/templates/showcase/dashboard/snippets/stats-indicators.html b/ckanext/showcase/templates/showcase/dashboard/snippets/stats-indicators.html new file mode 100644 index 00000000..523ba209 --- /dev/null +++ b/ckanext/showcase/templates/showcase/dashboard/snippets/stats-indicators.html @@ -0,0 +1,61 @@ + + + + + {{_('Total')}} + + {{ statistics.total or 0 }} + + + + + + + + {{_('Pending')}} + + {{ statistics.status_breakdown.a or 0 }} + + + + + + + + + {{_('Rejected')}} + + {{ statistics.status_breakdown.c or 0 }} + + + + + + + + + + + {{_('Needs Revision')}} + + {{ statistics.status_breakdown.b or 0 }} + + + + + + + + + + + {{_('Approved')}} + + {{ statistics.status_breakdown.d or 0 }} + + + + + + + diff --git a/ckanext/showcase/templates/showcase/dashboard/snippets/user_info.html b/ckanext/showcase/templates/showcase/dashboard/snippets/user_info.html new file mode 100644 index 00000000..c1cb52c9 --- /dev/null +++ b/ckanext/showcase/templates/showcase/dashboard/snippets/user_info.html @@ -0,0 +1,21 @@ + + + + {{ _('The User Information') }} + + + {{ h.user_image(user_info.user_dict.id, size=270) }} + {{ user_info.user_dict.display_name }} + {% if submitted_date %} + + + {{ _('Submitted on') }} + {{ h.date_str_to_datetime(submitted_date).strftime('%d/%m/%Y') }} + + {% endif %} + + {% if user_info.org_dict and not user_info.user_dict.sysadmin %} + {{user_info.org_dict.display_name}} + {% endif %} + + \ No newline at end of file diff --git a/ckanext/showcase/templates/showcase/edit_base.html b/ckanext/showcase/templates/showcase/edit_base.html index 6e25d8ed..22144b10 100644 --- a/ckanext/showcase/templates/showcase/edit_base.html +++ b/ckanext/showcase/templates/showcase/edit_base.html @@ -9,7 +9,7 @@ -{% block subtitle %}{{ _('Showcases') }}{% endblock %} +{% block subtitle %}{{ _('Reuses') }}{% endblock %} {% block styles %} {{ super() }} @@ -21,12 +21,12 @@ {% block breadcrumb_content %} {% if pkg %} {% set showcase = pkg.title or pkg.name %} - {% link_for _('Showcases'), named_route=showcase_index_route %} + {% link_for _('Reuses'), named_route=showcase_index_route %} {% link_for showcase|truncate(30), named_route=showcase_read_route, id=pkg.name %} {% link_for _('Edit'), named_route=showcase_edit_route, id=pkg.name %} {% else %} - {% link_for _('Showcases'), named_route=showcase_index_route %} - {{ _('Create Showcase') }} + {% link_for _('Reuses'), named_route=showcase_index_route %} + {{ _('Submit Reuse') }} {% endif %} {% endblock %} @@ -39,13 +39,13 @@ {% if self.content_action() | trim %} {% block content_action %} - {% link_for _('View showcase'), named_route=showcase_read_route, id=pkg.name, class_='btn', icon='eye-open' %} + {% link_for _('View reuse'), named_route=showcase_read_route, id=pkg.name, class_='btn', icon='eye-open' %} {% endblock %} {% endif %} {% block content_primary_nav %} - {{ h.build_nav_icon(showcase_edit_route, _('Edit showcase'), id=pkg.name) }} + {{ h.build_nav_icon(showcase_edit_route, _('Edit reuse'), id=pkg.name) }} {{ h.build_nav_icon(showcase_manage_route, _('Manage datasets'), id=pkg.name) }} {% endblock %} diff --git a/ckanext/showcase/templates/showcase/manage_datasets.html b/ckanext/showcase/templates/showcase/manage_datasets.html index 92ac5add..e5d20135 100644 --- a/ckanext/showcase/templates/showcase/manage_datasets.html +++ b/ckanext/showcase/templates/showcase/manage_datasets.html @@ -1,6 +1,6 @@ {% extends 'showcase/edit_base.html' %} -{% block subtitle %}{{ _('Showcases - Manage datasets') }}{% endblock %} +{% block subtitle %}{{ _('Reuses - Manage datasets') }}{% endblock %} {% block wrapper_class %} ckanext-showcase-edit-wrapper{% endblock %} @@ -33,7 +33,7 @@ - {{ _('Datasets available to add to this showcase') }} + {{ _('Datasets available to add to this reuse') }} {% block package_search_results_list %} {% if c.page.items %} @@ -49,7 +49,7 @@ {{ _('Datasets available to add to this showcase') }} - {{ _('Add to Showcase') }} + {{ _('Add to Reuse') }} @@ -94,7 +94,7 @@ - {{ _('Datasets in this showcase') }} + {{ _('Datasets in this reuse') }} {% if c.showcase_pkgs %} {{ h.csrf_input() if 'csrf_input' in h }} @@ -108,7 +108,7 @@ {{ _('Datasets in this showcase') }} - {{ _('Remove from Showcase') }} + {{ _('Remove from Reuse') }} @@ -139,7 +139,7 @@ {% else %} - {{ _('This showcase has no datasets associated to it') }}. + {{ _('This reuse has no datasets associated to it') }}. {% endif %} diff --git a/ckanext/showcase/templates/showcase/new.html b/ckanext/showcase/templates/showcase/new.html index df639d65..dff1e9cc 100644 --- a/ckanext/showcase/templates/showcase/new.html +++ b/ckanext/showcase/templates/showcase/new.html @@ -1,11 +1,11 @@ {% extends "showcase/edit_base.html" %} -{% block subtitle %}{{ _('Create Showcase') }}{% endblock %} +{% block subtitle %}{{ _('Submit Reuse') }}{% endblock %} {% block primary_content %} - {% block page_heading %}{{ _('Create a Showcase') }}{% endblock %} + {% block page_heading %}{{ _('Submit a Reuse') }}{% endblock %} {% block primary_content_inner %} {% block form %} {#- passing c to a snippet is bad but is required here diff --git a/ckanext/showcase/templates/showcase/new_package_form.html b/ckanext/showcase/templates/showcase/new_package_form.html index d1ca93a2..f92e4527 100644 --- a/ckanext/showcase/templates/showcase/new_package_form.html +++ b/ckanext/showcase/templates/showcase/new_package_form.html @@ -4,7 +4,6 @@ {% set showcase_read_route = 'showcase_blueprint.read' %} {% set showcase_delete_route = 'showcase_blueprint.delete' %} - {{ h.csrf_input() if 'csrf_input' in h }} @@ -15,7 +14,8 @@ {% block basic_fields %} {% block package_basic_fields_title %} - {{ form.input('title', id='field-title', label=_('Title'), placeholder=_('eg. A descriptive title'), value=data.title, error=errors.title, classes=['control-full', 'control-large'], attrs={'data-module': 'slug-preview-target'}) }} + {{ form.input('title', id='field-title', label=_('Title English'), placeholder=_('eg. A descriptive title in English'), value=data.title, error=errors.title, classes=['control-full', 'control-large'], attrs={'data-module': 'slug-preview-target'}, is_required=true) }} + {{ form.input('title_ar', id='field-title_ar', label=_('Title Arabic'), placeholder=_('eg. A descriptive title in Arabic'), value=data.title_ar, error=errors.title_ar, is_required=true) }} {% endblock %} {% block package_basic_fields_url %} @@ -32,20 +32,28 @@ {% if editor == 'ckeditor' %} {% asset 'showcase/ckeditor' %} {% - set ckeditor_attrs = { + set ckeditor_attrs_en = { + 'data-module': 'showcase-ckeditor', + 'data-editor': 'editor_en', + 'data-lang': h.lang(), + 'data-module-site_url': h.dump_json(h.url_for('/', locale='default', qualified=true))} + %} + {{ form.textarea('notes', id='editor_en', label=_('Description English'), placeholder=_('eg. Some useful notes about the data'), value=data.notes, error=errors.notes, attrs=ckeditor_attrs_en, is_required=true)}} + + {% + set ckeditor_attrs_ar = { 'data-module': 'showcase-ckeditor', - 'data-module-site_url': h.url_for('/', qualified=true)} + 'data-editor': 'editor_ar', + 'data-lang': h.lang(), + 'data-module-site_url': h.dump_json(h.url_for('/', locale='default', qualified=true))} %} - {{ form.textarea('notes', id='editor', label=_('Description'), placeholder=_('eg. Some useful notes about the data'), value=data.notes, error=errors.notes, attrs=ckeditor_attrs)}} + {{ form.textarea('notes_ar', id='editor_ar', label=_('Description Arabic'), placeholder=_('eg. Some useful notes about the data'), value=data.notes_ar, error=errors.notes_ar, attrs=ckeditor_attrs_ar, is_required=true)}} {% else %} - {{ form.markdown('notes', id='field-notes', label=_('Description'), placeholder=_('eg. Some useful notes about the data'), value=data.notes, error=errors.notes) }} + {{ form.markdown('notes', id='field-notes', label=_('Description English'), placeholder=_('eg. Some useful notes about the data'), value=data.notes, error=errors.notes) }} + {{ form.markdown('notes_ar', id='field-notes_ar', label=_('Description Arabic'), placeholder=_('eg. Some useful notes about the data'), value=data.notes_ar, error=errors.notes_ar) }} {% endif %} {% endblock %} - {% block package_basic_fields_tags %} - {% set tag_attrs = {'data-module': 'autocomplete', 'data-module-tags': '', 'data-module-source': '/api/2/util/tag/autocomplete?incomplete=?'} %} - {{ form.input('tag_string', id='field-tags', label=_('Tags'), placeholder=_('eg. economy, mental health, government'), value=data.tag_string, error=errors.tags, classes=['control-full'], attrs=tag_attrs) }} - {% endblock %} {% set is_upload = data.image_url and not data.image_url.startswith('http') %} {% set is_url = data.image_url and data.image_url.startswith('http') %} @@ -58,6 +66,34 @@ {{ form.input('url', label=_('External link'), id='field-url', placeholder=_('http://www.example.com'), value=data.url, error=errors.url, classes=['control-medium']) }} {% endblock %} + {% block package_metadata_type_field %} + + + {{ _("Reuse Type") }} + + + + {% set existing_types = data.get('reuse_type', []) %} + {% set missing_error = _('Missing value') %} + {% set empty_group = _('Please select the Reuse Type') %} + {{ empty_group }} + {% for reuse_type in h.ckanext_showcase_types() %} + + {{ reuse_type.text }} + + {% endfor %} + + {% for error in errors.reuse_type %} + {{error}} + {% endfor %} + + + {% endblock %} + {% block package_metadata_author %} {{ form.input('author', label=_('Submitted By'), id='field-author', placeholder=_('Joe Bloggs'), value=data.author, error=errors.author, classes=['control-medium']) }} @@ -68,13 +104,13 @@ {% block form_actions %} {% block delete_button %} - {% if form_style == 'edit' and h.check_access('ckanext_showcase_delete', {'id': data.id}) and not data.state == 'deleted' %} - {% set locale = h.dump_json({'content': _('Are you sure you want to delete this showcase?')}) %} + {% if h.check_access('ckanext_showcase_delete', {'id': data.id}) and not data.state == 'deleted' %} + {% set locale = h.dump_json({'content': _('Are you sure you want to delete this reuse?')}) %} {% block delete_button_text %}{{ _('Delete') }}{% endblock %} {% endif %} {% endblock %} {% block save_button %} - {% block save_button_text %}{% if form_style != 'edit' %}{{ _('Create Showcase') }}{% else %}{{ _('Update Showcase') }}{% endif %}{% endblock %} + {% block save_button_text %}{% if form_style != 'edit' %}{{ _('Submit Reuse') }}{% else %}{{ _('Update Reuse') }}{% endif %}{% endblock %} {% endblock %} {{ form.required_message() }} diff --git a/ckanext/showcase/templates/showcase/read.html b/ckanext/showcase/templates/showcase/read.html index f3b5f051..0b90b9d6 100644 --- a/ckanext/showcase/templates/showcase/read.html +++ b/ckanext/showcase/templates/showcase/read.html @@ -1,14 +1,16 @@ {% extends "page.html" %} {% set pkg = pkg_dict or c.pkg_dict %} -{% set name = pkg.title or pkg.name %} +{% set display_title = pkg.display_title %} +{% set display_notes = pkg.display_notes %} +{% set display_notes_formatted = pkg.display_notes_formatted %} {% set editor = h.showcase_get_wysiwyg_editor() %} {% set showcase_read_route = 'showcase_blueprint.read' %} {% set showcase_index_route = 'showcase_blueprint.index' %} {% set showcase_edit_route = 'showcase_blueprint.edit' %} -{% block subtitle %}{{ pkg.title or pkg.name }} - {{ _('Showcases') }}{% endblock %} +{% block subtitle %}{{ display_title }} - {{ _('Reuses') }}{% endblock %} {% block styles %} {{ super() }} @@ -25,7 +27,7 @@ {% block head_extras -%} {{ super() }} - {% set description = h.markdown_extract(pkg.notes, extract_length=200)|forceescape %} + {% set description = h.markdown_extract(display_notes, extract_length=200)|forceescape %} @@ -37,33 +39,37 @@ {% block breadcrumb_content_selected %} class="active"{% endblock %} {% block breadcrumb_content %} - {% set showcase = pkg.title or pkg.name %} - {{ h.nav_link(_('Showcases'), named_route=showcase_index_route, highlight_actions = 'new index') }} + {% set showcase = display_title %} + {{ h.nav_link(_('Reuses'), named_route=showcase_index_route) }} {% link_for showcase|truncate(30), named_route=showcase_read_route, id=pkg.name %} {% endblock %} -{% block page_header %} + +{% block content_primary_nav %} + {{ h.build_nav_icon(showcase_read_route, _('Reuse Details'), id=pkg.id, icon='info-circle') }} {% endblock %} {% block pre_primary %} {% endblock %} -{% block primary_content_inner %} + +{% block content_action %} {% if h.check_access('ckanext_showcase_update', {'id':pkg.id }) %} - - {% link_for _('Manage'), named_route=showcase_edit_route, id=pkg.name, class_='btn btn-default', icon='wrench' %} - + {% link_for _('Manage'), named_route=showcase_edit_route, id=pkg.name, class_='btn btn-default', icon='wrench' %} {% endif %} +{% endblock %} + +{% block primary_content_inner %} {% block package_description %} - {% if pkg.private %} + {% if pkg.approval_status.status != 'd' %} - - {{ _('Private') }} + + {{ pkg.approval_status.display_status }} {% endif %} {% block page_heading %} - {{ name }} + {{ display_title }} {% if pkg.state.startswith('draft') %} [{{ _('Draft') }}] {% endif %} @@ -71,17 +77,17 @@ {% if pkg.image_display_url %} - + {% endif %} {% block package_notes %} - {% if pkg.showcase_notes_formatted and editor == 'ckeditor' %} + {% if display_notes_formatted and editor == 'ckeditor' %} - {{ pkg.showcase_notes_formatted|safe }} + {{ display_notes_formatted|safe }} - {% elif pkg.showcase_notes_formatted %} + {% elif display_notes_formatted %} - {{ pkg.showcase_notes_formatted }} + {{ display_notes_formatted }} {% endif %} {% endblock %} diff --git a/ckanext/showcase/templates/showcase/search.html b/ckanext/showcase/templates/showcase/search.html index 1f2ade19..a206c033 100644 --- a/ckanext/showcase/templates/showcase/search.html +++ b/ckanext/showcase/templates/showcase/search.html @@ -5,16 +5,16 @@ {% set showcase_new_route = 'showcase_blueprint.new' %} -{% block subtitle %}{{ _("Showcases") }}{% endblock %} +{% block subtitle %}{{ _("Reuses") }}{% endblock %} {% block breadcrumb_content %} - {{ h.nav_link(_('Showcases'), named_route=showcase_index_route, highlight_actions = 'new index') }} + {{ h.nav_link(_('Reuses'), named_route=showcase_index_route) }} {% endblock %} {% block page_primary_action %} {% if h.check_access('ckanext_showcase_create') %} - {% link_for _('Add Showcase'), named_route=showcase_new_route, class_='btn btn-primary', icon='plus-square' %} + {% link_for _('Add Reuse'), named_route=showcase_new_route, class_='btn btn-primary', icon='plus-square' %} {% endif %} {% endblock %} @@ -34,7 +34,7 @@ (_('Last Modified'), 'metadata_modified desc'), (_('Popular'), 'views_recent desc') if g.tracking_enabled else (false, false) ] %} - {% snippet 'showcase/snippets/showcase_search_form.html', type='showcase', placeholder=_('Search showcases...'), query=c.q, sorting=sorting, sorting_selected=c.sort_by_selected, count=c.page.item_count, facets=facets, show_empty=request.args, error=c.query_error, fields=c.fields, no_bottom_border=true %} + {% snippet 'showcase/snippets/showcase_search_form.html', type='showcase', placeholder=_('Search reuses...'), query=c.q, sorting=sorting, sorting_selected=c.sort_by_selected, count=c.page.item_count, facets=facets, show_empty=request.args, error=c.query_error, fields=c.fields, no_bottom_border=true %} {% endblock %} {% block package_search_results_list %} diff --git a/ckanext/showcase/templates/showcase/snippets/helper.html b/ckanext/showcase/templates/showcase/snippets/helper.html index de0cf06b..fe722784 100644 --- a/ckanext/showcase/templates/showcase/snippets/helper.html +++ b/ckanext/showcase/templates/showcase/snippets/helper.html @@ -3,16 +3,11 @@ - {{ _('What are Showcases?') }} + {{ _('What are Reuses?') }} - {% trans %}Datasets have been used to build apps, websites and visualizations. They've been featured in articles, and written about in news reports and blog posts. Showcases collect together the best examples of datasets in use, to provide further insight, ideas, and inspiration.{% endtrans %} + {% trans %}Datasets have been used to build apps, websites and visualizations. They've been featured in articles, and written about in news reports and blog posts. Reuses collect together the best examples of datasets in use, to provide further insight, ideas, and inspiration.{% endtrans %} - {% if h.check_access('ckanext_showcase_admin_add') %} - - {% trans url=h.url_for(showcase_admins_route) %}Sysadmins can manage Showcase Admins from the Showcase configuration page.{% endtrans %} - - {% endif %} diff --git a/ckanext/showcase/templates/showcase/snippets/showcase_info.html b/ckanext/showcase/templates/showcase/snippets/showcase_info.html index 4b8f7b2a..415500b9 100644 --- a/ckanext/showcase/templates/showcase/snippets/showcase_info.html +++ b/ckanext/showcase/templates/showcase/snippets/showcase_info.html @@ -34,7 +34,7 @@ {{ pkg.title or pkg.name }} - {{ _('Datasets in Showcase') }} + {{ _('Datasets in Reuse') }} {% if showcase_pkgs %} {% for package in showcase_pkgs %} @@ -44,7 +44,7 @@ {% else %} - {{_('There are no Datasets in this Showcase')}} + {{_('There are no Datasets in this Reuse')}} {% endif %} {% endif %} diff --git a/ckanext/showcase/templates/showcase/snippets/showcase_item.html b/ckanext/showcase/templates/showcase/snippets/showcase_item.html index d0f7466e..a8bae6ff 100644 --- a/ckanext/showcase/templates/showcase/snippets/showcase_item.html +++ b/ckanext/showcase/templates/showcase/snippets/showcase_item.html @@ -10,26 +10,28 @@ #} {% set truncate = truncate or 180 %} {% set truncate_title = truncate_title or 80 %} -{% set title = package.title or package.name %} -{% set notes = h.markdown_extract(package.notes, extract_length=truncate) %} +{% set display_title = package.display_title %} +{% set display_notes = h.markdown_extract(package.display_notes, extract_length=truncate) %} + {% set showcase_read_route = 'showcase_blueprint.read' %} {% block package_item %} + {% block item_inner %} {% block image %} {% endblock %} {% block title %} - {{ h.link_to(title|truncate(truncate_title), h.url_for(showcase_read_route, id=package.name)) }} + {{ h.link_to(display_title|truncate(truncate_title), h.url_for(showcase_read_route, id=package.name)) }} {% endblock %} {% block notes %} - {% if notes %} - {{ notes|urlize }} + {% if display_notes %} + {{ display_notes|urlize }} {% else %} - {{ _("This showcase has no description") }} + {{ _("This reuse has no description") }} {% endif %} {% endblock %} {% block datasets %} @@ -40,15 +42,15 @@ {{ h.link_to(title|truncate(truncate_title), h.url_for {% endif %} {% endblock %} {% block link %} - - {{ _('View {showcase_title}').format(showcase_title=package.title) }} + + {{ _('View {showcase_title}').format(showcase_title=display_title) }} {% endblock %} {% if show_remove %} {{ h.csrf_input() if 'csrf_input' in h }} - + {% endif %} {% endblock %} diff --git a/ckanext/showcase/templates/showcase/snippets/showcase_search_result_text.html b/ckanext/showcase/templates/showcase/snippets/showcase_search_result_text.html index c01c2143..a9a93117 100644 --- a/ckanext/showcase/templates/showcase/snippets/showcase_search_result_text.html +++ b/ckanext/showcase/templates/showcase/snippets/showcase_search_result_text.html @@ -4,10 +4,10 @@ #} {% if type == 'showcase' %} - {% set text_query = ungettext("{number} showcase found for '{query}'", "{number} showcases found for '{query}'", count) %} - {% set text_query_none = _("No showcases found for '{query}'") %} - {% set text_no_query = ungettext("{number} showcase found", "{number} showcases found", count) %} - {% set text_no_query_none = _("No showcases found") %} + {% set text_query = ungettext("{number} reuse found for '{query}'", "{number} reuses found for '{query}'", count) %} + {% set text_query_none = _("No reuses found for '{query}'") %} + {% set text_no_query = ungettext("{number} reuse found", "{number} reuses found", count) %} + {% set text_no_query_none = _("No reuses found") %} {%- endif -%} {% if query %} diff --git a/ckanext/showcase/tests/action/test_create.py b/ckanext/showcase/tests/action/test_create.py index 3010e9c4..31367eeb 100644 --- a/ckanext/showcase/tests/action/test_create.py +++ b/ckanext/showcase/tests/action/test_create.py @@ -2,7 +2,7 @@ from ckan.model.package import Package import ckan.model as model -import ckan.plugins.toolkit as toolkit +import ckan.plugins.toolkit as tk from ckan.tests import factories, helpers @@ -27,7 +27,7 @@ def test_showcase_create_no_args(self): == 0 ) - with pytest.raises(toolkit.ValidationError): + with pytest.raises(tk.ValidationError): helpers.call_action( "ckanext_showcase_create", context=context, ) @@ -83,7 +83,7 @@ def test_showcase_create_with_existing_name(self): == 1 ) - with pytest.raises(toolkit.ValidationError): + with pytest.raises(tk.ValidationError): helpers.call_action( "ckanext_showcase_create", context=context, name="my-showcase", ) @@ -106,7 +106,7 @@ def test_association_create_no_args(self): """ sysadmin = factories.User(sysadmin=True) context = {"user": sysadmin["name"]} - with pytest.raises(toolkit.ValidationError): + with pytest.raises(tk.ValidationError): helpers.call_action( "ckanext_showcase_package_association_create", context=context, ) @@ -122,7 +122,7 @@ def test_association_create_missing_arg(self): package_id = factories.Dataset()["id"] context = {"user": sysadmin["name"]} - with pytest.raises(toolkit.ValidationError): + with pytest.raises(tk.ValidationError): helpers.call_action( "ckanext_showcase_package_association_create", context=context, @@ -195,7 +195,7 @@ def test_association_create_existing(self): showcase_id=showcase_id, ) # Attempted duplicate creation results in ValidationError - with pytest.raises(toolkit.ValidationError): + with pytest.raises(tk.ValidationError): helpers.call_action( "ckanext_showcase_package_association_create", context=context, @@ -268,7 +268,7 @@ def test_showcase_admin_add_existing_user(self): assert model.Session.query(ShowcaseAdmin).count() == 1 # Attempt second add - with pytest.raises(toolkit.ValidationError): + with pytest.raises(tk.ValidationError): helpers.call_action( "ckanext_showcase_admin_add", context={}, @@ -283,7 +283,7 @@ def test_showcase_admin_add_username_doesnot_exist(self): Calling ckanext_showcase_admin_add with non-existent username raises ValidationError and no ShowcaseAdmin object is created. """ - with pytest.raises(toolkit.ObjectNotFound): + with pytest.raises(tk.ObjectNotFound): helpers.call_action( "ckanext_showcase_admin_add", context={}, username="missing", ) @@ -296,7 +296,7 @@ def test_showcase_admin_add_no_args(self): Calling ckanext_showcase_admin_add with no args raises ValidationError and no ShowcaseAdmin object is created. """ - with pytest.raises(toolkit.ValidationError): + with pytest.raises(tk.ValidationError): helpers.call_action( "ckanext_showcase_admin_add", context={}, ) diff --git a/ckanext/showcase/tests/action/test_delete.py b/ckanext/showcase/tests/action/test_delete.py index 05c99aa0..30442af8 100644 --- a/ckanext/showcase/tests/action/test_delete.py +++ b/ckanext/showcase/tests/action/test_delete.py @@ -1,7 +1,7 @@ import pytest import ckan.model as model -import ckan.plugins.toolkit as toolkit +import ckan.plugins.toolkit as tk from ckan.tests import factories, helpers @@ -17,7 +17,7 @@ def test_showcase_delete_no_args(self): """ sysadmin = factories.Sysadmin() context = {"user": sysadmin["name"]} - with pytest.raises(toolkit.ValidationError): + with pytest.raises(tk.ValidationError): helpers.call_action( "ckanext_showcase_delete", context=context, ) @@ -29,7 +29,7 @@ def test_showcase_delete_incorrect_args(self): sysadmin = factories.Sysadmin() context = {"user": sysadmin["name"]} factories.Dataset(type="showcase") - with pytest.raises(toolkit.ObjectNotFound): + with pytest.raises(tk.ObjectNotFound): helpers.call_action( "ckanext_showcase_delete", context=context, id="blah-blah", ) @@ -200,7 +200,7 @@ def test_association_delete_no_args(self): """ sysadmin = factories.User(sysadmin=True) context = {"user": sysadmin["name"]} - with pytest.raises(toolkit.ValidationError): + with pytest.raises(tk.ValidationError): helpers.call_action( "ckanext_showcase_package_association_delete", context=context, ) @@ -214,7 +214,7 @@ def test_association_delete_missing_arg(self): package_id = factories.Dataset()["id"] context = {"user": sysadmin["name"]} - with pytest.raises(toolkit.ValidationError): + with pytest.raises(tk.ValidationError): helpers.call_action( "ckanext_showcase_package_association_delete", context=context, @@ -261,7 +261,7 @@ def test_association_delete_attempt_with_non_existent_association(self): assert model.Session.query(ShowcasePackageAssociation).count() == 0 context = {"user": sysadmin["name"]} - with pytest.raises(toolkit.ObjectNotFound): + with pytest.raises(tk.ObjectNotFound): helpers.call_action( "ckanext_showcase_package_association_delete", context=context, @@ -280,7 +280,7 @@ def test_association_delete_attempt_with_bad_package_ids(self): assert model.Session.query(ShowcasePackageAssociation).count() == 0 context = {"user": sysadmin["name"]} - with pytest.raises(toolkit.ValidationError): + with pytest.raises(tk.ValidationError): helpers.call_action( "ckanext_showcase_package_association_delete", context=context, @@ -404,7 +404,7 @@ def test_showcase_admin_remove_with_bad_username(self): ValidationError. """ - with pytest.raises(toolkit.ValidationError): + with pytest.raises(tk.ValidationError): helpers.call_action( "ckanext_showcase_admin_remove", context={}, @@ -416,7 +416,7 @@ def test_showcase_admin_remove_with_no_args(self): Calling showcase admin remove with no arg raises ValidationError. """ - with pytest.raises(toolkit.ValidationError): + with pytest.raises(tk.ValidationError): helpers.call_action( "ckanext_showcase_admin_remove", context={}, ) diff --git a/ckanext/showcase/tests/action/test_get.py b/ckanext/showcase/tests/action/test_get.py index 324d24c5..9faa0e31 100644 --- a/ckanext/showcase/tests/action/test_get.py +++ b/ckanext/showcase/tests/action/test_get.py @@ -3,7 +3,7 @@ import pytest from ckan.tests import factories, helpers -import ckan.plugins.toolkit as toolkit +import ckan.plugins.toolkit as tk @pytest.mark.usefixtures("with_plugins", "clean_db", "clean_index") @@ -12,7 +12,7 @@ def test_showcase_show_no_args(self): """ Calling showcase show with no args raises a ValidationError. """ - with pytest.raises(toolkit.ValidationError): + with pytest.raises(tk.ValidationError): helpers.call_action("ckanext_showcase_show") def test_showcase_show_with_id(self): @@ -45,7 +45,7 @@ def test_showcase_show_with_nonexisting_name(self): """ factories.Dataset(type="showcase", name="my-showcase") - with pytest.raises(toolkit.ObjectNotFound): + with pytest.raises(tk.ObjectNotFound): helpers.call_action( "ckanext_showcase_show", id="my-bad-name", ) @@ -292,7 +292,7 @@ def test_showcase_package_list_wrong_showcase_id(self): """ factories.Dataset(type="showcase")["id"] - with pytest.raises(toolkit.ValidationError): + with pytest.raises(tk.ValidationError): helpers.call_action( "ckanext_showcase_package_list", showcase_id="a-bad-id", ) @@ -431,7 +431,7 @@ def test_showcase_package_list_package_isnot_a_showcase(self): showcase_id=showcase_id, ) - with pytest.raises(toolkit.ValidationError): + with pytest.raises(tk.ValidationError): helpers.call_action( "ckanext_showcase_package_list", showcase_id=package["id"], ) @@ -475,7 +475,7 @@ def test_package_showcase_list_wrong_showcase_id(self): """ factories.Dataset()["id"] - with pytest.raises(toolkit.ValidationError): + with pytest.raises(tk.ValidationError): helpers.call_action( "ckanext_package_showcase_list", showcase_id="a-bad-id", ) @@ -561,7 +561,7 @@ def test_package_showcase_list_package_isnot_a_showcase(self): showcase_id=showcase["id"], ) - with pytest.raises(toolkit.ValidationError): + with pytest.raises(tk.ValidationError): helpers.call_action( "ckanext_package_showcase_list", package_id=showcase["id"], ) diff --git a/ckanext/showcase/tests/fixtures.py b/ckanext/showcase/tests/fixtures.py index 4f588e59..899a9af8 100644 --- a/ckanext/showcase/tests/fixtures.py +++ b/ckanext/showcase/tests/fixtures.py @@ -1,7 +1,7 @@ import pytest import ckan.model as model -from ckan.plugins import toolkit +from ckan.plugins import toolkit as tk @pytest.fixture @@ -12,6 +12,6 @@ def clean_db(reset_db, migrate_db_for): @pytest.fixture def clean_session(): - if toolkit.check_ckan_version(max_version="2.9.0"): + if tk.check_ckan_version(max_version="2.9.0"): if hasattr(model.Session, "revision"): model.Session.revision = None diff --git a/ckanext/showcase/tests/test_auth.py b/ckanext/showcase/tests/test_auth.py index e4fc6150..f16fec71 100644 --- a/ckanext/showcase/tests/test_auth.py +++ b/ckanext/showcase/tests/test_auth.py @@ -1,7 +1,7 @@ import pytest import json -import ckan.plugins.toolkit as toolkit +import ckan.plugins.toolkit as tk from ckan.tests import factories, helpers @@ -222,7 +222,7 @@ def test_auth_anon_user_cant_view_create_showcase(self, app): """ An anon (not logged in) user can't access the create showcase page. """ - if toolkit.check_ckan_version(min_version='2.10.0'): + if tk.check_ckan_version(min_version='2.10.0'): _get_request(app, "/showcase/new", status=401) else: # Remove when dropping support for 2.9 @@ -310,7 +310,7 @@ def test_auth_anon_user_cant_view_edit_showcase_page(self, app): An anon (not logged in) user can't access the showcase edit page. """ factories.Dataset(type="showcase", name="my-showcase") - if toolkit.check_ckan_version(min_version='2.10.0'): + if tk.check_ckan_version(min_version='2.10.0'): _get_request(app, "/showcase/edit/my-showcase", status=401) else: # Remove when dropping support for 2.9 @@ -372,7 +372,7 @@ def test_auth_anon_user_cant_view_manage_datasets(self, app): """ factories.Dataset(type="showcase", name="my-showcase") - if toolkit.check_ckan_version(min_version='2.10.0'): + if tk.check_ckan_version(min_version='2.10.0'): _get_request(app, "/showcase/manage_datasets/my-showcase", status=401) else: # Remove when dropping support for 2.9 @@ -434,7 +434,7 @@ def test_auth_anon_user_cant_view_delete_showcase_page(self, app): """ factories.Dataset(type="showcase", name="my-showcase") - if toolkit.check_ckan_version(min_version='2.10.0'): + if tk.check_ckan_version(min_version='2.10.0'): _get_request(app, "/showcase/delete/my-showcase", status=401) else: # Remove when dropping support for 2.9 @@ -583,7 +583,7 @@ def test_showcase_package_association_create_no_user(self): """ context = {"user": None, "model": None} - with pytest.raises(toolkit.NotAuthorized): + with pytest.raises(tk.NotAuthorized): helpers.call_auth( "ckanext_showcase_package_association_create", context=context, ) @@ -625,7 +625,7 @@ def test_showcase_package_association_create_unauthorized_creds(self): """ not_a_sysadmin = factories.User() context = {"user": not_a_sysadmin["name"], "model": None} - with pytest.raises(toolkit.NotAuthorized): + with pytest.raises(tk.NotAuthorized): helpers.call_auth( "ckanext_showcase_package_association_create", context=context, ) @@ -640,7 +640,7 @@ def test_showcase_package_association_delete_no_user(self): """ context = {"user": None, "model": None} - with pytest.raises(toolkit.NotAuthorized): + with pytest.raises(tk.NotAuthorized): helpers.call_auth( "ckanext_showcase_package_association_delete", context=context, ) @@ -682,7 +682,7 @@ def test_showcase_package_association_delete_unauthorized_creds(self): """ not_a_sysadmin = factories.User() context = {"user": not_a_sysadmin["name"], "model": None} - with pytest.raises(toolkit.NotAuthorized): + with pytest.raises(tk.NotAuthorized): helpers.call_auth( "ckanext_showcase_package_association_delete", context=context, ) @@ -696,7 +696,7 @@ def test_showcase_admin_add_no_user(self): """ context = {"user": None, "model": None} - with pytest.raises(toolkit.NotAuthorized): + with pytest.raises(tk.NotAuthorized): helpers.call_auth( "ckanext_showcase_admin_add", context=context, ) @@ -717,7 +717,7 @@ def test_showcase_admin_add_unauthorized_creds(self): """ not_a_sysadmin = factories.User() context = {"user": not_a_sysadmin["name"], "model": None} - with pytest.raises(toolkit.NotAuthorized): + with pytest.raises(tk.NotAuthorized): helpers.call_auth( "ckanext_showcase_admin_add", context=context, ) @@ -731,7 +731,7 @@ def test_showcase_admin_remove_no_user(self): """ context = {"user": None, "model": None} - with pytest.raises(toolkit.NotAuthorized): + with pytest.raises(tk.NotAuthorized): helpers.call_auth( "ckanext_showcase_admin_remove", context=context, ) @@ -752,7 +752,7 @@ def test_showcase_admin_remove_unauthorized_creds(self): """ not_a_sysadmin = factories.User() context = {"user": not_a_sysadmin["name"], "model": None} - with pytest.raises(toolkit.NotAuthorized): + with pytest.raises(tk.NotAuthorized): helpers.call_auth( "ckanext_showcase_admin_remove", context=context, ) @@ -766,7 +766,7 @@ def test_showcase_admin_list_no_user(self): """ context = {"user": None, "model": None} - with pytest.raises(toolkit.NotAuthorized): + with pytest.raises(tk.NotAuthorized): helpers.call_auth( "ckanext_showcase_admin_list", context=context, ) @@ -787,7 +787,7 @@ def test_showcase_admin_list_unauthorized_creds(self): """ not_a_sysadmin = factories.User() context = {"user": not_a_sysadmin["name"], "model": None} - with pytest.raises(toolkit.NotAuthorized): + with pytest.raises(tk.NotAuthorized): helpers.call_auth( "ckanext_showcase_admin_list", context=context, ) @@ -800,7 +800,7 @@ def test_auth_anon_user_cant_view_showcase_admin_manage_page(self, app): An anon (not logged in) user can't access the manage showcase admin page. """ - if toolkit.check_ckan_version(min_version='2.10.0'): + if tk.check_ckan_version(min_version='2.10.0'): _get_request(app, "/showcase/new", status=401) else: # Remove when dropping support for 2.9 diff --git a/ckanext/showcase/tests/test_converters.py b/ckanext/showcase/tests/test_converters.py index 339d5c50..84cb7481 100644 --- a/ckanext/showcase/tests/test_converters.py +++ b/ckanext/showcase/tests/test_converters.py @@ -3,7 +3,7 @@ import pytest import ckan.model as model -import ckan.plugins.toolkit as toolkit +import ckan.plugins.toolkit as tk from ckan.tests import factories @@ -71,7 +71,7 @@ def test_with_no_package_id_exists(self): """ context = {"session": model.Session} - with pytest.raises(toolkit.Invalid): + with pytest.raises(tk.Invalid): convert_package_name_or_id_to_title_or_name( "my-non-existent-id", context=context, ) diff --git a/ckanext/showcase/utils.py b/ckanext/showcase/utils.py index f9087f59..ab938a0b 100644 --- a/ckanext/showcase/utils.py +++ b/ckanext/showcase/utils.py @@ -13,7 +13,8 @@ import ckan.lib.navl.dictization_functions as dict_fns import ckan.lib.helpers as h import ckan.plugins.toolkit as tk -from ckanext.showcase.model import ShowcasePackageAssociation +from ckanext.showcase.data.constants import ApprovalStatus +from ckanext.showcase.model import ShowcaseApprovalStatus, ShowcasePackageAssociation _ = tk._ abort = tk.abort @@ -33,7 +34,7 @@ def check_edit_view_auth(id): } try: - tk.check_access('ckanext_showcase_update', context) + tk.check_access('ckanext_showcase_update', context, {'id':id}) except tk.NotAuthorized: return tk.abort( 401, @@ -58,7 +59,7 @@ def check_new_view_auth(): try: tk.check_access('ckanext_showcase_create', context) except tk.NotAuthorized: - return tk.abort(401, _('Unauthorized to create a package')) + return tk.abort(401, _('Unauthorized to create a reuse case')) def read_view(id): @@ -73,11 +74,11 @@ def read_view(id): # check if showcase exists try: - tk.g.pkg_dict = tk.get_action('package_show')(context, data_dict) + tk.g.pkg_dict = tk.get_action('ckanext_showcase_show')(context, data_dict) except tk.ObjectNotFound: - return tk.abort(404, _('Showcase not found')) + return tk.abort(404, _('Reuse Case not found')) except tk.NotAuthorized: - return tk.abort(401, _('Unauthorized to read showcase')) + return tk.abort(401, _('Unauthorized to read reuse case')) # get showcase packages tk.g.showcase_pkgs = tk.get_action('ckanext_showcase_package_list')( @@ -100,7 +101,7 @@ def manage_datasets_view(id): data_dict = {'id': id} try: - tk.check_access('ckanext_showcase_update', context) + tk.check_access('ckanext_showcase_update', context, data_dict) except tk.NotAuthorized: return tk.abort( 401, @@ -392,6 +393,9 @@ def url_with_params(url, params): return url + '?' + urlencode(params) + + + def delete_view(id): if 'cancel' in tk.request.args: tk.redirect_to('showcase_blueprint.edit', id=id) @@ -493,97 +497,12 @@ def dataset_showcase_list(id): return h.redirect_to( h.url_for(list_route, id=tk.g.pkg_dict['name'])) - pkg_showcase_ids = [showcase['id'] for showcase in tk.g.showcase_list] - site_showcases = tk.get_action('ckanext_showcase_list')(context, {}) - - tk.g.showcase_dropdown = [[showcase['id'], showcase['title']] - for showcase in site_showcases - if showcase['id'] not in pkg_showcase_ids] + tk.g.showcase_dropdown = [] return tk.render("package/dataset_showcase_list.html", extra_vars={'pkg_dict': tk.g.pkg_dict}) -def manage_showcase_admins(): - context = { - 'model': model, - 'session': model.Session, - 'user': tk.g.user or tk.g.author - } - - try: - tk.check_access('sysadmin', context, {}) - except tk.NotAuthorized: - return tk.abort(401, _('User not authorized to view page')) - - form_data = tk.request.form - admins_route = 'showcase_blueprint.admins' - - # We're trying to add a user to the showcase admins list. - if tk.request.method == 'POST' and form_data['username']: - username = form_data['username'] - try: - tk.get_action('ckanext_showcase_admin_add')( - {}, {'username': username} - ) - except tk.NotAuthorized: - abort(401, _('Unauthorized to perform that action')) - except tk.ObjectNotFound: - h.flash_error( - _("User '{user_name}' not found.").format(user_name=username)) - except tk.ValidationError as e: - h.flash_notice(e.error_summary) - else: - h.flash_success(_("The user is now a Showcase Admin")) - - return tk.redirect_to(h.url_for(admins_route)) - - tk.g.showcase_admins = tk.get_action('ckanext_showcase_admin_list')({},{}) - - return tk.render('admin/manage_showcase_admins.html') - - -def remove_showcase_admin(): - ''' - Remove a user from the Showcase Admin list. - ''' - context = { - 'model': model, - 'session': model.Session, - 'user': tk.g.user or tk.g.author - } - - try: - tk.check_access('sysadmin', context, {}) - except tk.NotAuthorized: - return tk.abort(401, _('User not authorized to view page')) - - form_data = tk.request.form - admins_route = 'showcase_blueprint.admins' - - if 'cancel' in form_data: - return tk.redirect_to(admins_route) - - user_id = tk.request.args['user'] - if tk.request.method == 'POST' and user_id: - user_id = tk.request.args['user'] - try: - tk.get_action('ckanext_showcase_admin_remove')( - {}, {'username': user_id} - ) - except tk.NotAuthorized: - return tk.abort(401, _('Unauthorized to perform that action')) - except tk.ObjectNotFound: - h.flash_error(_('The user is not a Showcase Admin')) - else: - h.flash_success(_('The user is no longer a Showcase Admin')) - - return tk.redirect_to(h.url_for(admins_route)) - - tk.g.user_dict = tk.get_action('user_show')({}, {'id': user_id}) - tk.g.user_id = user_id - return tk.render('admin/confirm_remove_showcase_admin.html') - def markdown_to_html(): ''' Migrates the notes of all showcases from markdown to html. @@ -639,3 +558,62 @@ def upload(): tk.abort(401, _('Unauthorized to upload file %s') % id) return json.dumps(url) + + + +def check_dashboard_list_view_auth(): + context = { + 'model': model, + 'session': model.Session, + 'user': tk.g.user or tk.g.author, + 'auth_user_obj': tk.g.userobj, + } + + try: + tk.check_access('ckanext_showcase_list', context) + except tk.NotAuthorized: + return tk.abort( + 401, + _('User not authorized to view the Reuse Cases Dashboard') + ) + +# Dashboard +def pager_url(params_nopage, + q = None, # noqa + page = None) -> str: + params = list(params_nopage) + params.append((u'page', page)) + return _url_with_params(h.url_for('showcase_blueprint.dashboard_index'), params) + +def _url_with_params(url: str, params) -> str: + params = _encode_params(params) + return url + u'?' + urlencode(params) + +def _encode_params(params): + return [(k, v.encode(u'utf-8') if isinstance(v, str) else str(v)) + for k, v in params] + + +def check_status_update_view_auth(id): + context = { + 'model': model, + 'session': model.Session, + 'user': tk.g.user or tk.g.author, + 'auth_user_obj': tk.g.userobj, + } + + try: + tk.check_access('ckanext_showcase_status_show', context) + except tk.NotAuthorized: + return tk.abort( + 401, + _('User not authorized to update the Reuse Cases status') + ) + + +def get_approved_showcase_ids(): + q = ShowcaseApprovalStatus.filter_showcases(status=ApprovalStatus.APPROVED.value) + return [ + showcase.id + for showcase in q.all() + ] \ No newline at end of file diff --git a/ckanext/showcase/views.py b/ckanext/showcase/views.py index f56ea453..35c1275b 100644 --- a/ckanext/showcase/views.py +++ b/ckanext/showcase/views.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from ckanext.showcase.data.constants import ApprovalStatus from flask import Blueprint @@ -106,18 +107,12 @@ def dataset_showcase_list(id): return utils.dataset_showcase_list(id) -def admins(): - return utils.manage_showcase_admins() - - -def admin_remove(): - return utils.remove_showcase_admin() - - def upload(): return utils.upload() + + showcase.add_url_rule('/showcase', view_func=index, endpoint="index") showcase.add_url_rule('/showcase/new', view_func=CreateView.as_view('new'), endpoint="new") showcase.add_url_rule('/showcase/delete/', @@ -137,18 +132,206 @@ def upload(): view_func=dataset_showcase_list, methods=['GET', 'POST'], endpoint="dataset_showcase_list") -showcase.add_url_rule('/ckan-admin/showcase_admins', - view_func=admins, - methods=['GET', 'POST'], - endpoint="admins") -showcase.add_url_rule('/ckan-admin/showcase_admin_remove', - view_func=admin_remove, - methods=['GET', 'POST'], - endpoint='admin_remove') showcase.add_url_rule('/showcase_upload', view_func=upload, methods=['POST']) + + +from flask import Blueprint, request +from ckan.lib.helpers import helper_functions as h + + +import logging +import ckan.lib.base as base +import ckan.lib.navl.dictization_functions as dict_fns +import ckan.logic as logic +import ckan.plugins.toolkit as tk +import ckan.model as model + +import ckanext.showcase.logic.schema as schema + +from typing import Union, cast +from flask import Blueprint +from flask.views import MethodView +from ckan.common import current_user, _, request + +from ckan.types import Context, Response +from flask import request +from functools import partial +from ckan.lib.helpers import Page + + +_get_action = logic.get_action +_tuplize_dict = logic.tuplize_dict +_clean_dict = logic.clean_dict +_parse_params = logic.parse_params + + +log = logging.getLogger(__name__) + + +def dashboard_list(): + utils.check_dashboard_list_view_auth() + context = cast(Context, { + u'model': model, + u'session': model.Session, + u'user': current_user.name, + u'auth_user_obj': current_user, + }) + + data_dict = {**request.args} + + data_dict, errors = dict_fns.validate(data_dict, schema.showcase_search_schema(), context) + + sort = data_dict.get('sort', 'metadata_created desc') + limit = data_dict.get('limit', 20) + showcases = tk.get_action('ckanext_showcase_search')(context, {**data_dict, 'sort':sort, 'limit': limit}) + + showcase_statistics = tk.get_action('ckanext_showcase_statics')(context, data_dict) + + + params_nopage = [ + (k, v) for k, v in request.args.items(multi=True) + if k != u'page' + ] + + pager_url = partial(utils.pager_url, params_nopage) + + pagination = Page( + collection=showcases['items'], + page=data_dict.get('page', 1), + url=pager_url, + item_count=showcases.get('count'), + items_per_page=limit, + ) + pagination.items=showcases['items'] + + + return base.render(u'showcase/dashboard/search.html', + extra_vars={ + "showcases": showcases.get('items'), + "count": showcases.get('total'), + u'errors': errors, + 'data_dict': data_dict, + 'selected_sorting': sort, + 'page': pagination, + 'statistics': showcase_statistics, + } + ) + + +class StatusUpdate(MethodView): + def _prepare(self) -> Context: + context = cast(Context, { + u'model': model, + u'session': model.Session, + u'user': current_user.name, + u'auth_user_obj': current_user, + }) + return context + + def get(self, + id: str, + data = None, + errors = None, + error_summary = None + ) -> Union[Response, str]: + utils.check_status_update_view_auth(id) + context = self._prepare() + + showcase = _get_action(u'ckanext_showcase_show')(context, {u'id': id}) + showcase_datasets = _get_action(u'ckanext_showcase_package_list')(context, {u'showcase_id': id}) + showcase_status = _get_action(u'ckanext_showcase_status_show')(context, {u'showcase_id': id}) + + data = {**showcase_status, **(data or {})} + + user_info = _get_action(u'user_management_show')({**context, 'ignore_auth': True}, {u'id': showcase.get('creator_user_id')}) + + errors = errors or {} + errors_json = h.dump_json(errors) + + + return base.render( + 'showcase/dashboard/actions.html', + extra_vars={ + u'data': data, + u'errors': errors, + u'error_summary': error_summary, + + u'showcase': showcase, + 'showcase_datasets': showcase_datasets, + 'showcase_status': showcase_status, + + u'user_info': user_info, + u'errors_json': errors_json + } + ) + + def post(self, id): + context = self._prepare() + + data_dict = _clean_dict( + dict_fns.unflatten( + _tuplize_dict(_parse_params(tk.request.form)))) + data_dict.update( + _clean_dict( + dict_fns.unflatten( + _tuplize_dict(_parse_params( + tk.request.files))))) + + data_dict['showcase_id'] = id + + try: + updated_showcase = tk.get_action('ckanext_showcase_status_update')(context, data_dict) + + except tk.ValidationError as e: + errors = e.error_dict + error_summary = e.error_summary + return self.get(id, data_dict, errors, error_summary) + + + url = h.url_for('showcase_blueprint.dashboard_read', id=id) + return h.redirect_to(url) + + + +def dashboard_read(id): + context = { + 'model': model, + 'session': model.Session, + 'user': tk.g.user or tk.g.author, + 'auth_user_obj': tk.g.userobj + } + data_dict = {'id': id} + + try: + showcase = tk.get_action('ckanext_showcase_show')(context, data_dict) + showcase_datasets = _get_action(u'ckanext_showcase_package_list')(context, {u'showcase_id': id}) + user_info = tk.get_action(u'user_management_show')({**context, 'ignore_auth': True}, {u'id': showcase.get('creator_user_id')}) + + except tk.ObjectNotFound: + return tk.abort(404, _('Reuse Case not found')) + except tk.NotAuthorized: + return tk.abort(401, _('User not authorized to view this Reuse Case')) + + return tk.render(u'showcase/dashboard/detail.html', + extra_vars={ + "showcase": showcase, + "user_info": user_info, + "showcase_datasets": showcase_datasets, + } + ) + + +showcase.add_url_rule('/dashboard/showcase', view_func=dashboard_list, endpoint="dashboard_index") +showcase.add_url_rule('/dashboard/showcase/update/', view_func=StatusUpdate.as_view('edit'), + methods=['GET', 'POST'], + endpoint="dashboard_update") +showcase.add_url_rule('/dashboard/showcase/', view_func=dashboard_read, endpoint="dashboard_read") + + + def get_blueprints(): return [showcase]
{{ _('Are you sure you want to remove this user as a Showcase Admin - {name}?').format(name=c.user_dict.name) }}
{{ _('Are you sure you want to remove this user as a Reuse Admin - {name}?').format(name=c.user_dict.name) }}
{{ _('There are currently no Showcase Admins.') }}
{{ _('There are currently no Reuse Admins.') }}
Showcase Admin: Can create and remove showcases. Can add and remove datasets from showcases.
Reuse Admin: Can create and remove reuses. Can add and remove datasets from reuses.
{{ _('There are no showcases that feature this dataset') }}
{{ _('There are no reuses that feature this dataset') }}
{{ _('Are you sure you want to delete showcase - {showcase_name}?').format(showcase_name=pkg.name) }}
{{ _('Are you sure you want to delete reuse - {showcase_name}?').format(showcase_name=pkg.name) }}
+ +
+ {% trans %} + The "Data Request" feature allows users to request datasets that are not yet available in the portal. Whether you're looking for specific data to support your research, project, or decision-making process, you can submit a request directly to the data providers. This feature enables collaboration between the public and data publishers, helping to expand the portal’s dataset offerings. Simply submit your request and track its progress as data providers work to make the requested information available. + {% endtrans %} +
{{_('Filter By')}}
{{_('Status')}}
{{_('From Date')}}
{{_('To Date')}}
{{_('Order By')}}
{{_('Total')}}
+ {{ statistics.total or 0 }} +
{{_('Pending')}}
+ {{ statistics.status_breakdown.a or 0 }} +
{{_('Rejected')}}
+ {{ statistics.status_breakdown.c or 0 }} +
{{_('Needs Revision')}}
+ {{ statistics.status_breakdown.b or 0 }} +
{{_('Approved')}}
+ {{ statistics.status_breakdown.d or 0 }} +
+ + {{ _('Submitted on') }} + {{ h.date_str_to_datetime(submitted_date).strftime('%d/%m/%Y') }} +
{{user_info.org_dict.display_name}}
- {{ _('This showcase has no datasets associated to it') }}. + {{ _('This reuse has no datasets associated to it') }}.
- {% trans %}Datasets have been used to build apps, websites and visualizations. They've been featured in articles, and written about in news reports and blog posts. Showcases collect together the best examples of datasets in use, to provide further insight, ideas, and inspiration.{% endtrans %} + {% trans %}Datasets have been used to build apps, websites and visualizations. They've been featured in articles, and written about in news reports and blog posts. Reuses collect together the best examples of datasets in use, to provide further insight, ideas, and inspiration.{% endtrans %}
- {% trans url=h.url_for(showcase_admins_route) %}Sysadmins can manage Showcase Admins from the Showcase configuration page.{% endtrans %} -
{{_('There are no Datasets in this Showcase')}}
{{_('There are no Datasets in this Reuse')}}
{{ _("This showcase has no description") }}
{{ _("This reuse has no description") }}