From a9dd3266cdfef6f6c680c02aea0f80774e15d1c8 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Wed, 13 Nov 2024 14:45:18 +0100 Subject: [PATCH 01/10] feature: add validators and some privatedatasets helpers (before implementing templates 2.6) --- .../d4science_theme/controllers/home.py | 2 +- .../d4science_theme/controllers/systemtype.py | 2 +- .../ckanext/d4science_theme/db.py | 62 ++++++ .../ckanext/d4science_theme/helpers.py | 76 +++++++ .../ckanext/d4science_theme/plugin.py | 166 +++++++++++++-- .../templates/snippets/package_item.html | 197 ++++++++++++------ .../ckanext/d4science_theme/validators.py | 24 +++ ckanext-d4science_theme/requirements.txt | 1 + ckanext-d4science_theme/setup.py | 3 +- 9 files changed, 447 insertions(+), 86 deletions(-) create mode 100644 ckanext-d4science_theme/ckanext/d4science_theme/db.py create mode 100644 ckanext-d4science_theme/ckanext/d4science_theme/validators.py diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/controllers/home.py b/ckanext-d4science_theme/ckanext/d4science_theme/controllers/home.py index c5746d5..7e5f31f 100644 --- a/ckanext-d4science_theme/ckanext/d4science_theme/controllers/home.py +++ b/ckanext-d4science_theme/ckanext/d4science_theme/controllers/home.py @@ -57,7 +57,7 @@ class d4SHomeController(): 'sort': 'views_recent desc', 'fq': 'capacity:"public"' } - query = logic.get_action('package_search')(context, data_dict) + query = logic.get_action('package_search')(context, data_dict or {}) c.search_facets = query['search_facets'] c.package_count = query['count'] c.datasets = query['results'] diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/controllers/systemtype.py b/ckanext-d4science_theme/ckanext/d4science_theme/controllers/systemtype.py index 8d94e5b..af16a6f 100644 --- a/ckanext-d4science_theme/ckanext/d4science_theme/controllers/systemtype.py +++ b/ckanext-d4science_theme/ckanext/d4science_theme/controllers/systemtype.py @@ -71,7 +71,7 @@ class d4STypeController(): 'sort': 'views_recent desc', 'fq': 'capacity:"public"' } - query = logic.get_action('package_search')(context, data_dict) + query = logic.get_action('package_search')(context, data_dict or {}) c.search_facets = query['search_facets'] c.package_count = query['count'] c.datasets = query['results'] diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/db.py b/ckanext-d4science_theme/ckanext/d4science_theme/db.py new file mode 100644 index 0000000..034a3ac --- /dev/null +++ b/ckanext-d4science_theme/ckanext/d4science_theme/db.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid + +# This file is part of CKAN Private Dataset Extension. + +# CKAN Private Dataset Extension is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# CKAN Private Dataset Extension is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with CKAN Private Dataset Extension. If not, see . + +from __future__ import absolute_import +from logging import getLogger + +import sqlalchemy as sa + +log = getLogger(__name__) + +AllowedUser = None + +def init_db(model): + log.debug("call initDB...") + global AllowedUser + if AllowedUser is None: + + class _AllowedUser(model.DomainObject): + + @classmethod + def get(cls, **kw): + '''Finds all the instances required.''' + query = model.Session.query(cls).autoflush(False) + results = query.filter_by(**kw).all() + log.debug("results in get %s", results) + if not isinstance(results, list): + log.debug("Errore: il risultato di get() non è una lista. Risultato:", results) + results = [] + return results + + AllowedUser = _AllowedUser + + log.debug("allowed user: %s", AllowedUser) + + # FIXME: Maybe a default value should not be included... + package_allowed_users_table = sa.Table( + 'package_allowed_users', + model.meta.metadata, + sa.Column('package_id', sa.types.UnicodeText, primary_key=True, default=u''), + sa.Column('user_name', sa.types.UnicodeText, primary_key=True, default=u''), + ) + + # Create the table only if it does not exist + package_allowed_users_table.create(checkfirst=True) + + model.meta.mapper(AllowedUser, package_allowed_users_table,) diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/helpers.py b/ckanext-d4science_theme/ckanext/d4science_theme/helpers.py index 2584283..e6ffcbd 100644 --- a/ckanext-d4science_theme/ckanext/d4science_theme/helpers.py +++ b/ckanext-d4science_theme/ckanext/d4science_theme/helpers.py @@ -25,6 +25,8 @@ import collections import ckan.plugins.toolkit as tk import ckan.logic as logic +from ckanext.d4science_theme import db + log = getLogger(__name__) systemtype_field = 'systemtypefield' @@ -730,3 +732,77 @@ def get_site_statistics() -> dict[str, int]: 'group_count': len(logic.get_action('group_list')({}, {})), 'organization_count': len(logic.get_action('organization_list')({}, {})) } + +#private dataset + +def is_dataset_acquired(pkg_dict): + + log.debug("is_dataset package value: %s", pkg_dict) #restituisce True + + db.init_db(model) + log.debug("post init %s", db) + + #return False + if tk.c.user: + return len(db.AllowedUser.get(package_id=pkg_dict['id'], user_name=tk.c.user)) > 0 + else: + return False + +def is_owner(pkg_dict): + return False + #if tk.c.userobj is not None: + # return tk.c.userobj.id == pkg_dict['creator_user_id'] + #else: + # return False + + +def get_allowed_users_str(users): + if users: + return ','.join([user for user in users]) + else: + return '' + + +def can_read(pkg_dict): + try: + context = {'user': tk.c.user, 'userobj': tk.c.userobj, 'model': model} + return tk.check_access('package_show', context, pkg_dict) + except tk.NotAuthorized: + return False + + +def get_config_bool_value(config_name, default_value=False): + env_name = config_name.upper().replace('.', '_') + value = os.environ.get(env_name, tk.config.get(config_name, default_value)) + return value if type(value) == bool else value.strip().lower() in ('true', '1', 'on') + + +def show_acquire_url_on_create(): + return get_config_bool_value('ckan.privatedatasets.show_acquire_url_on_create') + + +def show_acquire_url_on_edit(): + return get_config_bool_value('ckan.privatedatasets.show_acquire_url_on_edit') + + +def acquire_button(package): + ''' + Return a Get Access button for the given package id when the dataset has + an acquisition URL. + + :param package: the the package to request access when the get access + button is clicked + :type package: Package + + :returns: a get access button as an HTML snippet + :rtype: string + + ''' + + if 'acquire_url' in package and request.path.startswith('/dataset')\ + and package['acquire_url'] != '': + url_dest = package['acquire_url'] + data = {'url_dest': url_dest} + return tk.render_snippet('snippets/acquire_button.html', data) + else: + return '' \ No newline at end of file diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py b/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py index 5769285..9431c59 100644 --- a/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py +++ b/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py @@ -4,6 +4,7 @@ from logging import getLogger import ckan.plugins as plugins from ckanext.d4science_theme import helpers +from ckanext.d4science_theme import validators import ckan.plugins.toolkit as toolkit import ckan.model as model from ckanext.d4science_theme.controllers.home import d4SHomeController @@ -47,7 +48,7 @@ def _package_extras_save(extra_dicts, obj, context): session = context["session"] #ADDED BY FRANCESCO MANGIACRAPA - log.debug("extra_dicts: "+ str(extra_dicts)) + log.debug("extra_dicts: %s", extra_dicts) #print "extra_dicts: "+str(extra_dicts) extras_list = obj.extras_list @@ -66,7 +67,7 @@ def _package_extras_save(extra_dicts, obj, context): #print 'extra_dict key: '+extra_dict["key"] + ', value: '+extra_dict["value"] #new_extras.setdefault(extra_dict["key"], []).append(extra_dict["value"]) if extra_dict.get("deleted"): - log.debug("extra_dict deleted: "+str(extra_dict["key"])) + log.debug("extra_dict deleted: %s ", extra_dict["key"]) #print 'extra_dict deleted: '+extra_dict["key"] continue @@ -75,13 +76,13 @@ def _package_extras_save(extra_dicts, obj, context): new_extras.setdefault(extra_dict["key"], []).append(extra_dict["value"]) #ADDED BY FRANCESCO MANGIACRAPA - log.debug("new_extras: "+str(new_extras)) + log.debug("new_extras: %s", new_extras) #print "new_extras: "+str(new_extras) #new for key in set(new_extras.keys()) - set(old_extras.keys()): state = 'active' - log.debug("adding key: "+str(key)) + log.debug("adding key: %s", key) #print "adding key: "+str(key) extra_lst = new_extras[key] for extra in extra_lst: @@ -105,13 +106,13 @@ def _package_extras_save(extra_dicts, obj, context): for value in new_extras[key]: old_occur = old_extras[key].count(value) new_occur = new_extras[key].count(value) - log.debug("value: "+str(value) + ", new_occur: "+str(new_occur)+ ", old_occur: "+str(old_occur)) + log.debug("value: %s\n new_occur: %s\n old_occur: %s", value, new_occur, old_occur) #print "value: "+str(value) + ", new_occur: "+str(new_occur) + ", old_occur: "+str(old_occur) # it is an old value deleted or not if value in old_extras[key]: if old_occur == new_occur: #print "extra - occurrences of: "+str(value) +", are equal into both list" - log.debug("extra - occurrences of: "+str(value) +", are equal into both list") + log.debug("extra - occurrences of: %s are equal into both list", value) #there is a little bug, this code return always the first element, so I'm fixing with #FIX-STATUS extra_values = get_package_for_value(extras[key], value) #extras_list.append(extra) @@ -120,44 +121,44 @@ def _package_extras_save(extra_dicts, obj, context): extra.state = state session.add(extra) #print "extra updated: "+str(extra) - log.debug("extra updated: "+str(extra)) + log.debug("extra updated: %s", extra) elif new_occur > old_occur: #print "extra - a new occurrence of: "+str(value) +", is present into new list, adding it to old list" - log.debug("extra - a new occurrence of: "+str(value) +", is present into new list, adding it to old list") + log.debug("extra - a new occurrence of: %s, is present into new list, adding it to old list", value) state = 'active' extra = model.PackageExtra(state=state, key=key, value=value) extra.state = state session.add(extra) extras_list.append(extra) old_extras[key].append(value) - log.debug("old extra values updated: "+str(old_extras[key])) + log.debug("old extra values updated: %s", old_extras[key]) #print "old extra values updated: "+str(old_extras[key]) else: #remove all occurrences deleted - this code could be optimized, it is run several times but could be performed one shot countDelete = old_occur-new_occur - log.debug("extra - occurrence of: "+str(value) +", is not present into new list, removing "+str(countDelete) + " occurrence/s from old list") + log.debug("extra - occurrence of: %s, is not present into new list, removing: %s occurrence/s from old list", value, countDelete) #print "extra - occurrence of: "+str(value) +", is not present into new list, removing "+str(countDelete)+" occurrence/s from old list" extra_values = get_package_for_value(extras[key], value) for idx, extra in enumerate(extra_values): if idx < countDelete: #print "extra - occurrence of: "+str(value) +", is not present into new list, removing it from old list" - log.debug("pkg extra deleting: "+str(extra.value)) + log.debug("pkg extra deleting: %s", extra.value) #print "pkg extra deleting: "+str(extra.value) state = 'deleted' extra.state = state else: #print "pkg extra reactivating: "+str(extra.value) - log.debug("pkg extra reactivating: "+str(extra.value)) + log.debug("pkg extra reactivating: %s", extra.value) state = 'active' extra.state = state session.add(extra) else: #print "extra new value: "+str(value) - log.debug("extra new value: "+str(value)) + log.debug("extra new value: %s", value) state = 'active' extra = model.PackageExtra(state=state, key=key, value=value) extra.state = state @@ -193,13 +194,15 @@ def get_package_for_value(list_package, value): class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm): plugins.implements(plugins.IConfigurer) - plugins.implements(plugins.IDatasetForm) + plugins.implements(plugins.IDatasetForm, inherit=True) plugins.implements(plugins.ITemplateHelpers) plugins.implements(plugins.IFacets) #plugins.implements(IRoutes, inherit=True) #ckan 2.10 plugins.implements(plugins.IBlueprint) + plugins.implements(plugins.IValidators) + plugins.implements(plugins.IPackageController, inherit=True) # IConfigurer def update_config(self, config_): @@ -219,37 +222,157 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) toolkit.add_resource('assets', 'd4science_theme') # toolkit.add_resource('assets', 'd4science_scripts') + def _modify_package_schema(self): + log.debug("*** modify package ***") + + # Personalizza il campo 'extras' rimuovendo il validatore extra_key_not_in_root_schema + + return { + 'private': [toolkit.get_validator('ignore_missing'), + toolkit.get_validator('boolean_validator')], + 'extras': { + 'id': [toolkit.get_validator('ignore')], # Ignora 'id' come prima + 'key': [toolkit.get_validator('not_empty'), toolkit.get_validator('unicode_safe'), validators.ignore_duplicate_keys], # Aggiunto ignore duplicate + 'value': [toolkit.get_validator('not_missing')] + } + } + #IDatasetForm def create_package_schema(self): # let's grab the default schema in our plugin + log.debug("creating package....") schema = super(D4Science_ThemePlugin, self).create_package_schema() - schema = remove_check_replicated_custom_key(schema) + log.debug("schema after create prima del validator %s", schema) + #schema = remove_check_replicated_custom_key(schema) + #schema.update(self._modify_package_schema(schema)) + schema.update(self._modify_package_schema()) #d.package_dict_save = _package_dict_save + log.debug("create_package1 %s", schema) return schema #IDatasetForm def update_package_schema(self): + log.debug("** update_package **") schema = super(D4Science_ThemePlugin, self).update_package_schema() - schema = remove_check_replicated_custom_key(schema) + #schema = remove_check_replicated_custom_key(schema) + #schema.update(self._modify_package_schema(schema)) + schema.update(self._modify_package_schema()) + log.debug("update_package1 %s", schema) return schema #IDatasetForm def show_package_schema(self): + log.debug("** show package **") schema = super(D4Science_ThemePlugin, self).show_package_schema() - schema = remove_check_replicated_custom_key(schema) + log.debug("show_package1 %s", schema) + #schema = remove_check_replicated_custom_key(schema) + #schema.update(self._modify_package_schema(schema)) + schema.update({ + 'extras': { + 'id': [toolkit.get_validator('ignore')], # Ignora 'id' come prima + 'key': [toolkit.get_validator('not_empty'), toolkit.get_validator('unicode_safe'), validators.ignore_duplicate_keys], # Aggiunto ignore duplicate + 'value': [toolkit.get_validator('not_missing')] + } + }) + log.debug("show_package2 %s", schema) return schema #IDatasetForm def is_fallback(self): # Return True to register this plugin as the default handler for package types not handled by any other IDatasetForm plugin - return False + return True #IDatasetForm def package_types(self): # This plugin doesn't handle any special package types, it just # registers itself as the default (above). return [] + + def get_validators(self): + return { + 'ignore_duplicate_keys': validators.ignore_duplicate_keys + } + #IPackageController + #def before_dataset_search(self, search_params): + # search_params = search_params or {} + # return search_params + # + #def after_dataset_search(self, search_results, data_dict): + # return search_results + # + #def before_search(self, search_params): + # # Controlla se search_params è None e sostituiscilo con un dizionario vuoto + # search_params = search_params or {} + # return search_params + # + #def before_create(self, context, data_dict): + # self.apply_custom_extras_validator(data_dict) + # + #def before_update(self, context, data_dict): + # self.apply_custom_extras_validator(data_dict) + # + + ## utile ## + #def before_dataset_save(self, context, data_dict): + # log.debug("sto chiamando before_dataset_save") + # # Intercetta il salvataggio dei dataset prima che venga effettuato il controllo + # extras_list = data_dict.get('extras', []) + # + # # Aggiungi una logica per evitare che venga applicato un controllo su chiavi duplicate + # # Si elimina la logica di validazione delle chiavi duplicate + # unique_extras = {} + # for extra in extras_list: + # key = extra.get('key') + # if key in unique_extras: + # # Consenti la duplicazione delle chiavi, quindi non lo filtriamo + # unique_extras[key].append(extra) + # else: + # unique_extras[key] = [extra] + # + # # Restituisci un nuovo 'extras' senza rimuovere le chiavi duplicate + # data_dict['extras'] = [e for extras in unique_extras.values() for e in extras] + # return data_dict + def convert_to_boolean(self, value): + log.debug("value boolean %s", value) + if isinstance(value, str): + if value.lower() == 'true': + return True + elif value.lower() == 'false': + return False + return value + + #IValidator + def validate(self, context, data_dict, schema, action): + # Modifica il comportamento del validatore per ignorare le chiavi duplicate + # Assicurati che 'private' sia un valore booleano + log.debug("calling, validate, questo è lo schema: %s", schema) + data_dict['private'] = self.convert_to_boolean(data_dict.get('private', False)) + errors = [] + #todo change this function + if 'extras' in data_dict: + log.debug("extras presente") + extras = data_dict['extras'] + new_extras = [] + log.debug("extras value before for: %s e lunghezza: %s", extras, len(extras)) + # Aggiungi ogni coppia chiave-valore alla nuova lista, mantenendo i duplicati + for extra in extras: + new_extras.append(extra) + log.debug("Aggiunta extra con chiave duplicata: %s -> %s", extra['key'], extra['value']) + + # Aggiorna il data_dict con la lista di extras che include i duplicati + data_dict['extras'] = new_extras + log.debug("new extras mantenendo i duplicati: %s", new_extras) + log.debug("pre return validate %s", data_dict) + return data_dict, errors + + def apply_custom_extras_validator(self, data_dict): + """ + Funzione helper per applicare il validator agli extras + """ + extras = data_dict.get('extras', []) + # Esegui il validator per assicurare il salvataggio dei duplicati + validators.ignore_duplicate_keys('extras', data_dict, [], {}) #ITemplateHelpers def get_helpers(self): @@ -290,6 +413,13 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) 'd4science_get_location_to_bboxes' : helpers.get_location_to_bboxes, 'd4science_get_content_moderator_system_placeholder': helpers.get_content_moderator_system_placeholder, 'd4science_get_site_statistics': helpers.get_site_statistics, + 'is_dataset_acquired': helpers.is_dataset_acquired, + 'get_allowed_users_str': helpers.get_allowed_users_str, + 'is_owner': helpers.is_owner, + 'can_read': helpers.can_read, + 'show_acquire_url_on_create': helpers.show_acquire_url_on_create, + 'show_acquire_url_on_edit': helpers.show_acquire_url_on_edit, + 'acquire_button': helpers.acquire_button } #Overriding package_extras_save method diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/package_item.html b/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/package_item.html index 801d9c8..0e16bed 100644 --- a/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/package_item.html +++ b/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/package_item.html @@ -1,70 +1,139 @@ {# -Displays a single of dataset. - -package - A package to display. -item_class - The class name to use on the list item. -hide_resources - If true hides the resources (default: false). - -Example: - - {% snippet 'snippets/package_item.html', package=c.datasets[0] %} - -#} -{% set title = package.title or package.name %} -{% set notes = h.markdown_extract(package.notes, extract_length=180) %} - -{% block package_item %} + Displays a single of dataset. + + package - A package to display. + item_class - The class name to use on the list item. + hide_resources - If true hides the resources (default: false). + banner - If true displays a popular banner (default: false). + truncate - The length to trucate the description to (default: 180) + truncate_title - The length to truncate the title to (default: 80). + + Example: + + {% snippet 'snippets/package_item.html', package=c.datasets[0] %} + + #} + + {% 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) %} + + + {# + {% resource 'd4science_theme/custom.css' %} + CHANGED BY FRANCESCO.MANGIACRAPA + #} + + {% block package_item_content %} + + {% if package.private and not h.can_read(package) %}
  • - {% block content %}
    - {% block heading %} -

    - {% block heading_private %} - {% if package.private %} - - - {{ _('Private') }} - - {% endif %} - {% endblock %} - {% block heading_title %} - - {{title|truncate(80)}} - - {% endblock %} - {% block heading_meta %} - {% if package.get('state', '').startswith('draft') %} - {{ _('Draft') }} - {% elif package.get('state', '').startswith('deleted') %} - {{ _('Deleted') }} - {% endif %} - {{ h.popular('recent views', package.tracking_summary.recent, min=10) if package.tracking_summary }} - {% endblock %} -

    - {% endblock %} - {% block notes %} - {% if notes %} -
    {{ notes|urlize }}
    - {% else %} -

    {{ h.humanize_entity_type('package', package.type, 'no description') or _("There is no description for this dataset") }}

    +

    + {% if package.private and not h.can_read(package) %} + + + {{ _('Private') }} + {% endif %} - {% endblock %} -

    - {% block resources %} - {% if package.resources and not hide_resources %} - {% block resources_outer %} -
      - {% block resources_inner %} - {% for resource in h.dict_list_reduce(package.resources, 'format') %} -
    • - {{ resource }} -
    • - {% endfor %} - {% endblock %} -
    - {% endblock %} + + + + {% if package.private and not h.can_read(package) %} + {# {{ _(h.truncate(title, truncate_title)) }} #} + Dataset +
    + {{ h.acquire_button(package) }} + {% else %} + {# {{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }} #} + {{ h.link_to(h.truncate(title, truncate_title), url_for('dataset.read', id=package.name)) }} + {% endif %} + + + {% if package.get('state', '').startswith('draft') %} + {{ _('Draft') }} + {% elif package.get('state', '').startswith('deleted') %} + {{ _('Deleted') }} + {% endif %} + {{ h.popular('recent views', package.tracking_summary.recent, min=10) if package.tracking_summary }} + + {% if banner %} + {% endif %} - {% endblock %} - {% endblock %} + {% if notes %} + + {% endif %} +
  • -{% endblock %} + {% else %} +
  • +
    +

    + {% if package.private and not h.can_read(package) %} + + + {{ _('Private') }} + + {% endif %} + + + + {% if package.private and not h.can_read(package) %} + {{ _(h.truncate(title, truncate_title)) }} +
    + {{ h.acquire_button(package) }} + {% else %} + {# {{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }} #} + {{ h.link_to(h.truncate(title, truncate_title), url_for('dataset.read', id=package.name)) }} + {% endif %} + + + {% if package.get('state', '').startswith('draft') %} + {{ _('Draft') }} + {% elif package.get('state', '').startswith('deleted') %} + {{ _('Deleted') }} + {% endif %} + {{ h.popular('recent views', package.tracking_summary.recent, min=10) if package.tracking_summary }} +

    + {% if banner %} + + {% endif %} + {% if notes %} +
    {{ notes|urlize }}
    + {% endif %} +
    + {% if package.resources and not hide_resources %} + + {% endif %} + +
  • + {% endif %} + {% endblock %} \ No newline at end of file diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/validators.py b/ckanext-d4science_theme/ckanext/d4science_theme/validators.py new file mode 100644 index 0000000..f9780b7 --- /dev/null +++ b/ckanext-d4science_theme/ckanext/d4science_theme/validators.py @@ -0,0 +1,24 @@ +import ckan.plugins.toolkit as toolkit +from logging import getLogger + +log = getLogger(__name__) + +def ignore_duplicate_keys(key, data, errors, context): + """ + Validator che consente chiavi duplicate negli extras. + """ + # Estrarre gli extras dalla richiesta + log.debug("controllo extra (data) %s", data) + extras = data.get(key, []) + + # Log per il debugging + log.debug(f"Contenuto di 'data' per la chiave '{key}': {extras} (tipo: {type(extras)})") + + # Se ci sono duplicati, evita di sollevare un'eccezione + if isinstance(extras, list): # Verifica che extras sia una lista + log.debug(f"Ignorando chiave duplicata: {extra['key']}") + else: + log.debug(f"Errore: 'extras' non è una lista, ma è di tipo {type(extras)}") + + ## Mantieni la lista aggiornata, che consente chiavi duplicate + data[key] = extras \ No newline at end of file diff --git a/ckanext-d4science_theme/requirements.txt b/ckanext-d4science_theme/requirements.txt index 55f2a9b..0e23e19 100644 --- a/ckanext-d4science_theme/requirements.txt +++ b/ckanext-d4science_theme/requirements.txt @@ -1,3 +1,4 @@ webhelpers2 xmltodict pyqrcode +sqlalchemy diff --git a/ckanext-d4science_theme/setup.py b/ckanext-d4science_theme/setup.py index d3a3ba1..a616430 100644 --- a/ckanext-d4science_theme/setup.py +++ b/ckanext-d4science_theme/setup.py @@ -69,8 +69,7 @@ setup( # installed, specify them here. If using Python 2.6 or less, then these # have to be included in MANIFEST.in as well. include_package_data=True, - package_data={ - }, + #package_data={}, # Although 'package_data' is the preferred approach, in some case you may # need to place data files outside of your packages. From 610736b1c07f689f173ccd8fe47373aa686b592f Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Wed, 13 Nov 2024 15:52:11 +0100 Subject: [PATCH 02/10] feature: change template files according to template 2.6 (safe) --- .../templates/package/read.html | 47 ++++--- .../package/snippets/additional_info.html | 101 +++++++++++---- .../package/snippets/extras_table.html | 59 +++++++++ .../templates/package/snippets/info.html | 81 ++++++------ .../package/snippets/qrcdode_show.html | 6 + .../package/snippets/resource_item.html | 122 +++++++++++------- .../package/snippets/resources_list.html | 106 ++++++++++----- .../templates/snippets/tag_list.html | 2 +- 8 files changed, 363 insertions(+), 161 deletions(-) create mode 100644 ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/extras_table.html create mode 100644 ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/qrcdode_show.html diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/read.html b/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/read.html index db5061f..a851298 100644 --- a/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/read.html +++ b/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/read.html @@ -1,21 +1,24 @@ -{% extends "package/read_base.html" %} +{% ckan_extends %} +{% set dataset_extent = h.get_pkg_dict_extra(c.pkg_dict, 'spatial', '') %} +{% set d4science_cms_obj_placeholders = h.d4science_get_content_moderator_system_placeholder() %} +{% set moderation_item_status = h.get_pkg_dict_extra(c.pkg_dict,d4science_cms_obj_placeholders.item_status,'') %} {% block primary_content_inner %} - {{ super() }} - {% set dataset_extent = h.get_pkg_dict_extra(c.pkg_dict, 'spatial', '') %} - {% if dataset_extent %} - {% snippet "spatial/snippets/dataset_map.html", extent=dataset_extent %} - {% endif %} +{% if moderation_item_status %} + + {{ moderation_item_status }} + +{% endif %} {% block package_description %} {% if pkg.private %} - - + + {{ _('Private') }} {% endif %} -

    +
    {% block page_heading %} - {{ h.dataset_display_name(pkg) }} + {{ pkg.title or pkg.name }} {% if pkg.state.startswith('draft') %} [{{ _('Draft') }}] {% endif %} @@ -23,28 +26,34 @@ [{{ _('Deleted') }}] {% endif %} {% endblock %} -

    + {% block package_notes %} {% if pkg.notes %} -
    - {{ h.render_markdown(h.get_translated(pkg, 'notes')) }} +
    + {{ h.render_markdown(pkg.notes) }}
    {% endif %} {% endblock %} {# FIXME why is this here? seems wrong #} {% endblock %} + + + {% if dataset_extent %} + {% snippet "spatial/snippets/dataset_map.html", extent=dataset_extent %} + {% endif %} + + {% block package_tags %} +
    {{_('Tags')}}
    + {% snippet "package/snippets/tags.html", tags=pkg.tags %} + {% endblock %} {% block package_resources %} {% snippet "package/snippets/resources_list.html", pkg=pkg, resources=pkg.resources %} {% endblock %} - {% block package_tags %} - {% snippet "package/snippets/tags.html", tags=pkg.tags %} - {% endblock %} - {% block package_additional_info %} {% snippet "package/snippets/additional_info.html", pkg_dict=pkg %} {% endblock %} - -{% endblock %} + +{% endblock %} \ No newline at end of file diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/additional_info.html b/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/additional_info.html index d4070ce..e058199 100644 --- a/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/additional_info.html +++ b/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/additional_info.html @@ -1,5 +1,74 @@ +{% set key_item_url = _('Item') + ' URL' %} + + + + +
    -

    {{ _('Additional Info') }}

    + {% block extras scoped %} + {# + This performs a sort + {% for extra in h.sorted_extras(pkg_dict.extras) %} + #} + {% if pkg_dict.extras %} + {# Added by Francesco Mangiacrapa, see 17901 #} + {% set extra_item_url = h.get_pkg_dict_extra(pkg_dict,key_item_url) %} + {% if extra_item_url %} +
    {{ key_item_url }}
    + + + + + +
    {{ extra_item_url }}{% snippet "package/snippets/qrcode_show.html", package_url=extra_item_url %}
    + {% endif %} + {% set extras_indexed_for_categories = h.d4science_get_extras_indexed_for_namespaces(pkg_dict.extras) %} + {% for k_cat in extras_indexed_for_categories %} + {% set category_idx = extras_indexed_for_categories[k_cat] %} + {% if(k_cat!='nocategory') %} +
    {{category_idx.category.title}}
    + {% if category_idx.category.description %} +

    Description: {{category_idx.category.description}}

    + {% endif %} + + + + + + + + + {% set my_extras = h.d4science_get_extra_for_category(extras_indexed_for_categories, k_cat) %} + {% snippet "package/snippets/extras_table.html", my_extras=my_extras, key_item_url=key_item_url %} + +
    {{ _('Field') }}{{ _('Value') }}
    +
    + {% endif %} + {% endfor %} + {% set my_extras = h.d4science_get_extra_for_category(extras_indexed_for_categories, 'nocategory') %} + {% if my_extras|length > 0 %} +
    {{ _('Additional Info') }}
    + + + + + + + + + {% snippet "package/snippets/extras_table.html", my_extras=my_extras, key_item_url=key_item_url %} + +
    {{ _('Field') }}{{ _('Value') }}
    +
    + {% endif %} + {% endif %} + {% endblock %} +
    {{ _('Management Info') }}
    @@ -13,11 +82,7 @@ {% if h.is_url(pkg_dict.url) %} - + {% else %} {% endif %} @@ -35,16 +100,17 @@ {% endif %} - + {# Added by Francesco Mangiacrapa #} + {% set user_maintainer = h.d4science_get_user_info(pkg_dict.maintainer) %} {% if pkg_dict.maintainer_email %} - + {% elif pkg_dict.maintainer %} - + {% endif %} @@ -78,18 +144,7 @@ {% endif %} - - {% block extras scoped %} - {% for extra in h.sorted_extras(pkg_dict.extras) %} - {% set key, value = extra %} - - - - - {% endfor %} - {% endblock %} - - {% endblock %} - +
    {{ _('Source') }} - - {{ pkg_dict.url }} - - {{ h.link_to(pkg_dict.url, pkg_dict.url, rel='foaf:homepage', target='_blank') }}{{ pkg_dict.url }}{{ pkg_dict.author }}
    {{ _('Maintainer') }}{{ h.mail_to(email_address=pkg_dict.maintainer_email, name=pkg_dict.maintainer) }}{{ h.mail_to(email_address=pkg_dict.maintainer_email, name=user_maintainer.fullname if (user_maintainer and user_maintainer.fullname) else pkg_dict.maintainer) }}
    {{ _('Maintainer') }}{{ pkg_dict.maintainer }}{{ user_maintainer.fullname if (user_maintainer and user_maintainer.fullname) else pkg_dict.maintainer }}
    {{ _(key|e) }}{{ value }}
    -
    + {% endblock %} + \ No newline at end of file diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/extras_table.html b/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/extras_table.html new file mode 100644 index 0000000..4a809cc --- /dev/null +++ b/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/extras_table.html @@ -0,0 +1,59 @@ + + + + {% set d4science_cms_obj_placeholders = h.d4science_get_content_moderator_system_placeholder() %} + {% for extra_lnk in my_extras %} + {% set index = loop.index %} + {% for k in extra_lnk.keys() %} + {# Added by Francesco Mangiacrapa, see: #21701 #} + {% set extra_value = extra_lnk[k] %} + + {% if extra_value is defined and extra_value|length %} + {# Added by Francesco Mangiacrapa, see: #7055 #} + {% set isHttp = extra_value.startswith(('http://', 'https://')) %} + + + {% if k != key_item_url and (not k.startswith(d4science_cms_obj_placeholders.prefix)) %} + {{ _(k) }} + {% if isHttp %} + {% if k == 'graphic-preview-file'%} + {{ + {% else %} + {{ extra_value }} + {% endif %} + {% elif k == 'responsible-party' %} +
    {{ extra_value }}
    + {% elif k == 'dataset-reference-date' %} +
    {{ extra_value }}
    + {% elif k == 'coupled-resource' %} +
    {{ extra_value }}
    + {% elif k.startswith('Zenodo') %} + + {% elif k == 'spatial' %} +
    {{ extra_value }}
    + {% else %} + {{ extra_value }} + {% endif %} + {% endif %} + {% endif %} + {% endfor %} + {% endfor %} \ No newline at end of file diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/info.html b/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/info.html index 20678ff..60420b8 100644 --- a/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/info.html +++ b/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/info.html @@ -1,41 +1,48 @@ {# -Displays a sidebar module with information for given package - -pkg - The package dict that owns the resources. - -Example: - - {% snippet "package/snippets/info.html", pkg=pkg %} - -#} -{% block package_info %} - {% if pkg %} -
    -
    -
    - {% block package_info_inner %} - {% block heading %} -

    {{ h.dataset_display_name(pkg) }}

    - {% endblock %} - {% block nums %} - {% set num_followers = h.follow_count('dataset', pkg.id) %} -
    -
    -
    {{ _('Followers') }}
    -
    {{ h.SI_number_span(num_followers) }}
    -
    -
    - {% endblock %} - {% block follow_button %} - {% if not hide_follow_button %} - -
    - {% endif %} -{% endblock %} + + {% endif %} + {% endblock %} + \ No newline at end of file diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/qrcdode_show.html b/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/qrcdode_show.html new file mode 100644 index 0000000..a0ec361 --- /dev/null +++ b/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/qrcdode_show.html @@ -0,0 +1,6 @@ +{% set qr_code_image = h.d4science_get_qrcode_for_url(package_url) %} +{% block package_qrcode_for_url %} +
    {{
    +{% endblock %} + + \ No newline at end of file diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resource_item.html b/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resource_item.html index 31d1c56..0f668fc 100644 --- a/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resource_item.html +++ b/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resource_item.html @@ -1,82 +1,108 @@ -{# - Renders a single resource with icons and view links. - - res - A resource dict to render - pkg - A package dict that the resource belongs to - can_edit - Whether the user is allowed to edit the resource - url_is_edit - Whether the link to the resource should be to editing it (set to False to make the link view the resource) - url - URL of the resource details page(resource edit/read depending on url_is_edit, by default). - - Example: - - {% snippet "package/snippets/resource_item.html", res=resource, pkg=pkg, can_edit=True, url_is_edit=False %} - -#} -{% set url_action = pkg.type ~ ('_resource.edit' if url_is_edit and can_edit else '_resource.read') %} -{% set url = url or h.url_for(url_action, id=pkg.name, resource_id=res.id) %} - +{% set can_edit = h.check_access('package_update', {'id':pkg.id }) %} +{% set url_action = 'resource_edit' if url_is_edit and can_edit else 'resource_read' %} +{# {% set url = h.url_for(controller='package', action=url_action, id=pkg.name, resource_id=res.id) %} #} +{% set url = h.url_for('dataset_resource.read', id=pkg.name, resource_id=res.id) %}
  • +{# Added by Francesco Mangiacrapa block custom_view_on_resources see:4851 #} +{% block custom_view_on_resources %} +{% set user = c.user %} +{% if user %} {% block resource_item_title %} - {{ h.resource_display_name(res) | truncate(50) }}{{ h.get_translated(res, 'format') }} - {{ h.popular('views', res.tracking_summary.total, min=10) if res.tracking_summary }} + {{ h.resource_display_name(res) | truncate(50) }}{{ res.format }} + {{ h.popular('views', res.tracking_summary.total, min=10) }} {% endblock %} {% block resource_item_description %}

    {% if res.description %} - {{ h.markdown_extract(h.get_translated(res, 'description'), extract_length=80) }} + {{ h.markdown_extract(res.description, extract_length=80) }} {% endif %}

    {% endblock %} {% block resource_item_explore %} {% if not url_is_edit %} + {# Only if can edit, explorer button is shown with several facility #} + {% if can_edit %} + {% else %} + + {{ _('Go to resource') }} + + {% endif %} {% endif %} {% endblock %} +{% else %} + {% block resource_item_title2 %} + {# Updated by Francesco Mangiacrapa, see: #10056 #} + + {{ h.resource_display_name(res) | truncate(50) }}{{ res.format }} + {{ h.popular('views', res.tracking_summary.total, min=10) }} + + {% endblock %} +
    + {% block resource_item_description2 %} +

    + {% if res.description %} + {{ h.markdown_extract(res.description, extract_length=80) }} + {% endif %} +

    + {% endblock %} + The resource: '{{ h.resource_display_name(res) | truncate(30) }}' is not accessible as guest user. You must login to access it! +
    +{% endif %} +{% endblock %}
  • + diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resources_list.html b/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resources_list.html index 9c6dd63..3ab3b78 100644 --- a/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resources_list.html +++ b/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resources_list.html @@ -1,37 +1,77 @@ -{# +{# Renders a list of resources with icons and view links. - - resources - A list of resources (dicts) to render - pkg - A package dict that the resources belong to. - + + resources - A list of resources to render + pkg - A package object that the resources belong to. + Example: - - {% snippet "package/snippets/resources_list.html", pkg=pkg, resources=pkg.resources %} - + + {% snippet "package/snippets/resources_list.html", pkg=pkg, resources=pkg.resources %} + #} - -
    -

    {{ _('Data and Resources') }}

    - {% block resource_list %} - {% if resources %} -
      - {% block resource_list_inner %} - {% set can_edit = can_edit or h.check_access('package_update', {'id':pkg.id }) %} - {% for resource in resources %} - {% snippet 'package/snippets/resource_item.html', pkg=pkg, res=resource, can_edit=can_edit %} - {% endfor %} - {% endblock %} -
    - {% else %} - {% block resource_list_empty %} - {% if h.check_access('resource_create', {'package_id': pkg['id']}) %} - {% trans url=h.url_for(pkg.type ~ '_resource.new', id=pkg.name) %} -

    This dataset has no data, why not add some?

    - {% endtrans %} - {% else %} -

    {{ _('This dataset has no data') }}

    - {% endif %} - {% endblock %} + + + +
    +
    {{ _('Data and Resources') }}
    + {% set user = c.user %} + {# Added by Francesco Mangiacrapa #10389 #} + {% if not user %} +
    To access the resources you must log in
    {% endif %} - {% endblock %} -
    + {# end #} + {% block resource_list %} + {% if resources %} +
      + {% block resource_list_inner %} + {% for resource in resources %} + {% snippet 'package/snippets/resource_item.html', pkg=pkg, res=resource %} + {% endfor %} + {% endblock %} +
    + {% else %} + {% if h.check_access('resource_create', {'package_id': pkg['id']}) %} + {% trans url=h.url_for(controller='package', action='new_resource', id=pkg.name) %} +

    This dataset has no data, why not add some?

    + {% endtrans %} + {% else %} +

    {{ _('This dataset has no data') }}

    + {% endif %} + {% endif %} + {% endblock %} +
    + \ No newline at end of file diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/tag_list.html b/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/tag_list.html index 77f753e..52c9c8d 100644 --- a/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/tag_list.html +++ b/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/tag_list.html @@ -13,7 +13,7 @@ From 401fcbe1c7327ea5dc79a7bf189dfbaef7826421 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Wed, 13 Nov 2024 15:52:26 +0100 Subject: [PATCH 03/10] fix: add comment --- ckanext-d4science_theme/ckanext/d4science_theme/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/validators.py b/ckanext-d4science_theme/ckanext/d4science_theme/validators.py index f9780b7..0168c54 100644 --- a/ckanext-d4science_theme/ckanext/d4science_theme/validators.py +++ b/ckanext-d4science_theme/ckanext/d4science_theme/validators.py @@ -21,4 +21,4 @@ def ignore_duplicate_keys(key, data, errors, context): log.debug(f"Errore: 'extras' non è una lista, ma è di tipo {type(extras)}") ## Mantieni la lista aggiornata, che consente chiavi duplicate - data[key] = extras \ No newline at end of file + #data[key] = extras \ No newline at end of file From 890b920e1b785e16adae1e538639f91874d7c5be Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Thu, 28 Nov 2024 15:19:55 +0100 Subject: [PATCH 04/10] add duplicate keys in extras (missing delete and update) --- .../ckanext/d4science_theme/plugin.py | 350 +++++++++++------- 1 file changed, 212 insertions(+), 138 deletions(-) diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py b/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py index 9431c59..e6c9b30 100644 --- a/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py +++ b/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py @@ -10,16 +10,28 @@ import ckan.model as model from ckanext.d4science_theme.controllers.home import d4SHomeController from ckanext.d4science_theme.controllers.systemtype import d4STypeController from ckanext.d4science_theme.controllers.organization import OrganizationVREController +import sqlalchemy as sa +from ckan.config.middleware.common_middleware import TrackingMiddleware +import ckan.lib.dictization.model_save as model_save #from ckan.controllers.home import HomeController #from ckan.plugins import IRoutes from flask import Blueprint, render_template +from ckan.types import Context + +from typing import ( + Any, Collection, Optional, TYPE_CHECKING, Type, Union, cast, overload, + Literal, +) + +if TYPE_CHECKING: + import ckan.model as modelDict from ckan.common import ( g ) from flask import Flask, g from ckan.lib.app_globals import app_globals -import ckan.plugins.toolkit as toolkit +from ckan.logic import get_action # Created by Francesco Mangiacrapa @@ -38,145 +50,178 @@ def remove_check_replicated_custom_key(schema): #CREATED BY FRANCESCO MANGIACRAPA FOR OVERRIDING THE package_extras_save FROM dictization.model_save.py # Is this needed? -def _package_extras_save(extra_dicts, obj, context): - ''' It can save repeated extras as key-value ''' +def _package_extras_save(extra_dicts: Optional[list[dict[str, Any]]], pkg: 'model.Package', + context: Context) -> None: allow_partial_update = context.get("allow_partial_update", False) if extra_dicts is None and allow_partial_update: + log.debug("extra dicts is NONE") return - - model = context["model"] + + log.debug("USING CUSTOM SAVE") + #pass + #model = context["model"] session = context["session"] - - #ADDED BY FRANCESCO MANGIACRAPA - log.debug("extra_dicts: %s", extra_dicts) - #print "extra_dicts: "+str(extra_dicts) - - extras_list = obj.extras_list - #extras = dict((extra.key, extra) for extra in extras_list) - old_extras = {} - extras = {} - for extra in extras_list or []: - old_extras.setdefault(extra.key, []).append(extra.value) - extras.setdefault(extra.key, []).append(extra) + + old_extras = pkg._extras + log.debug("OLD_EXTRAS", old_extras) - #ADDED BY FRANCESCO MANGIACRAPA - #print "old_extras: "+str(old_extras) - - new_extras = {} + new_extras = [] # Lista di dizionari per supportare chiavi duplicate for extra_dict in extra_dicts or []: - #print 'extra_dict key: '+extra_dict["key"] + ', value: '+extra_dict["value"] - #new_extras.setdefault(extra_dict["key"], []).append(extra_dict["value"]) if extra_dict.get("deleted"): - log.debug("extra_dict deleted: %s ", extra_dict["key"]) - #print 'extra_dict deleted: '+extra_dict["key"] continue - #if extra_dict['value'] is not None and not extra_dict["value"] == "": - if extra_dict['value'] is not None: - new_extras.setdefault(extra_dict["key"], []).append(extra_dict["value"]) + if extra_dict['value'] is None: + pass + else: + new_extras.append({"key": extra_dict["key"], "value": extra_dict["value"]}) - #ADDED BY FRANCESCO MANGIACRAPA - log.debug("new_extras: %s", new_extras) - #print "new_extras: "+str(new_extras) + # new + log.debug("new_extra:", new_extras) + for new_extra in new_extras: + new_key = new_extra["key"] + new_value = new_extra["value"] - #new - for key in set(new_extras.keys()) - set(old_extras.keys()): - state = 'active' - log.debug("adding key: %s", key) - #print "adding key: "+str(key) - extra_lst = new_extras[key] - for extra in extra_lst: - extra = model.PackageExtra(state=state, key=key, value=extra) - session.add(extra) - extras_list.append(extra) + # Verifica se l'extra esiste già in old_extras con lo stesso valore + if not any(extra.key == new_key and extra.value == new_value for extra in old_extras.values()): + # Crea un nuovo extra solo se non esiste già proviamo ad usare il modello packageExtra + #extra = {"key": key, "value": value} + extra = model.PackageExtra(key = new_key, value = new_value, package_id = pkg.id, state= 'active') #state? + log.debug("extra:", extra) + session.add(extra) + #pkg.extras[key] = value + #log.debug("pkg.extras[key]", pkg.extras) + + # changed + for extra in old_extras.values(): + matching_new_extras = [new_extra for new_extra in new_extras if new_extra["key"] == extra.key] + log.debug("MATCHING EXTRAS", matching_new_extras) + if matching_new_extras: + for new_extra in matching_new_extras: + if new_extra["value"] != extra.value: + # Aggiorna il valore se differente + extra.value = new_extra["value"] + extra.state = 'active' + log.debug("changed", extra) + #session.add(extra) per l'update non dovrebbe servire + + # deleted + log.debug("Pre delete", old_extras, old_extras.values()) + log.debug("new extra pre delete", new_extra) + to_delete = [ + extra for extra in old_extras.values() + if not any(new_extra["key"] == extra.key and new_extra["value"] == extra.value for new_extra in new_extras) + ] + log.debug("TO DELETE", to_delete) + for extra in to_delete: + log.debug('delete extra', extra) + extra.state = 'deleted' + session.delete(extra) - #deleted - for key in set(old_extras.keys()) - set(new_extras.keys()): - log.debug("deleting key: "+str(key)) - #print "deleting key: "+str(key) - extra_lst = extras[key] - for extra in extra_lst: - state = 'deleted' - extra.state = state - extras_list.remove(extra) - - #changed - for key in set(new_extras.keys()) & set(old_extras.keys()): - #for each value of new list - for value in new_extras[key]: - old_occur = old_extras[key].count(value) - new_occur = new_extras[key].count(value) - log.debug("value: %s\n new_occur: %s\n old_occur: %s", value, new_occur, old_occur) - #print "value: "+str(value) + ", new_occur: "+str(new_occur) + ", old_occur: "+str(old_occur) - # it is an old value deleted or not - if value in old_extras[key]: - if old_occur == new_occur: - #print "extra - occurrences of: "+str(value) +", are equal into both list" - log.debug("extra - occurrences of: %s are equal into both list", value) - #there is a little bug, this code return always the first element, so I'm fixing with #FIX-STATUS - extra_values = get_package_for_value(extras[key], value) - #extras_list.append(extra) - for extra in extra_values: - state = 'active' - extra.state = state - session.add(extra) - #print "extra updated: "+str(extra) - log.debug("extra updated: %s", extra) - - elif new_occur > old_occur: - #print "extra - a new occurrence of: "+str(value) +", is present into new list, adding it to old list" - log.debug("extra - a new occurrence of: %s, is present into new list, adding it to old list", value) - state = 'active' - extra = model.PackageExtra(state=state, key=key, value=value) - extra.state = state - session.add(extra) - extras_list.append(extra) - old_extras[key].append(value) - log.debug("old extra values updated: %s", old_extras[key]) - #print "old extra values updated: "+str(old_extras[key]) - - else: - #remove all occurrences deleted - this code could be optimized, it is run several times but could be performed one shot - countDelete = old_occur-new_occur - log.debug("extra - occurrence of: %s, is not present into new list, removing: %s occurrence/s from old list", value, countDelete) - #print "extra - occurrence of: "+str(value) +", is not present into new list, removing "+str(countDelete)+" occurrence/s from old list" - extra_values = get_package_for_value(extras[key], value) - for idx, extra in enumerate(extra_values): - if idx < countDelete: - #print "extra - occurrence of: "+str(value) +", is not present into new list, removing it from old list" - log.debug("pkg extra deleting: %s", extra.value) - #print "pkg extra deleting: "+str(extra.value) - state = 'deleted' - extra.state = state - - else: - #print "pkg extra reactivating: "+str(extra.value) - log.debug("pkg extra reactivating: %s", extra.value) - state = 'active' - extra.state = state - session.add(extra) - - else: - #print "extra new value: "+str(value) - log.debug("extra new value: %s", value) - state = 'active' - extra = model.PackageExtra(state=state, key=key, value=value) - extra.state = state - session.add(extra) - extras_list.append(extra) - - - #for each value of old list - for value in old_extras[key]: - #if value is not present in new list - if value not in new_extras[key]: - extra_values = get_package_for_value(extras[key], value) - for extra in extra_values: - #print "not present extra deleting: "+str(extra) - log.debug("not present extra deleting: "+str(extra)) - state = 'deleted' - extra.state = state + ##ADDED BY FRANCESCO MANGIACRAPA + # + #extras_list = pkg.extras + ##extras_list = list(obj.extras) + ##extras = dict((extra.key, extra) for extra in extras_list) + #old_extras = {} + ##old_extras = {extra.key: extra for extra in extras_list} + #extras = {} + #for extra in extras_list or []: + # old_extras.setdefault(extra.key, []).append(extra.value) + # extras.setdefault(extra.key, []).append(extra) + # + ##ADDED BY FRANCESCO MANGIACRAPA + ##print "old_extras: "+str(old_extras) + # + #new_extras = {} + #for extra_dict in extra_dicts or []: + # #new_extras.setdefault(extra_dict["key"], []).append(extra_dict["value"]) + # if extra_dict.get("deleted"): + # continue + # + # #if extra_dict['value'] is not None and not extra_dict["value"] == "": + # if extra_dict['value'] is not None: + # new_extras.setdefault(extra_dict["key"], []).append(extra_dict["value"]) + # + ##ADDED BY FRANCESCO MANGIACRAPA + #log.debug("new_extras: %s", new_extras) + ##print "new_extras: "+str(new_extras) + # + ##new + #for key in set(new_extras.keys()) - set(old_extras.keys()): + # state = 'active' + # extra_lst = new_extras[key] + # for extra in extra_lst: + # extra = model.PackageExtra(state=state, key=key, value=extra) + # session.add(extra) + # extras_list.append(extra) + # + ##deleted + #for key in set(old_extras.keys()) - set(new_extras.keys()): + # extra_lst = extras[key] + # for extra in extra_lst: + # state = 'deleted' + # extra.state = state + # extras_list.remove(extra) + # + ##changed + #for key in set(new_extras.keys()) & set(old_extras.keys()): + # #for each value of new list + # for value in new_extras[key]: + # old_occur = old_extras[key].count(value) + # new_occur = new_extras[key].count(value) + # # it is an old value deleted or not + # if value in old_extras[key]: + # if old_occur == new_occur: + # #there is a little bug, this code return always the first element, so I'm fixing with #FIX-STATUS + # extra_values = get_package_for_value(extras[key], value) + # #extras_list.append(extra) + # for extra in extra_values: + # state = 'active' + # extra.state = state + # session.add(extra) + # #print "extra updated: "+str(extra) + # log.debug("extra updated: %s", extra) + # + # elif new_occur > old_occur: + # state = 'active' + # extra = model.PackageExtra(state=state, key=key, value=value) + # extra.state = state + # session.add(extra) + # extras_list.append(extra) + # old_extras[key].append(value) + # + # else: + # #remove all occurrences deleted - this code could be optimized, it is run several times but could be performed one shot + # countDelete = old_occur-new_occur + # extra_values = get_package_for_value(extras[key], value) + # for idx, extra in enumerate(extra_values): + # if idx < countDelete: + # state = 'deleted' + # extra.state = state + # + # else: + # state = 'active' + # extra.state = state + # session.add(extra) + # + # else: + # state = 'active' + # extra = model.PackageExtra(state=state, key=key, value=value) + # extra.state = state + # session.add(extra) + # extras_list.append(extra) + # + # + # #for each value of old list + # for value in old_extras[key]: + # #if value is not present in new list + # if value not in new_extras[key]: + # extra_values = get_package_for_value(extras[key], value) + # for extra in extra_values: + # state = 'deleted' + # extra.state = state + #ADDED BY FRANCESCO MANGIACRAPA def get_package_for_value(list_package, value): @@ -191,6 +236,31 @@ def get_package_for_value(list_package, value): return lst +#OVERRIDING BASE SQL ALCHEMY ENGINE INSTANCE serve per la connessione con gcube? +def _init_TrackingMiddleware(self, app, config): + self.app = app + log.debug('TrackingMiddleware d4Science instance') + sqlalchemy_url = config.get('sqlalchemy.url') + log.debug('sqlalchemy_url read: '+str(sqlalchemy_url)) + + sqlalchemy_pool = config.get('sqlalchemy.pool_size') + if sqlalchemy_pool is None: + sqlalchemy_pool = 5 + + log.debug('sqlalchemy_pool read: '+str(sqlalchemy_pool)) + sqlalchemy_overflow = config.get('sqlalchemy.max_overflow') + + if sqlalchemy_overflow is None: + sqlalchemy_overflow = 10 + + log.debug('sqlalchemy_overflow read: '+str(sqlalchemy_overflow)) + + try: + self.engine = sa.create_engine(sqlalchemy_url, pool_size=int(sqlalchemy_pool), max_overflow=int(sqlalchemy_overflow)) + except TypeError as e: + log.error('pool size does not work: ' +str(e.args)) + self.engine = sa.create_engine(sqlalchemy_url) + class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm): plugins.implements(plugins.IConfigurer) @@ -220,7 +290,6 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) # that we'll use to refer to this fanstatic directory from CKAN # templates. toolkit.add_resource('assets', 'd4science_theme') - # toolkit.add_resource('assets', 'd4science_scripts') def _modify_package_schema(self): log.debug("*** modify package ***") @@ -242,30 +311,33 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) # let's grab the default schema in our plugin log.debug("creating package....") schema = super(D4Science_ThemePlugin, self).create_package_schema() - log.debug("schema after create prima del validator %s", schema) - #schema = remove_check_replicated_custom_key(schema) + #log.debug("schema after create prima del validator %s", schema) + schema = remove_check_replicated_custom_key(schema) + #log.debug("create_package1 (remove __before): %s", schema) #schema.update(self._modify_package_schema(schema)) schema.update(self._modify_package_schema()) #d.package_dict_save = _package_dict_save - log.debug("create_package1 %s", schema) + #log.debug("create_package2 (remove extras validator): %s", schema) return schema #IDatasetForm def update_package_schema(self): log.debug("** update_package **") schema = super(D4Science_ThemePlugin, self).update_package_schema() - #schema = remove_check_replicated_custom_key(schema) + schema = remove_check_replicated_custom_key(schema) + #log.debug("update_package1 (remove __before) %s", schema) #schema.update(self._modify_package_schema(schema)) schema.update(self._modify_package_schema()) - log.debug("update_package1 %s", schema) + #log.debug("update_package2 (remove extras validator) %s", schema) return schema #IDatasetForm def show_package_schema(self): log.debug("** show package **") schema = super(D4Science_ThemePlugin, self).show_package_schema() - log.debug("show_package1 %s", schema) - #schema = remove_check_replicated_custom_key(schema) + #log.debug("show_package1 %s", schema) + schema = remove_check_replicated_custom_key(schema) + #log.debug("show_package1.5 no before %s", schema) #schema.update(self._modify_package_schema(schema)) schema.update({ 'extras': { @@ -274,7 +346,7 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) 'value': [toolkit.get_validator('not_missing')] } }) - log.debug("show_package2 %s", schema) + #log.debug("show_package2 %s", schema) return schema #IDatasetForm @@ -424,7 +496,7 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) #Overriding package_extras_save method # Is this needed? - # model_save.package_extras_save = _package_extras_save + model_save.package_extras_save = _package_extras_save #Overriding index home controller - rimosso in ckan 2.10 #d4sHC = d4SHomeController() @@ -432,6 +504,9 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) global d4s_ctg_namespaces_controller + #OVERRIDING BASE SQL ALCHEMY ENGINE INSTANCE + TrackingMiddleware.__init__ = _init_TrackingMiddleware + #if d4s_ctg_namespaces_controller is None: # log.info("d4s_ctg_namespaces_controller instancing...") # d4s_ctg_namespaces_controller = helpers.get_d4s_namespace_controller() @@ -567,4 +642,3 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) return facets_dict - From 691798a2b96a276c8be179f64f9b27315539431f Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Fri, 29 Nov 2024 14:36:32 +0100 Subject: [PATCH 05/10] feat: duplicate key in extras update d4science theme to ckan 2.10 --- .../ckanext/d4science_theme/plugin.py | 312 +++++------------- 1 file changed, 91 insertions(+), 221 deletions(-) diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py b/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py index e6c9b30..0a50fa7 100644 --- a/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py +++ b/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py @@ -23,21 +23,12 @@ from typing import ( Literal, ) -if TYPE_CHECKING: - import ckan.model as modelDict - from ckan.common import ( g ) from flask import Flask, g -from ckan.lib.app_globals import app_globals -from ckan.logic import get_action -# Created by Francesco Mangiacrapa -# francesco.mangiacrapa@isti.cnr.it -# ISTI-CNR Pisa (ITALY) - log = getLogger(__name__) d4s_ctg_namespaces_controller = None @@ -48,193 +39,112 @@ def remove_check_replicated_custom_key(schema): return schema -#CREATED BY FRANCESCO MANGIACRAPA FOR OVERRIDING THE package_extras_save FROM dictization.model_save.py -# Is this needed? -def _package_extras_save(extra_dicts: Optional[list[dict[str, Any]]], pkg: 'model.Package', +def _package_extras_save( + extra_dicts: Optional[list[dict[str, Any]]], pkg: 'model.Package', context: Context) -> None: allow_partial_update = context.get("allow_partial_update", False) if extra_dicts is None and allow_partial_update: - log.debug("extra dicts is NONE") return - - log.debug("USING CUSTOM SAVE") - #pass - #model = context["model"] + session = context["session"] - - old_extras = pkg._extras - log.debug("OLD_EXTRAS", old_extras) + model = context["model"] + #extras_list = obj.extras_list + extras_list = session.query(model.PackageExtra).filter_by(package_id=pkg.id).all() + #extras = dict((extra.key, extra) for extra in extras_list) + old_extras = {} + extras = {} + for extra in extras_list or []: + old_extras.setdefault(extra.key, []).append(extra.value) + extras.setdefault(extra.key, []).append(extra) - new_extras = [] # Lista di dizionari per supportare chiavi duplicate + new_extras = {} for extra_dict in extra_dicts or []: + #new_extras.setdefault(extra_dict["key"], []).append(extra_dict["value"]) if extra_dict.get("deleted"): continue - if extra_dict['value'] is None: - pass - else: - new_extras.append({"key": extra_dict["key"], "value": extra_dict["value"]}) + #if extra_dict['value'] is not None and not extra_dict["value"] == "": + if extra_dict['value'] is not None: + new_extras.setdefault(extra_dict["key"], []).append(extra_dict["value"]) - # new - log.debug("new_extra:", new_extras) - for new_extra in new_extras: - new_key = new_extra["key"] - new_value = new_extra["value"] - - # Verifica se l'extra esiste già in old_extras con lo stesso valore - if not any(extra.key == new_key and extra.value == new_value for extra in old_extras.values()): - # Crea un nuovo extra solo se non esiste già proviamo ad usare il modello packageExtra - #extra = {"key": key, "value": value} - extra = model.PackageExtra(key = new_key, value = new_value, package_id = pkg.id, state= 'active') #state? - log.debug("extra:", extra) - session.add(extra) - #pkg.extras[key] = value - #log.debug("pkg.extras[key]", pkg.extras) - - # changed - for extra in old_extras.values(): - matching_new_extras = [new_extra for new_extra in new_extras if new_extra["key"] == extra.key] - log.debug("MATCHING EXTRAS", matching_new_extras) - if matching_new_extras: - for new_extra in matching_new_extras: - if new_extra["value"] != extra.value: - # Aggiorna il valore se differente - extra.value = new_extra["value"] - extra.state = 'active' - log.debug("changed", extra) - #session.add(extra) per l'update non dovrebbe servire - - # deleted - log.debug("Pre delete", old_extras, old_extras.values()) - log.debug("new extra pre delete", new_extra) - to_delete = [ - extra for extra in old_extras.values() - if not any(new_extra["key"] == extra.key and new_extra["value"] == extra.value for new_extra in new_extras) - ] - log.debug("TO DELETE", to_delete) - for extra in to_delete: - log.debug('delete extra', extra) - extra.state = 'deleted' - session.delete(extra) + #new + for key in set(new_extras.keys()) - set(old_extras.keys()): + state = 'active' + extra_lst = new_extras[key] + for extra in extra_lst: + extra = model.PackageExtra(state=state, key=key, value=extra, package_id = pkg.id) + session.add(extra) + extras_list.append(extra) + #deleted + for key in set(old_extras.keys()) - set(new_extras.keys()): + extra_lst = extras[key] + for extra in extra_lst: + state = 'deleted' + extra.state = state + extras_list.remove(extra) + + #changed + for key in set(new_extras.keys()) & set(old_extras.keys()): + #for each value of new list + for value in new_extras[key]: + old_occur = old_extras[key].count(value) + new_occur = new_extras[key].count(value) + # it is an old value deleted or not + if value in old_extras[key]: + if old_occur == new_occur: + #there is a little bug, this code return always the first element, so I'm fixing with #FIX-STATUS + extra_values = get_package_for_value(extras[key], value) + #extras_list.append(extra) + for extra in extra_values: + state = 'active' + extra.state = state + session.add(extra) + + elif new_occur > old_occur: + state = 'active' + extra = model.PackageExtra(state=state, key=key, value=value, package_id = pkg.id) + extra.state = state + session.add(extra) + extras_list.append(extra) + old_extras[key].append(value) + + else: + #remove all occurrences deleted - this code could be optimized, it is run several times but could be performed one shot + countDelete = old_occur-new_occur + extra_values = get_package_for_value(extras[key], value) + for idx, extra in enumerate(extra_values): + if idx < countDelete: + state = 'deleted' + extra.state = state + + else: + state = 'active' + extra.state = state + session.add(extra) #valuta se metterlo dentro il for, ma fuori dall'else + + else: + state = 'active' + extra = model.PackageExtra(state=state, key=key, value=value, package_id = pkg.id) + extra.state = state + session.add(extra) + extras_list.append(extra) + + + #for each value of old list + for value in old_extras[key]: + #if value is not present in new list + if value not in new_extras[key]: + extra_values = get_package_for_value(extras[key], value) + for extra in extra_values: + state = 'deleted' + extra.state = state + #add session.delete(extra)? - ##ADDED BY FRANCESCO MANGIACRAPA - # - #extras_list = pkg.extras - ##extras_list = list(obj.extras) - ##extras = dict((extra.key, extra) for extra in extras_list) - #old_extras = {} - ##old_extras = {extra.key: extra for extra in extras_list} - #extras = {} - #for extra in extras_list or []: - # old_extras.setdefault(extra.key, []).append(extra.value) - # extras.setdefault(extra.key, []).append(extra) - # - ##ADDED BY FRANCESCO MANGIACRAPA - ##print "old_extras: "+str(old_extras) - # - #new_extras = {} - #for extra_dict in extra_dicts or []: - # #new_extras.setdefault(extra_dict["key"], []).append(extra_dict["value"]) - # if extra_dict.get("deleted"): - # continue - # - # #if extra_dict['value'] is not None and not extra_dict["value"] == "": - # if extra_dict['value'] is not None: - # new_extras.setdefault(extra_dict["key"], []).append(extra_dict["value"]) - # - ##ADDED BY FRANCESCO MANGIACRAPA - #log.debug("new_extras: %s", new_extras) - ##print "new_extras: "+str(new_extras) - # - ##new - #for key in set(new_extras.keys()) - set(old_extras.keys()): - # state = 'active' - # extra_lst = new_extras[key] - # for extra in extra_lst: - # extra = model.PackageExtra(state=state, key=key, value=extra) - # session.add(extra) - # extras_list.append(extra) - # - ##deleted - #for key in set(old_extras.keys()) - set(new_extras.keys()): - # extra_lst = extras[key] - # for extra in extra_lst: - # state = 'deleted' - # extra.state = state - # extras_list.remove(extra) - # - ##changed - #for key in set(new_extras.keys()) & set(old_extras.keys()): - # #for each value of new list - # for value in new_extras[key]: - # old_occur = old_extras[key].count(value) - # new_occur = new_extras[key].count(value) - # # it is an old value deleted or not - # if value in old_extras[key]: - # if old_occur == new_occur: - # #there is a little bug, this code return always the first element, so I'm fixing with #FIX-STATUS - # extra_values = get_package_for_value(extras[key], value) - # #extras_list.append(extra) - # for extra in extra_values: - # state = 'active' - # extra.state = state - # session.add(extra) - # #print "extra updated: "+str(extra) - # log.debug("extra updated: %s", extra) - # - # elif new_occur > old_occur: - # state = 'active' - # extra = model.PackageExtra(state=state, key=key, value=value) - # extra.state = state - # session.add(extra) - # extras_list.append(extra) - # old_extras[key].append(value) - # - # else: - # #remove all occurrences deleted - this code could be optimized, it is run several times but could be performed one shot - # countDelete = old_occur-new_occur - # extra_values = get_package_for_value(extras[key], value) - # for idx, extra in enumerate(extra_values): - # if idx < countDelete: - # state = 'deleted' - # extra.state = state - # - # else: - # state = 'active' - # extra.state = state - # session.add(extra) - # - # else: - # state = 'active' - # extra = model.PackageExtra(state=state, key=key, value=value) - # extra.state = state - # session.add(extra) - # extras_list.append(extra) - # - # - # #for each value of old list - # for value in old_extras[key]: - # #if value is not present in new list - # if value not in new_extras[key]: - # extra_values = get_package_for_value(extras[key], value) - # for extra in extra_values: - # state = 'deleted' - # extra.state = state - -#ADDED BY FRANCESCO MANGIACRAPA def get_package_for_value(list_package, value): - ''' Returns a list of packages containing the value passed in input - ''' - lst = [] - for x in list_package: - if x.value == value: - lst.append(x) - else: - return lst - - return lst + '''Returns a list of packages containing the value passed in input''' + return [x for x in list_package if x.value == value] #OVERRIDING BASE SQL ALCHEMY ENGINE INSTANCE serve per la connessione con gcube? def _init_TrackingMiddleware(self, app, config): @@ -364,47 +274,7 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) return { 'ignore_duplicate_keys': validators.ignore_duplicate_keys } - - #IPackageController - #def before_dataset_search(self, search_params): - # search_params = search_params or {} - # return search_params - # - #def after_dataset_search(self, search_results, data_dict): - # return search_results - # - #def before_search(self, search_params): - # # Controlla se search_params è None e sostituiscilo con un dizionario vuoto - # search_params = search_params or {} - # return search_params - # - #def before_create(self, context, data_dict): - # self.apply_custom_extras_validator(data_dict) - # - #def before_update(self, context, data_dict): - # self.apply_custom_extras_validator(data_dict) - # - ## utile ## - #def before_dataset_save(self, context, data_dict): - # log.debug("sto chiamando before_dataset_save") - # # Intercetta il salvataggio dei dataset prima che venga effettuato il controllo - # extras_list = data_dict.get('extras', []) - # - # # Aggiungi una logica per evitare che venga applicato un controllo su chiavi duplicate - # # Si elimina la logica di validazione delle chiavi duplicate - # unique_extras = {} - # for extra in extras_list: - # key = extra.get('key') - # if key in unique_extras: - # # Consenti la duplicazione delle chiavi, quindi non lo filtriamo - # unique_extras[key].append(extra) - # else: - # unique_extras[key] = [extra] - # - # # Restituisci un nuovo 'extras' senza rimuovere le chiavi duplicate - # data_dict['extras'] = [e for extras in unique_extras.values() for e in extras] - # return data_dict def convert_to_boolean(self, value): log.debug("value boolean %s", value) if isinstance(value, str): From 543a954125186a20e2f2d0e2e4722d14fad1b8d2 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Fri, 29 Nov 2024 15:22:36 +0100 Subject: [PATCH 06/10] style: :fire: removed comments removed comments --- ckanext-d4science_theme/ckanext/d4science_theme/plugin.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py b/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py index 0a50fa7..2076b60 100644 --- a/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py +++ b/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py @@ -224,7 +224,6 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) #log.debug("schema after create prima del validator %s", schema) schema = remove_check_replicated_custom_key(schema) #log.debug("create_package1 (remove __before): %s", schema) - #schema.update(self._modify_package_schema(schema)) schema.update(self._modify_package_schema()) #d.package_dict_save = _package_dict_save #log.debug("create_package2 (remove extras validator): %s", schema) @@ -236,7 +235,6 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) schema = super(D4Science_ThemePlugin, self).update_package_schema() schema = remove_check_replicated_custom_key(schema) #log.debug("update_package1 (remove __before) %s", schema) - #schema.update(self._modify_package_schema(schema)) schema.update(self._modify_package_schema()) #log.debug("update_package2 (remove extras validator) %s", schema) return schema @@ -248,7 +246,6 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) #log.debug("show_package1 %s", schema) schema = remove_check_replicated_custom_key(schema) #log.debug("show_package1.5 no before %s", schema) - #schema.update(self._modify_package_schema(schema)) schema.update({ 'extras': { 'id': [toolkit.get_validator('ignore')], # Ignora 'id' come prima @@ -256,7 +253,6 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) 'value': [toolkit.get_validator('not_missing')] } }) - #log.debug("show_package2 %s", schema) return schema #IDatasetForm From c0354a322e1a034a444b0414ccb20e4a749410e2 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Mon, 2 Dec 2024 15:47:41 +0100 Subject: [PATCH 07/10] feat: Add privatedataset inside the plugin Add privatedataset files (backend) to d4science plugin --- .../ckanext/d4science_theme/db.py | 2 +- .../fanstatic/allowed_users.js | 56 ++++ .../ckanext/d4science_theme/helpers.py | 12 +- .../ckanext/d4science_theme/plugin.py | 307 +++++++++++++++++- .../privatedatasets/actions.py | 239 ++++++++++++++ .../d4science_theme/privatedatasets/auth.py | 130 ++++++++ .../privatedatasets/constants.py | 28 ++ .../privatedatasets/converters_validators.py | 108 ++++++ .../privatedatasets/parser/__init__.py | 0 .../privatedatasets/parser/fiware.py | 69 ++++ .../d4science_theme/privatedatasets/views.py | 53 +++ 11 files changed, 992 insertions(+), 12 deletions(-) create mode 100644 ckanext-d4science_theme/ckanext/d4science_theme/fanstatic/allowed_users.js create mode 100644 ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/actions.py create mode 100644 ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/auth.py create mode 100644 ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/constants.py create mode 100644 ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/converters_validators.py create mode 100644 ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/parser/__init__.py create mode 100644 ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/parser/fiware.py create mode 100644 ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/views.py diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/db.py b/ckanext-d4science_theme/ckanext/d4science_theme/db.py index 034a3ac..88099d9 100644 --- a/ckanext-d4science_theme/ckanext/d4science_theme/db.py +++ b/ckanext-d4science_theme/ckanext/d4science_theme/db.py @@ -27,7 +27,6 @@ log = getLogger(__name__) AllowedUser = None def init_db(model): - log.debug("call initDB...") global AllowedUser if AllowedUser is None: @@ -38,6 +37,7 @@ def init_db(model): '''Finds all the instances required.''' query = model.Session.query(cls).autoflush(False) results = query.filter_by(**kw).all() + ### TODO: capire se questo controllo serve log.debug("results in get %s", results) if not isinstance(results, list): log.debug("Errore: il risultato di get() non è una lista. Risultato:", results) diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/fanstatic/allowed_users.js b/ckanext-d4science_theme/ckanext/d4science_theme/fanstatic/allowed_users.js new file mode 100644 index 0000000..0d27556 --- /dev/null +++ b/ckanext-d4science_theme/ckanext/d4science_theme/fanstatic/allowed_users.js @@ -0,0 +1,56 @@ +/* + * (C) Copyright 2014 CoNWeT Lab., Universidad Politécnica de Madrid + * + * This file is part of CKAN Private Dataset Extension. + * + * CKAN Private Dataset Extension is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * CKAN Private Dataset Extension is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with CKAN Private Dataset Extension. If not, see + * . + * + */ + +/* Dataset allowed_users, searchable and acquire_url toggler + * allowed_users, acquire_url and searchable can only be active when a + * user attempts to create a private dataset + */ + +this.ckan.module('allowed-users', function ($, _) { + return { + initialize: function() { + this.original_acquire_url = $('[name=acquire_url]').val(); + $('#field-private').on('change', this._onChange); + this._onChange(); //Initial + }, + _onChange: function() { + var ds_private = $('#field-private').val(); + + if (ds_private == 'True') { + $('#field-allowed_users_str').prop('disabled', false); //Enable + $('#field-acquire_url').prop('disabled', false); //Enable + $('#field-searchable').prop('disabled', false); //Enable + $('[name=acquire_url]').val(this.original_acquire_url); //Set previous acquire URL + } else { + $('#field-allowed_users_str').prop('disabled', true); //Disable + $('#field-acquire_url').prop('disabled', true); //Disable + $('#field-searchable').prop('disabled', true); //Disable + + //Remove previous values + $('#field-allowed_users_str').select2('val', ''); + this.original_acquire_url = $('[name=acquire_url]').val(); //Get previous value + $('[name=acquire_url]').val(''); //Acquire URL should be reseted + $('#field-searchable').val('True'); + } + } + }; + }); + \ No newline at end of file diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/helpers.py b/ckanext-d4science_theme/ckanext/d4science_theme/helpers.py index e6ffcbd..e70b5e8 100644 --- a/ckanext-d4science_theme/ckanext/d4science_theme/helpers.py +++ b/ckanext-d4science_theme/ckanext/d4science_theme/helpers.py @@ -733,7 +733,7 @@ def get_site_statistics() -> dict[str, int]: 'organization_count': len(logic.get_action('organization_list')({}, {})) } -#private dataset +####### PRIVATE DATASETS SECTION ######## def is_dataset_acquired(pkg_dict): @@ -749,11 +749,11 @@ def is_dataset_acquired(pkg_dict): return False def is_owner(pkg_dict): - return False - #if tk.c.userobj is not None: - # return tk.c.userobj.id == pkg_dict['creator_user_id'] - #else: - # return False + #return False #TODO: debug this + if tk.c.userobj is not None: + return tk.c.userobj.id == pkg_dict['creator_user_id'] + else: + return False def get_allowed_users_str(users): diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py b/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py index 2076b60..5cd93b1 100644 --- a/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py +++ b/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py @@ -15,7 +15,7 @@ from ckan.config.middleware.common_middleware import TrackingMiddleware import ckan.lib.dictization.model_save as model_save #from ckan.controllers.home import HomeController #from ckan.plugins import IRoutes -from flask import Blueprint, render_template +from flask import Blueprint, render_template, Flask, g, redirect, url_for from ckan.types import Context from typing import ( @@ -26,8 +26,15 @@ from typing import ( from ckan.common import ( g ) -from flask import Flask, g +#private datasets imports +from ckan.lib.plugins import DefaultPermissionLabels +from ckanext.d4science_theme.privatedatasets import auth, actions, constants, converters_validators as conv_val +from ckanext.d4science_theme import db +from ckanext.d4science_theme.privatedatasets.views import acquired_datasets +from ckan.lib import search + +HIDDEN_FIELDS = [constants.ALLOWED_USERS, constants.SEARCHABLE] log = getLogger(__name__) @@ -172,7 +179,7 @@ def _init_TrackingMiddleware(self, app, config): self.engine = sa.create_engine(sqlalchemy_url) -class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm): +class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm, DefaultPermissionLabels): plugins.implements(plugins.IConfigurer) plugins.implements(plugins.IDatasetForm, inherit=True) plugins.implements(plugins.ITemplateHelpers) @@ -184,6 +191,14 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) plugins.implements(plugins.IValidators) plugins.implements(plugins.IPackageController, inherit=True) + #PRIVATE DATASETS SECTION + plugins.implements(plugins.IAuthFunctions) + #plugins.implements(plugins.IRoutes, inherit=True) + plugins.implements(plugins.IActions) + plugins.implements(plugins.IPermissionLabels) + plugins.implements(plugins.IResourceController) + + # IConfigurer def update_config(self, config_): # Add this plugin's templates dir to CKAN's extra_template_paths, so @@ -202,13 +217,26 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) toolkit.add_resource('assets', 'd4science_theme') def _modify_package_schema(self): - log.debug("*** modify package ***") - # Personalizza il campo 'extras' rimuovendo il validatore extra_key_not_in_root_schema return { + #private datasets 'private': [toolkit.get_validator('ignore_missing'), toolkit.get_validator('boolean_validator')], + constants.ALLOWED_USERS_STR: [toolkit.get_validator('ignore_missing'), + conv_val.private_datasets_metadata_checker], + constants.ALLOWED_USERS: [conv_val.allowed_users_convert, + toolkit.get_validator('ignore_missing'), + conv_val.private_datasets_metadata_checker], + constants.ACQUIRE_URL: [toolkit.get_validator('ignore_missing'), + conv_val.private_datasets_metadata_checker, + conv_val.url_checker, + toolkit.get_converter('convert_to_extras')], + constants.SEARCHABLE: [toolkit.get_validator('ignore_missing'), + conv_val.private_datasets_metadata_checker, + toolkit.get_converter('convert_to_extras'), + toolkit.get_validator('boolean_validator')], + #d4science_theme 'extras': { 'id': [toolkit.get_validator('ignore')], # Ignora 'id' come prima 'key': [toolkit.get_validator('not_empty'), toolkit.get_validator('unicode_safe'), validators.ignore_duplicate_keys], # Aggiunto ignore duplicate @@ -247,6 +275,14 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) schema = remove_check_replicated_custom_key(schema) #log.debug("show_package1.5 no before %s", schema) schema.update({ + #private datasets + constants.ALLOWED_USERS: [conv_val.get_allowed_users, + toolkit.get_validator('ignore_missing')], + constants.ACQUIRE_URL: [toolkit.get_converter('convert_from_extras'), + toolkit.get_validator('ignore_missing')], + constants.SEARCHABLE: [toolkit.get_converter('convert_from_extras'), + toolkit.get_validator('ignore_missing')], + #d4science_theme 'extras': { 'id': [toolkit.get_validator('ignore')], # Ignora 'id' come prima 'key': [toolkit.get_validator('not_empty'), toolkit.get_validator('unicode_safe'), validators.ignore_duplicate_keys], # Aggiunto ignore duplicate @@ -255,6 +291,19 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) }) return schema + + #IDatasetForm + def is_fallback(self): + # Return True to register this plugin as the default handler for package types not handled by any other IDatasetForm plugin + return True + + #IDatasetForm + def package_types(self): + # This plugin doesn't handle any special package types, it just + # registers itself as the default (above). + return [] + + #IDatasetForm def is_fallback(self): # Return True to register this plugin as the default handler for package types not handled by any other IDatasetForm plugin @@ -351,6 +400,7 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) 'd4science_get_location_to_bboxes' : helpers.get_location_to_bboxes, 'd4science_get_content_moderator_system_placeholder': helpers.get_content_moderator_system_placeholder, 'd4science_get_site_statistics': helpers.get_site_statistics, + #privatedatasets section 'is_dataset_acquired': helpers.is_dataset_acquired, 'get_allowed_users_str': helpers.get_allowed_users_str, 'is_owner': helpers.is_owner, @@ -462,6 +512,7 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) ('/organization_vre', 'organization_vre', d4sOC.index), ('/tags', 'tags', tags), ('/groups', 'groups', groups), + ('/dashboard/acquired', 'acquired_datasets', acquired_datasets) #privatedatasets rule ] for rule in rules: blueprint.add_url_rule(*rule) @@ -508,3 +559,249 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm) return facets_dict + ###################################################################### + ######################## PRIVATEDATASET FORM ######################### + ###################################################################### + + def __init__(self, name=None): + self.indexer = search.PackageSearchIndex() + + def is_fallback(self): + # Return True to register this plugin as the default handler for package types not handled by any other IDatasetForm plugin + return True + + def package_types(self): + # This plugin doesn't handle any special package types, it just + # registers itself as the default (above). + return [] + + ###################################################################### + ########################### AUTH FUNCTIONS ########################### + ###################################################################### + + def get_auth_functions(self): + auth_functions = {'package_show': auth.package_show, + 'package_update': auth.package_update, + 'resource_show': auth.resource_show, + constants.PACKAGE_ACQUIRED: auth.package_acquired, + constants.ACQUISITIONS_LIST: auth.acquisitions_list, + constants.PACKAGE_DELETED: auth.revoke_access} + + return auth_functions + + ###################################################################### + ############################## IACTIONS ############################## + ###################################################################### + + def get_actions(self): + action_functions = {constants.PACKAGE_ACQUIRED: actions.package_acquired, + constants.ACQUISITIONS_LIST: actions.acquisitions_list, + constants.PACKAGE_DELETED: actions.revoke_access} + + return action_functions + + ###################################################################### + ######################### IPACKAGECONTROLLER ######################### + ###################################################################### + + def _delete_pkg_atts(self, pkg_dict, attrs): + for attr in attrs: + if attr in pkg_dict: + del pkg_dict[attr] + + def before_dataset_index(self, pkg_dict): + + if 'extras_' + constants.SEARCHABLE in pkg_dict: + if pkg_dict['extras_searchable'] == 'False': + pkg_dict['capacity'] = 'private' + else: + pkg_dict['capacity'] = 'public' + + return pkg_dict + + def after_dataset_create(self, context, pkg_dict): + session = context['session'] + update_cache = False + + db.init_db(context['model']) + + # Get the users and the package ID + if constants.ALLOWED_USERS in pkg_dict: + + allowed_users = pkg_dict[constants.ALLOWED_USERS] + package_id = pkg_dict['id'] + + # Get current users + users = db.AllowedUser.get(package_id=package_id) + + # Delete users and save the list of current users + current_users = [] + for user in users: + current_users.append(user.user_name) + if user.user_name not in allowed_users: + session.delete(user) + update_cache = True + + # Add non existing users + for user_name in allowed_users: + if user_name not in current_users: + out = db.AllowedUser() + out.package_id = package_id + out.user_name = user_name + out.save() + session.add(out) + update_cache = True + + session.commit() + + # The cache should be updated. Otherwise, the system may return + # outdated information in future requests + if update_cache: + new_pkg_dict = toolkit.get_action('package_show')( + {'model': context['model'], + 'ignore_auth': True, + 'validate': False, + 'use_cache': False}, + {'id': package_id}) + + # Prevent acquired datasets jumping to the first position + revision = toolkit.get_action('revision_show')({'ignore_auth': True}, {'id': new_pkg_dict['revision_id']}) + new_pkg_dict['metadata_modified'] = revision.get('timestamp', '') + self.indexer.update_dict(new_pkg_dict) + + return pkg_dict + + def after_dataset_update(self, context, pkg_dict): + return self.after_dataset_create(context, pkg_dict) + + def after_dataset_show(self, context, pkg_dict): + + void = False + + for resource in pkg_dict['resources']: + if resource == {}: + void = True + + if void: + del pkg_dict['resources'] + del pkg_dict['num_resources'] + + user_obj = context.get('auth_user_obj') + updating_via_api = context.get(constants.CONTEXT_CALLBACK, False) + + # allowed_users and searchable fileds can be only viewed by (and only if the dataset is private): + # * the dataset creator + # * the sysadmin + # * users allowed to update the allowed_users list via the notification API + if pkg_dict.get('private') is False or not updating_via_api and (not user_obj or (pkg_dict['creator_user_id'] != user_obj.id and not user_obj.sysadmin)): + # The original list cannot be modified + attrs = list(HIDDEN_FIELDS) + self._delete_pkg_atts(pkg_dict, attrs) + + return pkg_dict + + def after_dataset_delete(self, context, pkg_dict): + session = context['session'] + package_id = pkg_dict['id'] + + # Get current users + db.init_db(context['model']) + users = db.AllowedUser.get(package_id=package_id) + + # Delete all the users + for user in users: + session.delete(user) + session.commit() + + return pkg_dict + + def after_dataset_search(self, search_results, search_params): + for result in search_results['results']: + # Extra fields should not be returned + # The original list cannot be modified + attrs = list(HIDDEN_FIELDS) + + # Additionally, resources should not be included if the user is not allowed + # to show the resource + context = { + 'model': model, + 'session': model.Session, + 'user': toolkit.c.user, + 'user_obj': toolkit.c.userobj + } + + try: + toolkit.check_access('package_show', context, result) + except toolkit.NotAuthorized: + # NotAuthorized exception is risen when the user is not allowed + # to read the package. + attrs.append('resources') + # Delete + self._delete_pkg_atts(result, attrs) + + return search_results + + def before_dataset_view(self, pkg_dict): + + for resource in pkg_dict['resources']: + + context = { + 'model': model, + 'session': model.Session, + 'user': toolkit.c.user, + 'user_obj': toolkit.c.userobj + } + + try: + toolkit.check_access('resource_show', context, resource) + except toolkit.NotAuthorized: + pkg_dict['resources'].remove(resource) + pkg_dict = self.before_view(pkg_dict) + return pkg_dict + + def get_dataset_labels(self, dataset_obj): + labels = super(D4Science_ThemePlugin, self).get_dataset_labels( + dataset_obj) + + if getattr(dataset_obj, 'searchable', False): + labels.append('searchable') + + return labels + + def get_user_dataset_labels(self, user_obj): + labels = super(D4Science_ThemePlugin, self).get_user_dataset_labels( + user_obj) + + labels.append('searchable') + return labels + + ###################################################################### + ######################### IRESOURCECONTROLLER ######################## + ###################################################################### + + def before_resource_create(self, context, resource): + pass + + def after_resource_create(self, context, resource): + pass + + def before_resource_update(self, context, current, resource): + pass + + def before_resource_delete(self, context, resource, resources): + pass + + def before_resource_show(self, resource_dict): + + context = { + 'model': model, + 'session': model.Session, + 'user': toolkit.c.user, + 'user_obj': toolkit.c.userobj + } + + try: + toolkit.check_access('resource_show', context, resource_dict) + except toolkit.NotAuthorized: + resource_dict.clear() + return resource_dict \ No newline at end of file diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/actions.py b/ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/actions.py new file mode 100644 index 0000000..23b93f5 --- /dev/null +++ b/ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/actions.py @@ -0,0 +1,239 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 - 2017 CoNWeT Lab., Universidad Politécnica de Madrid +# Copyright (c) 2018 Future Internet Consulting and Development Solutions S.L. + +# This file is part of CKAN Private Dataset Extension. + +# CKAN Private Dataset Extension is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# CKAN Private Dataset Extension is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with CKAN Private Dataset Extension. If not, see . + +from __future__ import absolute_import + +import importlib +import logging +import os + +import ckan.plugins as plugins + +from ckanext.d4science_theme.privatedatasets import constants +from ckanext.d4science_theme import db + +log = logging.getLogger(__name__) + +PARSER_CONFIG_PROP = 'ckan.d4science_theme.privatedatasets.parser' + + +def package_acquired(context, request_data): + ''' + API action to be called every time a user acquires a dataset in an external service. + + This API should be called to add the user to the list of allowed users. + + Since each service can provide a different way of pushing the data, the received + data will be forwarded to the parser set in the preferences. This parser should + return a dict similar to the following one: + {'errors': ["...", "...", ...] + 'users_datasets': [{'user': 'user_name', 'datasets': ['ds1', 'ds2', ...]}, ...]} + 1) 'errors' contains the list of errors. It should be empty if no errors arised + while the notification is parsed + 2) 'users_datasets' is the lists of datasets available for each user (each element + of this list is a dictionary with two fields: user and datasets). + + :parameter request_data: Depends on the parser + :type request_data: dict + + :return: A list of warnings or None if the list of warnings is empty + :rtype: dict + + ''' + context['method'] = 'grant' + return _process_package(context, request_data) + + +def acquisitions_list(context, data_dict): + ''' + API to retrieve the list of datasets that have been acquired by a certain user + + :parameter user: The user whose acquired dataset you want to retrieve. This parameter + is optional. If you don't include this identifier, the system will use the one + of the user that is performing the request + :type user: string + + :return: The list of datarequest that has been acquired by the specified user + :rtype: list + ''' + + if data_dict is None: + data_dict = {} + + if 'user' not in data_dict and 'user' in context: + data_dict['user'] = context['user'] + + plugins.toolkit.check_access(constants.ACQUISITIONS_LIST, context.copy(), data_dict) + + # Init db + db.init_db(context['model']) + + # Init the result array + result = [] + + # Check that the user exists + try: + plugins.toolkit.get_validator('user_name_exists')(data_dict['user'], context.copy()) + except Exception: + raise plugins.toolkit.ValidationError('User %s does not exist' % data_dict['user']) + + # Get the datasets acquired by the user + query = db.AllowedUser.get(user_name=data_dict['user']) + + # Get the datasets + for dataset in query: + try: + dataset_show_func = 'package_show' + func_data_dict = {'id': dataset.package_id} + internal_context = context.copy() + + # Check that the the dataset can be accessed and get its data + # FIX: If the check_access function is not called, an exception is risen. + plugins.toolkit.check_access(dataset_show_func, internal_context, func_data_dict) + dataset_dict = plugins.toolkit.get_action(dataset_show_func)(internal_context, func_data_dict) + + # Only packages with state == 'active' can be shown + if dataset_dict.get('state', None) == 'active': + result.append(dataset_dict) + except Exception: + pass + + return result + + +def revoke_access(context, request_data): + ''' + API action to be called in order to revoke access grants of an user. + + This API should be called to delete the user from the list of allowed users. + + Since each service can provide a different way of pushing the data, the received + data will be forwarded to the parser set in the preferences. This parser should + return a dict similar to the following one: + {'errors': ["...", "...", ...] + 'users_datasets': [{'user': 'user_name', 'datasets': ['ds1', 'ds2', ...]}, ...]} + 1) 'errors' contains the list of errors. It should be empty if no errors arised + while the notification is parsed + 2) 'users_datasets' is the lists of datasets available for each user (each element + of this list is a dictionary with two fields: user and datasets). + + :parameter request_data: Depends on the parser + :type request_data: dict + + :return: A list of warnings or None if the list of warnings is empty + :rtype: dict + + ''' + context['method'] = 'revoke' + return _process_package(context, request_data) + + +def _process_package(context, request_data): + log.info('Notification received: %s' % request_data) + + # Check access + method = constants.PACKAGE_ACQUIRED if context.get('method') == 'grant' else constants.PACKAGE_DELETED + plugins.toolkit.check_access(method, context, request_data) + + # Get the parser from the configuration + class_path = os.environ.get(PARSER_CONFIG_PROP.upper().replace('.', '_'), plugins.toolkit.config.get(PARSER_CONFIG_PROP, '')) + + if class_path != '': + try: + cls = class_path.split(':') + class_package = cls[0] + class_name = cls[1] + parser_cls = getattr(importlib.import_module(class_package), class_name) + parser = parser_cls() + except Exception as e: + raise plugins.toolkit.ValidationError({'message': '%s: %s' % (type(e).__name__, str(e))}) + else: + raise plugins.toolkit.ValidationError({'message': '%s not configured' % PARSER_CONFIG_PROP}) + + # Parse the result using the parser set in the configuration + # Expected result: {'errors': ["...", "...", ...] + # 'users_datasets': [{'user': 'user_name', 'datasets': ['ds1', 'ds2', ...]}, ...]} + result = parser.parse_notification(request_data) + + warns = [] + + for user_info in result['users_datasets']: + for dataset_id in user_info['datasets']: + + try: + context_pkg_show = context.copy() + context_pkg_show['ignore_auth'] = True + context_pkg_show[constants.CONTEXT_CALLBACK] = True + dataset = plugins.toolkit.get_action('package_show')(context_pkg_show, {'id': dataset_id}) + + # This operation can only be performed with private datasets + # This check is redundant since the package_update function will throw an exception + # if a list of allowed users is included in a public dataset. However, this check + # should be performed in order to avoid strange future exceptions + if dataset.get('private', None) is True: + + # Create the array if it does not exist + if constants.ALLOWED_USERS not in dataset or dataset[constants.ALLOWED_USERS] is None: + dataset[constants.ALLOWED_USERS] = [] + + method = context['method'] == 'grant' + present = user_info['user'] in dataset[constants.ALLOWED_USERS] + # Deletes the user only if it is in the list + if (not method and present) or (method and not present): + if method: + dataset[constants.ALLOWED_USERS].append(user_info['user']) + else: + dataset[constants.ALLOWED_USERS].remove(user_info['user']) + + context_pkg_update = context.copy() + context_pkg_update['ignore_auth'] = True + + # Set creator as the user who is performing the changes + user_show = plugins.toolkit.get_action('user_show') + creator_user_id = dataset.get('creator_user_id', '') + user_show_context = {'ignore_auth': True} + user = user_show(user_show_context, {'id': creator_user_id}) + context_pkg_update['user'] = user.get('name', '') + + plugins.toolkit.get_action('package_update')(context_pkg_update, dataset) + log.info('Action %s access to dataset ended successfully' % context['method']) + else: + log.debug('Action %s access to dataset not completed. The dataset %s already %s access to the user %s' % (context['method'], dataset_id, context['method'], user_info['user'])) + else: + log.debug('Dataset %s is public. Cannot %s access to users' % (dataset_id, context['method'])) + warns.append('Unable to upload the dataset %s: It\'s a public dataset' % dataset_id) + + except plugins.toolkit.ObjectNotFound: + # If a dataset does not exist in the instance, an error message will be returned to the user. + # However the process won't stop and the process will continue with the remaining datasets. + log.debug('Dataset %s was not found in this instance' % dataset_id) + warns.append('Dataset %s was not found in this instance' % dataset_id) + except plugins.toolkit.ValidationError as e: + # Some datasets does not allow to introduce the list of allowed users since this property is + # only valid for private datasets outside an organization. In this case, a wanr will return + # but the process will continue + # WARN: This exception should not be risen anymore since public datasets are not updated. + message = '%s(%s): %s' % (dataset_id, constants.ALLOWED_USERS, e.error_dict[constants.ALLOWED_USERS][0]) + log.debug(message) + warns.append(message) + + # Return warnings that inform about non-existing datasets + if len(warns) > 0: + return {'warns': warns} \ No newline at end of file diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/auth.py b/ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/auth.py new file mode 100644 index 0000000..8b79c61 --- /dev/null +++ b/ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/auth.py @@ -0,0 +1,130 @@ +import ckan.lib.helpers as helpers +import ckan.logic.auth as logic_auth +import ckan.plugins.toolkit as tk +try: + import ckan.authz as authz +except ImportError: + import ckan.new_authz as authz + +import ckanext.d4science_theme.db as db + +from ckan.common import _, request + + +@tk.auth_allow_anonymous_access +def package_show(context, data_dict): + user = context.get('user') + user_obj = context.get('auth_user_obj') + package = logic_auth.get_package_object(context, data_dict) + + # datasets can be read by its creator + if package and user_obj and package.creator_user_id == user_obj.id: + return {'success': True} + + # Not active packages can only be seen by its owners + if package.state == 'active': + # anyone can see a public package + if not package.private: + return {'success': True} + + # if the user has rights to read in the organization or in the group + if package.owner_org: + authorized = authz.has_user_permission_for_group_or_org( + package.owner_org, user, 'read') + else: + authorized = False + + # if the user is not authorized yet, we should check if the + # user is in the allowed_users object + if not authorized: + # Init the model + db.init_db(context['model']) + + # Branch not executed if the database return an empty list + if db.AllowedUser.get(package_id=package.id, user_name=user): + authorized = True + + if not authorized: + # Show a flash message with the URL to acquire the dataset + # This message only can be shown when the user tries to access the dataset via its URL (/dataset/...) + # The message cannot be displayed in other pages that uses the package_show function such as + # the user profile page + + if hasattr(package, 'extras') and 'acquire_url' in package.extras and request.path.startswith('/dataset/')\ + and package.extras['acquire_url'] != '': + helpers.flash_notice(_('This private dataset can be acquired. To do so, please click ' + + 'here') % package.extras['acquire_url'], + allow_html=True) + + return {'success': False, 'msg': _('User %s not authorized to read package %s') % (user, package.id)} + else: + return {'success': True} + else: + return {'success': False, 'msg': _('User %s not authorized to read package %s') % (user, package.id)} + + +def package_update(context, data_dict): + user = context.get('user') + user_obj = context.get('auth_user_obj') + package = logic_auth.get_package_object(context, data_dict) + + # Only the package creator can update it + if package and user_obj and package.creator_user_id == user_obj.id: + return {'success': True} + + # if the user has rights to update a dataset in the organization or in the group + if package and package.owner_org: + authorized = authz.has_user_permission_for_group_or_org( + package.owner_org, user, 'update_dataset') + else: + authorized = False + + if not authorized: + return {'success': False, 'msg': _('User %s is not authorized to edit package %s') % (user, package.id)} + else: + return {'success': True} + + +@tk.auth_allow_anonymous_access +def resource_show(context, data_dict): + # This function is needed since CKAN resource_show function uses the default package_show + # function instead of the one defined in the plugin. + # A bug is openend in order to be able to remove this function + # https://github.com/ckan/ckan/issues/1818 + # It's fixed now, so this function can be deleted when the new version is released. + _model = context['model'] + user = context.get('user') + resource = logic_auth.get_resource_object(context, data_dict) + + # check authentication against package + query = _model.Session.query(_model.Package)\ + .join(_model.ResourceGroup)\ + .join(_model.Resource)\ + .filter(_model.ResourceGroup.id == resource.resource_group_id) + pkg = query.first() + if not pkg: + raise tk.ObjectNotFound(_('No package found for this resource, cannot check auth.')) + + pkg_dict = {'id': pkg.id} + authorized = package_show(context, pkg_dict).get('success') + + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to read resource %s') % (user, resource.id)} + else: + return {'success': True} + + +@tk.auth_allow_anonymous_access +def package_acquired(context, data_dict): + # TODO: Improve security + return {'success': True} + +def acquisitions_list(context, data_dict): + # Users can get only their acquisitions list + return {'success': context['user'] == data_dict['user']} + +#V2 old repo d4science +@tk.auth_allow_anonymous_access +def revoke_access(context, data_dict): + # TODO: Check functionality and improve security(if needed) + return {'success': True} \ No newline at end of file diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/constants.py b/ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/constants.py new file mode 100644 index 0000000..7f04e65 --- /dev/null +++ b/ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/constants.py @@ -0,0 +1,28 @@ + +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid + +# This file is part of CKAN Private Dataset Extension. + +# CKAN Private Dataset Extension is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# CKAN Private Dataset Extension is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with CKAN Private Dataset Extension. If not, see . + +ALLOWED_USERS = 'allowed_users' +ACQUISITIONS_LIST = 'acquisitions_list' +ALLOWED_USERS_STR = 'allowed_users_str' +SEARCHABLE = 'searchable' +ACQUIRE_URL = 'acquire_url' +CONTEXT_CALLBACK = 'updating_via_cb' +PACKAGE_ACQUIRED = 'package_acquired' +PACKAGE_DELETED = 'revoke_access' \ No newline at end of file diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/converters_validators.py b/ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/converters_validators.py new file mode 100644 index 0000000..ef73d11 --- /dev/null +++ b/ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/converters_validators.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid +# Copyright (c) 2019 Future Internet Consulting and Development Solutions S.L. + +# This file is part of CKAN Private Dataset Extension. + +# CKAN Private Dataset Extension is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# CKAN Private Dataset Extension is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with CKAN Private Dataset Extension. If not, see . + +from __future__ import absolute_import + +from itertools import count +import re + +import ckan.plugins.toolkit as toolkit +from ckan.common import _ +import six + +from ckanext.d4science_theme.privatedatasets import constants +from ckanext.d4science_theme import db + + +def private_datasets_metadata_checker(key, data, errors, context): + + dataset_id = data.get(('id',)) + private_val = data.get(('private',)) + + # Avoid missing value + # "if not private_val:" is not valid because private_val can be False + if not isinstance(private_val, six.string_types) and not isinstance(private_val, bool): + private_val = None + + # If the private field is not included in the data dict, we must check the current value + if private_val is None and dataset_id: + dataset_dict = toolkit.get_action('package_show')({'ignore_auth': True}, {'id': dataset_id}) + private_val = dataset_dict.get('private') + + private = private_val is True if isinstance(private_val, bool) else private_val == 'True' + metadata_value = data[key] + + # If allowed users are included and the dataset is not private outside and organization, an error will be raised. + if metadata_value and not private: + errors[key].append(_('This field is only valid when you create a private dataset')) + + +def allowed_users_convert(key, data, errors, context): + + # By default, all the fileds are in the data dictionary even if they contains nothing. In this case, + # the value is 'ckan.lib.navl.dictization_functions.Missing' and for this reason the type is checked + + # Get the allowed user list + if (constants.ALLOWED_USERS,) in data and isinstance(data[(constants.ALLOWED_USERS,)], list): + allowed_users = data[(constants.ALLOWED_USERS,)] + elif (constants.ALLOWED_USERS_STR,) in data and isinstance(data[(constants.ALLOWED_USERS_STR,)], six.string_types): + allowed_users_str = data[(constants.ALLOWED_USERS_STR,)].strip() + allowed_users = [allowed_user for allowed_user in allowed_users_str.split(',') if allowed_user.strip() != ''] + else: + allowed_users = None + + if allowed_users is not None: + current_index = max([int(k[1]) for k in data.keys() if len(k) == 2 and k[0] == key[0]] + [-1]) + + if len(allowed_users) == 0: + data[(constants.ALLOWED_USERS,)] = [] + else: + for num, allowed_user in zip(count(current_index + 1), allowed_users): + allowed_user = allowed_user.strip() + data[(key[0], num)] = allowed_user + + +def get_allowed_users(key, data, errors, context): + pkg_id = data[('id',)] + + db.init_db(context['model']) + + users = db.AllowedUser.get(package_id=pkg_id) + + for i, user in enumerate(users): + data[(key[0], i)] = user.user_name + + +def url_checker(key, data, errors, context): + url = data.get(key, None) + + if url: + # DJango Regular Expression to check URLs + regex = re.compile( + r'^https?://' # scheme is validated separately + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}(?. + +import re +from urllib.parse import urlparse + +from ckan.common import request +import ckan.plugins.toolkit as tk + + +class FiWareNotificationParser(object): + + def parse_notification(self, request_data): + my_host = request.host + + fields = ['customer_name', 'resources'] + + for field in fields: + if field not in request_data: + raise tk.ValidationError({'message': '%s not found in the request' % field}) + + # Parse the body + resources = request_data['resources'] + user_name = request_data['customer_name'] + datasets = [] + + if not isinstance(user_name, str): + raise tk.ValidationError({'message': 'Invalid customer_name format'}) + + if not isinstance(resources, list): + raise tk.ValidationError({'message': 'Invalid resources format'}) + + for resource in resources: + if isinstance(resource, dict) and 'url' in resource: + parsed_url = urlparse(resource['url']) + dataset_name = re.findall('^/dataset/([^/]+).*$', parsed_url.path) + + resource_url = parsed_url.netloc + if ':' in my_host and ':' not in resource_url: + # Add the default port depending on the protocol + default_port = '80' if parsed_url.scheme == 'http' else '443' + resource_url = resource_url + default_port + + if len(dataset_name) == 1: + if resource_url == my_host: + datasets.append(dataset_name[0]) + else: + raise tk.ValidationError({'message': 'Dataset %s is associated with the CKAN instance located at %s, expected %s' + % (dataset_name[0], resource_url, my_host)}) + else: + raise tk.ValidationError({'message': 'Invalid resource format'}) + + return {'users_datasets': [{'user': user_name, 'datasets': datasets}]} diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/views.py b/ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/views.py new file mode 100644 index 0000000..c21cb8b --- /dev/null +++ b/ckanext-d4science_theme/ckanext/d4science_theme/privatedatasets/views.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2018 Future Internet Consulting and Development Solutions S.L. + +# This file is part of CKAN Private Dataset Extension. + +# CKAN Private Dataset Extension is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# CKAN Private Dataset Extension is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with CKAN Private Dataset Extension. If not, see . + +from __future__ import absolute_import, unicode_literals + +from ckan import logic, model +from ckan.common import _, g +from ckan.lib import base +import ckan.plugins.toolkit as toolkit + +from flask import render_template + +from ckanext.d4science_theme.privatedatasets import constants + + +def acquired_datasets(): + context = {'auth_user_obj': g.userobj, 'for_view': True, 'model': model, 'session': model.Session, 'user': g.user} + data_dict = {'user_obj': g.userobj} + try: + user_dict = toolkit.get_action('user_show')(context, data_dict) + acquired_datasets = toolkit.get_action(constants.ACQUISITIONS_LIST)(context, None) + except logic.NotFound: + base.abort(404, _('User not found')) + except logic.NotAuthorized: + base.abort(403, _('Not authorized to see this page')) + + extra_vars = { + 'user_dict': user_dict, + 'acquired_datasets': acquired_datasets, + } + return render_template('user/dashboard_acquired.html', extra_vars) + + +class AcquiredDatasetsControllerUI(): + + def acquired_datasets(self): + return acquired_datasets() From d891a8038f98dfb3317de7bcb17fc3de952222aa Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Mon, 2 Dec 2024 16:02:06 +0100 Subject: [PATCH 08/10] fix dataset navigation --- .../templates/snippets/package_item.html | 141 ++++++++++++++---- 1 file changed, 111 insertions(+), 30 deletions(-) diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/package_item.html b/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/package_item.html index 0e16bed..4226fe8 100644 --- a/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/package_item.html +++ b/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/package_item.html @@ -18,47 +18,108 @@ {% 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 owner = h.is_owner(package) %} + + {# {% resource 'd4science_theme/privatedatasets.css' %}#} {# - {% resource 'd4science_theme/custom.css' %} CHANGED BY FRANCESCO.MANGIACRAPA #} {% block package_item_content %} + + {# + {{ package.extras }} + #} + {% block toolbar %} + + {% endblock %} + + {% if package.private and not h.can_read(package) %}
  • -
    + +
    + +
    +

    Access required...

    + × +

    +
    +
    + {% set orgTitle = package.organization.title %} +
    +
    + {% set mvalue = h.d4science_theme_get_systemtype_value_from_extras(package, package.extras) %} + {% set mcolor = h.d4science_get_color_for_type(mvalue) %} + {{ mvalue }} +

    {% if package.private and not h.can_read(package) %} {{ _('Private') }} + {{ _(h.truncate(title, truncate_title)) }} {% endif %} - - + {% endif %} {% if package.private and not h.can_read(package) %} - {# {{ _(h.truncate(title, truncate_title)) }} #} - Dataset + {# {{ _(h.truncate(title, truncate_title)) }} Dataset#}
    {{ h.acquire_button(package) }} {% else %} - {# {{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }} #} - {{ h.link_to(h.truncate(title, truncate_title), url_for('dataset.read', id=package.name)) }} + {{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }} {% endif %} - + {% if package.get('state', '').startswith('draft') %} {{ _('Draft') }} {% elif package.get('state', '').startswith('deleted') %} @@ -70,13 +131,18 @@ {% endif %} {% if notes %} - +
    {{ notes|urlize(70,target='_blank') }}
    {% endif %}

  • {% else %}
  • +
    + {% set mvalue = h.d4science_theme_get_systemtype_value_from_extras(package, package.extras)%} + {% set mcolor = h.d4science_get_color_for_type(mvalue) %} + {{ mvalue }} +

    {% if package.private and not h.can_read(package) %} @@ -84,7 +150,6 @@ {{ _('Private') }} {% endif %} - - + {% endif %} + {% if package.private and not h.can_read(package) %} {{ _(h.truncate(title, truncate_title)) }}
    {{ h.acquire_button(package) }} {% else %} - {# {{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }} #} - {{ h.link_to(h.truncate(title, truncate_title), url_for('dataset.read', id=package.name)) }} + {# CHANGED BY FRANCESCO MANGIACRAPA #} + {{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='dataset', action='read', id=package.name)) }} {% endif %} - + {% if package.get('state', '').startswith('draft') %} {{ _('Draft') }} {% elif package.get('state', '').startswith('deleted') %} @@ -120,20 +185,36 @@ {% endif %} {% if notes %} -
    {{ notes|urlize }}
    +
    {{ notes|urlize(70,target='_blank') }}
    {% endif %}
    {% if package.resources and not hide_resources %} - - {% endif %} - + + {# CHANGED BY FRANCESCO MANGIACRAPA, see: #7055 #} +
      + {% for resource in package.resources %} + {% set resource_format = resource.format %} + {# TO INTERNAL RESOURCE PAGE set url = h.url_for(controller='package', action='resource_read', id=package.name, resource_id=resource.id) #} +
    • + + {% if c.userobj %} + {# USER IS LOGGED #} + {# CHANGED BY FRANCESCO MANGIACRAPA #11178 #} + {{ resource_format }} + {% else %} + {# CHANGED BY FRANCESCO MANGIACRAPA #12377 #} + {{ resource.format }} +
      + The resource: '{{ h.resource_display_name(resource) | truncate(30) }}' is not accessible as guest user. You must login to access it! +
      + + {% endif %} + +
    • + {% endfor %} +
    + {% endif %} +

  • {% endif %} {% endblock %} \ No newline at end of file From 29361b145f10fc392e7f479db675b80f556d5f70 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Mon, 2 Dec 2024 16:09:11 +0100 Subject: [PATCH 09/10] add acquire_button snippet file --- .../templates/snippets/acquire_button.html | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/acquire_button.html diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/acquire_button.html b/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/acquire_button.html new file mode 100644 index 0000000..a9f88ba --- /dev/null +++ b/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/acquire_button.html @@ -0,0 +1,16 @@ +{# + + Displays a Get Access button to request access to a private dataset. + + ulr_dest - target url + + Example: + + {% snippet 'snippets/acquire_button.html', url_dest=url %} + + #} + + + {{ _('Acquire') }} + + \ No newline at end of file From f0ffcce9f814c0d1134ca927dc3ce896195785ad Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Mon, 2 Dec 2024 16:19:37 +0100 Subject: [PATCH 10/10] add dataset_acquired html file --- .../templates/user/dashboard_acquired.html | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 ckanext-d4science_theme/ckanext/d4science_theme/templates/user/dashboard_acquired.html diff --git a/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/dashboard_acquired.html b/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/dashboard_acquired.html new file mode 100644 index 0000000..a9fc325 --- /dev/null +++ b/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/dashboard_acquired.html @@ -0,0 +1,19 @@ +{% extends "user/dashboard.html" %} + +{% block dashboard_activity_stream_context %}{% endblock %} + +{% block page_primary_action %} + {% link_for _('Acquire Dataset'), controller='package', action='search', class_="btn btn-primary", icon="shopping-cart" %} +{% endblock %} + +{% block primary_content_inner %} +

    {{ _('Acquired Datasets') }}

    + {% if acquired_datasets %} + {% snippet 'snippets/package_list.html', packages=acquired_datasets %} + {% else %} +

    + {{ _('You haven\'t acquired any datasets.') }} + {% link_for _('Acquire one now?'), controller='package', action='search' %} +

    + {% endif %} +{% endblock %}