Merge branch 'main' into feature/homepage_restyling

This commit is contained in:
marco_carollo 2024-12-03 12:04:43 +01:00
commit 0056bd8dcd
27 changed files with 1902 additions and 303 deletions

View File

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

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
from logging import getLogger
import sqlalchemy as sa
log = getLogger(__name__)
AllowedUser = None
def init_db(model):
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()
### 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)
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,)

View File

@ -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
* <http://www.gnu.org/licenses/>.
*
*/
/* 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');
}
}
};
});

View File

@ -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 DATASETS SECTION ########
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 #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):
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 ''

View File

@ -4,26 +4,37 @@ 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
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 flask import Blueprint, render_template, Flask, g, redirect, url_for
from ckan.types import Context
from typing import (
Any, Collection, Optional, TYPE_CHECKING, Type, Union, cast, overload,
Literal,
)
from ckan.common import (
g
)
from flask import Flask, g
from ckan.lib.app_globals import app_globals
import ckan.plugins.toolkit as toolkit
#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
# Created by Francesco Mangiacrapa
# francesco.mangiacrapa@isti.cnr.it
# ISTI-CNR Pisa (ITALY)
HIDDEN_FIELDS = [constants.ALLOWED_USERS, constants.SEARCHABLE]
log = getLogger(__name__)
@ -35,22 +46,17 @@ 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, 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:
return
model = context["model"]
session = context["session"]
#ADDED BY FRANCESCO MANGIACRAPA
log.debug("extra_dicts: "+ str(extra_dicts))
#print "extra_dicts: "+str(extra_dicts)
extras_list = obj.extras_list
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 = {}
@ -58,41 +64,27 @@ def _package_extras_save(extra_dicts, obj, context):
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 []:
#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"]))
#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"])
#ADDED BY FRANCESCO MANGIACRAPA
log.debug("new_extras: "+str(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))
#print "adding key: "+str(key)
extra_lst = new_extras[key]
for extra in extra_lst:
extra = model.PackageExtra(state=state, key=key, value=extra)
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()):
log.debug("deleting key: "+str(key))
#print "deleting key: "+str(key)
extra_lst = extras[key]
for extra in extra_lst:
state = 'deleted'
@ -105,13 +97,9 @@ 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))
#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")
#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)
@ -119,47 +107,32 @@ def _package_extras_save(extra_dicts, obj, context):
state = 'active'
extra.state = state
session.add(extra)
#print "extra updated: "+str(extra)
log.debug("extra updated: "+str(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")
state = 'active'
extra = model.PackageExtra(state=state, key=key, value=value)
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)
log.debug("old extra values updated: "+str(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")
#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))
#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))
state = 'active'
extra.state = state
session.add(extra)
session.add(extra) #valuta se metterlo dentro il for, ma fuori dall'else
else:
#print "extra new value: "+str(value)
log.debug("extra new value: "+str(value))
state = 'active'
extra = model.PackageExtra(state=state, key=key, value=value)
extra = model.PackageExtra(state=state, key=key, value=value, package_id = pkg.id)
extra.state = state
session.add(extra)
extras_list.append(extra)
@ -171,35 +144,60 @@ def _package_extras_save(extra_dicts, obj, context):
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
#add session.delete(extra)?
#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
'''Returns a list of packages containing the value passed in input'''
return [x for x in list_package if x.value == 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):
class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm, DefaultPermissionLabels):
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)
#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_):
@ -217,32 +215,87 @@ 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):
# 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
'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()
#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())
#d.package_dict_save = _package_dict_save
#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)
#log.debug("update_package1 (remove __before) %s", schema)
schema.update(self._modify_package_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.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
'value': [toolkit.get_validator('not_missing')]
}
})
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):
@ -251,6 +304,63 @@ class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm)
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
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
}
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):
log.info("get_helpers called...")
@ -290,11 +400,19 @@ 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,
'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
# 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()
@ -302,6 +420,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()
@ -391,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)
@ -437,4 +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

View File

@ -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 <http://www.gnu.org/licenses/>.
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}

View File

@ -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 ' +
'<a target="_blank" href="%s">here</a>') % 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}

View File

@ -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 <http://www.gnu.org/licenses/>.
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'

View File

@ -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 <http://www.gnu.org/licenses/>.
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,}(?<!-)\.?)|' # domain...
r'localhost|' # localhost...
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4
r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6
r'(?::\d+)?' # optional port
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
if regex.match(url) is None:
errors[key].append(_('The URL "%s" is not valid.') % url)

View File

@ -0,0 +1,69 @@
# -*- 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 <http://www.gnu.org/licenses/>.
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}]}

View File

@ -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 <http://www.gnu.org/licenses/>.
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()

View File

@ -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 %}
<span class="label pull-right" style='margin-left:5px; padding:3px;'>
{{ moderation_item_status }}
</span>
{% endif %}
{% block package_description %}
{% if pkg.private %}
<span class="dataset-private badge badge-inverse pull-right">
<i class="fa fa-lock"></i>
<span class="dataset-private label label-inverse pull-right">
<i class="icon-lock"></i>
{{ _('Private') }}
</span>
{% endif %}
<h1>
<div class="principaltitle">
{% block page_heading %}
{{ h.dataset_display_name(pkg) }}
{{ pkg.title or pkg.name }}
{% if pkg.state.startswith('draft') %}
[{{ _('Draft') }}]
{% endif %}
@ -23,11 +26,11 @@
[{{ _('Deleted') }}]
{% endif %}
{% endblock %}
</h1>
</div>
{% block package_notes %}
{% if pkg.notes %}
<div class="notes embedded-content">
{{ h.render_markdown(h.get_translated(pkg, 'notes')) }}
<div class="notes embedded-content" style="white-space:pre-line;">
{{ h.render_markdown(pkg.notes) }}
</div>
{% endif %}
{% endblock %}
@ -35,14 +38,20 @@
<span class="insert-comment-thread"></span>
{% endblock %}
{% block package_resources %}
{% snippet "package/snippets/resources_list.html", pkg=pkg, resources=pkg.resources %}
{% endblock %}
{% if dataset_extent %}
{% snippet "spatial/snippets/dataset_map.html", extent=dataset_extent %}
{% endif %}
{% block package_tags %}
<div class="sectiontitle">{{_('Tags')}}</div>
{% 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_additional_info %}
{% snippet "package/snippets/additional_info.html", pkg_dict=pkg %}
{% endblock %}

View File

@ -1,5 +1,74 @@
{% set key_item_url = _('Item') + ' URL' %}
<script type="text/javascript" src="/fanstatic/d4science_theme/d4science_scripts.js"></script>
<script>
//ADDED by Francesco Mangiacrapa
jsonToHTML = function(containerID, cssClassToTable) {
CKAN_D4S_JSON_Util.jsonToHTML(containerID, cssClassToTable);
}
</script>
<section class="additional-info">
<h3>{{ _('Additional Info') }}</h3>
{% 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 %}
<div><div class="sectiontitle">{{ key_item_url }}</div>
<table class="qr-code-table">
<tr>
<td><a href={{ extra_item_url }} target="_blank">{{ extra_item_url }}</a></td>
<td>{% snippet "package/snippets/qrcode_show.html", package_url=extra_item_url %}</td>
</tr>
</table></div>
{% 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') %}
<div><div class="sectiontitle">{{category_idx.category.title}}</div>
{% if category_idx.category.description %}
<div class="notes embedded-content"><p>Description: {{category_idx.category.description}}</p></div>
{% endif %}
<table class="table table-striped table-bordered table-condensed">
<thead>
<tr>
<th scope="col">{{ _('Field') }}</th>
<th scope="col">{{ _('Value') }}</th>
</tr>
</thead>
<tbody>
{% 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 %}
</tbody>
</table>
</div>
{% endif %}
{% endfor %}
{% set my_extras = h.d4science_get_extra_for_category(extras_indexed_for_categories, 'nocategory') %}
{% if my_extras|length > 0 %}
<div><div class="sectiontitle">{{ _('Additional Info') }}</div>
<table class="table table-striped table-bordered table-condensed">
<thead>
<tr>
<th scope="col">{{ _('Field') }}</th>
<th scope="col">{{ _('Value') }}</th>
</tr>
</thead>
<tbody>
{% snippet "package/snippets/extras_table.html", my_extras=my_extras, key_item_url=key_item_url %}
</tbody>
</table>
</div>
{% endif %}
{% endif %}
{% endblock %}
<div class="sectiontitle">{{ _('Management Info') }}</div>
<table class="table table-striped table-bordered table-condensed">
<thead>
<tr>
@ -13,11 +82,7 @@
<tr>
<th scope="row" class="dataset-label">{{ _('Source') }}</th>
{% if h.is_url(pkg_dict.url) %}
<td class="dataset-details" property="foaf:homepage">
<a href="{{ pkg_dict.url }}" rel="foaf:homepage" target="_blank">
{{ pkg_dict.url }}
</a>
</td>
<td class="dataset-details" property="foaf:homepage">{{ h.link_to(pkg_dict.url, pkg_dict.url, rel='foaf:homepage', target='_blank') }}</td>
{% else %}
<td class="dataset-details" property="foaf:homepage">{{ pkg_dict.url }}</td>
{% endif %}
@ -35,16 +100,17 @@
<td class="dataset-details" property="dc:creator">{{ pkg_dict.author }}</td>
</tr>
{% endif %}
{# Added by Francesco Mangiacrapa #}
{% set user_maintainer = h.d4science_get_user_info(pkg_dict.maintainer) %}
{% if pkg_dict.maintainer_email %}
<tr>
<th scope="row" class="dataset-label">{{ _('Maintainer') }}</th>
<td class="dataset-details" property="dc:contributor">{{ h.mail_to(email_address=pkg_dict.maintainer_email, name=pkg_dict.maintainer) }}</td>
<td class="dataset-details" property="dc:contributor">{{ h.mail_to(email_address=pkg_dict.maintainer_email, name=user_maintainer.fullname if (user_maintainer and user_maintainer.fullname) else pkg_dict.maintainer) }}</td>
</tr>
{% elif pkg_dict.maintainer %}
<tr>
<th scope="row" class="dataset-label">{{ _('Maintainer') }}</th>
<td class="dataset-details" property="dc:contributor">{{ pkg_dict.maintainer }}</td>
<td class="dataset-details" property="dc:contributor">{{ user_maintainer.fullname if (user_maintainer and user_maintainer.fullname) else pkg_dict.maintainer }}</td>
</tr>
{% endif %}
@ -78,18 +144,7 @@
</td>
</tr>
{% endif %}
{% block extras scoped %}
{% for extra in h.sorted_extras(pkg_dict.extras) %}
{% set key, value = extra %}
<tr rel="dc:relation" resource="_:extra{{ i }}">
<th scope="row" class="dataset-label" property="rdfs:label">{{ _(key|e) }}</th>
<td class="dataset-details" property="rdf:value">{{ value }}</td>
</tr>
{% endfor %}
{% endblock %}
{% endblock %}
</tbody>
</tbody>
</table>
{% endblock %}
</section>

View File

@ -0,0 +1,59 @@
<!-- Applying "Show more"/"Show less" to 'spatial field', see #23298 -->
<script type="text/javascript">
function manageLongField(containerID, n) {
try {
var str = document.getElementById(containerID).innerHTML;
if (str && str.length <= n) {
return str;
}
var newContent = "<div><input type='checkbox' class='read-more-state' id='post-"+containerID+"'' /><label for='post-"+containerID+"'' class='read-more-trigger'></label><p class='read-more-wrap'>";
newContent += str.substring(0, n);
newContent += "<span class='read-more-target'>";
newContent += str.substring(n, str.length);
newContent += "</span></p></div>"
document.getElementById(containerID).innerHTML = newContent
} catch (e) {
console.log('invalid containerID ' + containerID, e);
}
}
</script>
<!-- reading placeholder used by D4Science Content Moderator System, see #22854 -->
{% 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] %}
<!-- showing only extra fields with no empty values -->
{% if extra_value is defined and extra_value|length %}
{# Added by Francesco Mangiacrapa, see: #7055 #}
{% set isHttp = extra_value.startswith(('http://', 'https://')) %}
<tr rel="dc:relation" resource="_:extra{{ i }}">
<!-- no showing ITEM URL and D4Science CMS fields into table -->
{% if k != key_item_url and (not k.startswith(d4science_cms_obj_placeholders.prefix)) %}
<th scope="row" class="dataset-label" property="rdfs:label">{{ _(k) }}</th>
{% if isHttp %}
{% if k == 'graphic-preview-file'%}
<td class="dataset-details" property="rdf:value"><img src={{ extra_value }} alt={{ extra_value }}></td>
{% else %}
<td class="dataset-details" property="rdf:value"><a href={{ extra_value }} target="_blank">{{ extra_value }}</a></td>
{% endif %}
{% elif k == 'responsible-party' %}
<td class="dataset-details" property="rdf:value"><div id="responsible-party-extra-{{index}}" class="d4s-json-table">{{ extra_value }}</div><script>jsonToHTML('responsible-party-extra-{{index}}')</script></td>
{% elif k == 'dataset-reference-date' %}
<td class="dataset-details" property="rdf:value"><div id="dataset-reference-date-extra-{{index}}" class="d4s-json-table">{{ extra_value }}</div><script>jsonToHTML('dataset-reference-date-extra-{{index}}')</script></td>
{% elif k == 'coupled-resource' %}
<td class="dataset-details" property="rdf:value"><div id="coupled-resource-date-extra-{{index}}" class="d4s-json-table">{{ extra_value }}</div><script>jsonToHTML('coupled-resource-date-extra-{{index}}')</script></td>
{% elif k.startswith('Zenodo') %}
<td class="dataset-details" property="rdf:value"><div id="related-identifier-extra-{{index}}" class="d4s-json-table">{{ extra_value }}</div><script>jsonToHTML('related-identifier-extra-{{index}}')</script></td>
{% elif k == 'spatial' %}
<td class="dataset-details" property="rdf:value"><div id='spatial-{{index}}'>{{ extra_value }}</div><script>manageLongField('spatial-{{index}}', 150)</script></td>
{% else %}
<td class="dataset-details" property="rdf:value">{{ extra_value }}</td>
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{% endfor %}

View File

@ -1,41 +1,48 @@
{#
Displays a sidebar module with information for given package
Displays a sidebard module with information for given package
pkg - The package dict that owns the resources.
pkg - The package dict that owns the resources.
Example:
Example:
{% snippet "package/snippets/info.html", pkg=pkg %}
{% snippet "package/snippets/info.html", pkg=pkg %}
#}
{% block package_info %}
{% if pkg %}
<section class="module module-narrow">
<div class="module context-info">
<div class="module-content">
{% block package_info_inner %}
{% block heading %}
<h1 class="heading">{{ h.dataset_display_name(pkg) }}</h1>
{% endblock %}
{% block nums %}
{% set num_followers = h.follow_count('dataset', pkg.id) %}
<div class="nums">
<dl>
<dt>{{ _('Followers') }}</dt>
<dd data-module="followers-counter" data-module-id="{{ pkg.id }}" data-module-num_followers="{{ num_followers }}">{{ h.SI_number_span(num_followers) }}</dd>
</dl>
</div>
{% endblock %}
{% block follow_button %}
{% if not hide_follow_button %}
<div class="follow_button">
{{ h.follow_button('dataset', pkg.id) }}
#}
{% block package_info %}
{% if pkg %}
<section class="module module-narrow">
<div class="module context-info">
<div class="module-content">
{% block package_info_inner %}
{% block heading %}
<div class="infotitle">{{ h.dataset_display_name(pkg) }}</div>
{% endblock %}
{% if pkg.extras %}
{% for extra in pkg.extras if extra['key'] == 'graphic-preview-file' %}
<div class="graphic-preview-style">
<div><a href={{ extra['value'] }} target="_blank"><img src={{ extra['value'] }} alt='graphic-preview-file'>Show Graphic</a></div>
</div>
{% endfor %}
{% endif %}
{% block nums %}
<div class="nums">
<dl>
<dt>{{ _('Followers') }}</dt>
<dd>{{ h.SI_number_span(h.follow_count('dataset', pkg.id)) }}</dd>
</dl>
</div>
{% endif %}
{% endblock %}
{% block follow_button %}
{% if not hide_follow_button %}
<div class="follow_button">
{{ h.follow_button('dataset', pkg.name) }}
</div>
{% endif %}
{% endblock %}
{% endblock %}
{% endblock %}
</div>
</div>
</div>
</section>
{% endif %}
{% endblock %}
</section>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,6 @@
{% set qr_code_image = h.d4science_get_qrcode_for_url(package_url) %}
{% block package_qrcode_for_url %}
<div style="padding: 1px;"><img src='data:image/svg+xml;base64, {{ qr_code_image }}' alt={{ package_url }}> </div>
{% endblock %}

View File

@ -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) %}
<li class="resource-item" data-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 %}
<a class="heading" href="{{ url }}" title="{{ res.name or res.description }}">
{{ h.resource_display_name(res) | truncate(50) }}<span class="format-label" property="dc:format" data-format="{{ res.format.lower() or 'data' }}">{{ h.get_translated(res, 'format') }}</span>
{{ h.popular('views', res.tracking_summary.total, min=10) if res.tracking_summary }}
{{ h.resource_display_name(res) | truncate(50) }}<span class="format-label" property="dc:format" data-format="{{ res.format.lower() or 'data' }}">{{ res.format }}</span>
{{ h.popular('views', res.tracking_summary.total, min=10) }}
</a>
{% endblock %}
{% block resource_item_description %}
<p class="description">
{% if res.description %}
{{ h.markdown_extract(h.get_translated(res, 'description'), extract_length=80) }}
{{ h.markdown_extract(res.description, extract_length=80) }}
{% endif %}
</p>
{% endblock %}
{% block resource_item_explore %}
{% if not url_is_edit %}
{# Only if can edit, explorer button is shown with several facility #}
{% if can_edit %}
<div class="dropdown btn-group">
<a href="#" class="btn btn-primary dropdown-toggle" type="button" id="dropdownExplorer" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa fa-share"></i>
<a href="#" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
<i class="icon-share-alt"></i>
{{ _('Explore') }}
<span class="caret"></span>
</a>
<ul class="dropdown-menu" aria-labelledby="dropdownExplorer">
<ul class="dropdown-menu">
{% block resource_item_explore_links %}
{% block explore_view %}
<li>
<a class="dropdown-item" href="{{ url }}">
{% if res.has_views %}
<i class="fa fa-chart-bar"></i>
{{ _('Preview') }}
{% else %}
<i class="fa fa-info-circle"></i>
{{ _('More information') }}
{% endif %}
</a>
</li>
{% endblock explore_view %}
{% if res.url and h.is_url(res.url) %}
<li>
<a class="dropdown-item resource-url-analytics" href="{{ res.url }}" target="_blank" rel="noreferrer">
{% if res.has_views or res.url_type == 'upload' %}
<i class="fa fa-arrow-circle-down"></i>
{{ _('Download') }}
<a href="{{ url }}">
{% if res.has_views %}
<i class="icon-bar-chart"></i>
{{ _('Preview') }}
{% else %}
<i class="fa fa-external-link"></i>
{{ _('Go to resource') }}
<i class="icon-info-sign"></i>
{{ _('More information') }}
{% endif %}
</a>
</li>
{% if res.url and h.is_url(res.url) %}
{# Added by Francesco Mangiacrapa #}
<script type="text/javascript" >
//handles the click event
function handleClick(event, element, url) {
window.open(url, "_blank", null);
return false;
}
</script>
{# Comment by Francesco Mangiacrapa, in order to avoid "Download" and "Go to Resource" facilities through the Explore button #}
<!--<li>
<a class="resource-url-analytics" href="#" onclick="return handleClick(event, this, '{{ res.url }}');">
{% if res.has_views %}
<i class="icon-download"></i>
{{ _('Download') }}
{% else %}
<i class="icon-external-link"></i>
{{ _('Go to resource') }}
{% endif %}
</a>
</li>-->
{% endif %}
{% if can_edit %}
{#{% if can_edit %}#}
<li>
<a class="dropdown-item" href="{{ h.url_for(pkg.type ~ '_resource.edit', id=pkg.name, resource_id=res.id) }}">
<i class="fa fa-pencil-square"></i>
{# <a href="{{ h.url_for(controller='package', action='resource_edit', id=pkg.name, resource_id=res.id) }}"> #}
<a href="{{ h.url_for('dataset_resource.edit', id=pkg.name, resource_id=res.id) }}">
<i class="icon-edit"></i>
{{ _('Edit') }}
</a>
</li>
{% endif %}
{#{% endif %}#}
{% endblock %}
</ul>
</div>
{% else %}
<a href="{{ url }}" title="Go to {{ url }}">
{{ _('Go to resource') }}
</a>
{% endif %}
{% endif %}
{% endblock %}
{% else %}
{% block resource_item_title2 %}
{# Updated by Francesco Mangiacrapa, see: #10056 #}
<a class="heading" href="javascript:CKAN_D4S_Functions_Util.showPopupD4S(null, 'myPopup_{{res.id}}', '250px');">
{{ h.resource_display_name(res) | truncate(50) }}<span class="format-label" property="dc:format" data-format="{{ res.format.lower() or 'data' }}">{{ res.format }}</span>
{{ h.popular('views', res.tracking_summary.total, min=10) }}
</a>
{% endblock %}
<div class="popupD4S" onclick="CKAN_D4S_Functions_Util.showPopupD4S(event, 'myPopup_{{res.id}}', '250px')">
{% block resource_item_description2 %}
<p class="description">
{% if res.description %}
{{ h.markdown_extract(res.description, extract_length=80) }}
{% endif %}
</p>
{% endblock %}
<span class="popuptext" id="myPopup_{{res.id}}">The resource: '{{ h.resource_display_name(res) | truncate(30) }}' is not accessible as guest user. You must login to access it!</span>
</div>
{% endif %}
{% endblock %}
</li>

View File

@ -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 %}
#}
<section id="dataset-resources" class="resources">
<h2>{{ _('Data and Resources') }}</h2>
{% block resource_list %}
{% if resources %}
<ul class="{% block resource_list_class %}resource-list{% endblock %}">
{% 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 %}
</ul>
{% 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) %}
<p class="empty">This dataset has no data, <a href="{{ url }}">why not add some?</a></p>
{% endtrans %}
{% else %}
<p class="empty">{{ _('This dataset has no data') }}</p>
{% endif %}
{% endblock %}
<script type="text/javascript" >
//Task #10389
window.addEventListener("message",
function (e) {
var curr_loc = window.location.toString()
var orgin = e.origin.toString()
if(curr_loc.startsWith(orgin)){
//alert("ignoring message from myself");
return;
}
//console.log("origin: "+e.data)
if(e.data == null)
return;
var pMess = JSON.parse(e.data)
//console.log(pMess.explore_vres_landing_page)
window.linktogateway = pMess.explore_vres_landing_page;
goToHomeLink("resource-private")
},false);
//Task #10389
goToHomeLink = function (divId) {
var myDiv = document.getElementById(divId);
var myHost = window.linktogateway.substring(0, window.linktogateway.lastIndexOf("/"));
console.log("my host: "+myHost)
if(myDiv && myHost){
myDiv.innerHTML= myDiv.innerHTML + ". <a target=\"_blank\" href="+myHost+"/home"+">Go to Login...</a>";
}
}
</script>
<section id="dataset-resources" class="resources">
<div class="sectiontitle">{{ _('Data and Resources') }}</div>
{% set user = c.user %}
{# Added by Francesco Mangiacrapa #10389 #}
{% if not user %}
<div id="resource-private" class="required-access">To access the resources you must log in</div>
{% endif %}
{% endblock %}
</section>
{# end #}
{% block resource_list %}
{% if resources %}
<ul class="{% block resource_list_class %}resource-list{% endblock %}">
{% block resource_list_inner %}
{% for resource in resources %}
{% snippet 'package/snippets/resource_item.html', pkg=pkg, res=resource %}
{% endfor %}
{% endblock %}
</ul>
{% else %}
{% if h.check_access('resource_create', {'package_id': pkg['id']}) %}
{% trans url=h.url_for(controller='package', action='new_resource', id=pkg.name) %}
<p class="empty">This dataset has no data, <a href="{{ url }}">why not add some?</a></p>
{% endtrans %}
{% else %}
<p class="empty">{{ _('This dataset has no data') }}</p>
{% endif %}
{% endif %}
{% endblock %}
</section>

View File

@ -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 %}
#}
<a href={{ url_dest }} class="btn btn-mini" target="_blank">
<i class="icon-shopping-cart fa fa-shopping-cart"></i>
{{ _('Acquire') }}
</a>

View File

@ -1,70 +1,220 @@
{#
Displays a single of dataset.
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).
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:
Example:
{% snippet 'snippets/package_item.html', package=c.datasets[0] %}
{% 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 %}
{% set truncate = truncate or 180 %}
{% set truncate_title = truncate_title or 80 %}
{% set title = package.title or package.name %}
{% set notes = h.markdown_extract(package.notes, extract_length=truncate) %}
{% set acquired = h.is_dataset_acquired(package) %}
{% set owner = h.is_owner(package) %}
{# {% resource 'd4science_theme/privatedatasets.css' %}#}
{#
CHANGED BY FRANCESCO.MANGIACRAPA
#}
{% block package_item_content %}
<!--Added by Francesco Mangiacrapa, See #6569, #7544, #8032, #8127 -->
{#
{{ package.extras }}
#}
{% block toolbar %}
{% endblock %}
<script type="text/javascript" >
flinktogateway = function (event, orgTitle) {
var myDiv = document.getElementById("d4s_modal-div");
var clickPosition = CKAN_D4S_Functions_Util.getPosition(myDiv, event)
var myPosition = JSON.parse(clickPosition);
//console.log('msg')
var mypopup = document.getElementById("myD4SModal");
mypopup.style.display = "block";
var offset = myDiv.offsetHeight? myDiv.offsetHeight/2 : 0;
myDiv.style.top = (myPosition.posY - Number(offset))+"px";
// Get the <span> element that closes the modal
var span = document.getElementById("d4s_span_modal");
// When the user clicks on <span> (x), close the modal
span.onclick = function() {
mypopup.style.display = "none";
}
// When the user clicks anywhere outside of the modal, close it
window.onclick = function(event) {
if (event.target == myD4SModal) {
mypopup.style.display = "none";
}
}
var myPopupContent = document.getElementById("d4s_p_content");
var msgComplete = "This {{ _('dataset') }} is private to the VRE: <b>"+orgTitle+"</b>.<br/>You can request access to VREs using 'Explore Virtual Research Environments'";
if(window.linktogateway != null)
myPopupContent.innerHTML= msgComplete + " <a target=\"_blank\" href="+window.linktogateway+">Go to Explore...</a>";
else {
myPopupContent.innerHTML= msgComplete;
}
}
</script>
{% if package.private and not h.can_read(package) %}
<li class="{{ item_class or "dataset-item" }}">
{% block content %}
<div class="dataset-content">
{% block heading %}
<h2 class="dataset-heading">
{% block heading_private %}
{% if package.private %}
<span class="dataset-private badge bg-secondary">
<i class="fa fa-lock"></i>
{{ _('Private') }}
</span>
{% endif %}
{% endblock %}
{% block heading_title %}
<a href="{{ h.url_for('%s.read' % package.type, id=package.name) }}" title="{{ title }}">
{{title|truncate(80)}}
</a>
{% endblock %}
{% block heading_meta %}
{% if package.get('state', '').startswith('draft') %}
<span class="badge bg-info">{{ _('Draft') }}</span>
{% elif package.get('state', '').startswith('deleted') %}
<span class="badge bg-danger">{{ _('Deleted') }}</span>
{% endif %}
{{ h.popular('recent views', package.tracking_summary.recent, min=10) if package.tracking_summary }}
{% endblock %}
</h2>
{% endblock %}
{% block notes %}
{% if notes %}
<div>{{ notes|urlize }}</div>
{% else %}
<p class="empty">{{ h.humanize_entity_type('package', package.type, 'no description') or _("There is no description for this dataset") }}</p>
{% endif %}
{% endblock %}
<!-- The Modal -->
<div id="myD4SModal" class="d4s_modal">
<!-- Modal content -->
<div id="d4s_modal-div" class="d4s_modal-content">
<h3>Access required...</h3>
<span id="d4s_span_modal" class="d4s_close">&times;</span>
<p id="d4s_p_content"></p>
</div>
</div>
{% block resources %}
{% if package.resources and not hide_resources %}
{% block resources_outer %}
<ul class="dataset-resources list-unstyled">
{% block resources_inner %}
{% for resource in h.dict_list_reduce(package.resources, 'format') %}
<li>
<a href="{{ h.url_for(package.type ~ '.read', id=package.name) }}" class="badge badge-default" data-format="{{ resource.lower() }}">{{ resource }}</a>
</li>
{% endfor %}
{% endblock %}
</ul>
{% endblock %}
{% set orgTitle = package.organization.title %}
<div class="dataset-content d4s_div_clickable" onclick="flinktogateway(event, '{{orgTitle}}')" title="This {{ _('dataset') }} is private to the VRE: {{package.organization.title}}. You can request access to VREs using 'Explore Virtual Research Environments'">
<div class="show_meatadatatype">
{% set mvalue = h.d4science_theme_get_systemtype_value_from_extras(package, package.extras) %}
{% set mcolor = h.d4science_get_color_for_type(mvalue) %}
<span class="button__badge" style="color: {{ mcolor }}">{{ mvalue }}</span>
</div>
<h3 class="dataset-heading">
{% if package.private and not h.can_read(package) %}
<span class="dataset-private label label-inverse">
<i class="icon-lock"></i>
{{ _('Private') }}
</span>
{{ _(h.truncate(title, truncate_title)) }}
{% endif %}
{% if acquired and not owner %}
<span class="dataset-private label label-acquired">
<i class="icon-shopping-cart"></i>
{{ _('Acquired') }}
</span>
{% endif %}
<!-- Customizations Acquire Button -->
{% if package.private and not h.can_read(package) %}
{# {{ _(h.truncate(title, truncate_title)) }} Dataset#} <!-- THIS IS PACKAGE TITLE -->
<div class="divider"/>
{{ h.acquire_button(package) }}
{% else %}
{{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }}
{% endif %}
<!-- End of customizations Acquire Button -->
{% if package.get('state', '').startswith('draft') %}
<span class="label label-info">{{ _('Draft') }}</span>
{% elif package.get('state', '').startswith('deleted') %}
<span class="label label-important">{{ _('Deleted') }}</span>
{% endif %}
{{ h.popular('recent views', package.tracking_summary.recent, min=10) if package.tracking_summary }}
</h3>
{% if banner %}
<span class="banner">{{ _('Popular') }}</span>
{% endif %}
{% endblock %}
{% endblock %}
{% if notes %}
<div style="word-wrap:break-word;">{{ notes|urlize(70,target='_blank') }}</div><!-- THIS IS PACKAGE DESCRIPTION -->
{% endif %}
</div>
</li>
{% endblock %}
{% else %}
<li class="{{ item_class or "dataset-item" }}">
<div class="dataset-content">
<div class="show_meatadatatype">
{% set mvalue = h.d4science_theme_get_systemtype_value_from_extras(package, package.extras)%}
{% set mcolor = h.d4science_get_color_for_type(mvalue) %}
<span class="button__badge" style="color: {{ mcolor }}">{{ mvalue }}</span>
</div>
<h3 class="dataset-heading">
{% if package.private and not h.can_read(package) %}
<span class="dataset-private label label-inverse">
<i class="icon-lock"></i>
{{ _('Private') }}
</span>
{% endif %}
{% if acquired and not owner %}
<span class="dataset-private label label-acquired">
<i class="icon-shopping-cart"></i>
{{ _('Acquired') }}
</span>
{% endif %}
{% if owner %}
<span class="dataset-private label label-owner">
<i class="icon-user"></i>
{{ _('Owner') }}
</span>
{% endif %}
<!-- Customizations Acquire Button -->
{% if package.private and not h.can_read(package) %}
{{ _(h.truncate(title, truncate_title)) }}
<div class="divider"/>
{{ h.acquire_button(package) }}
{% else %}
{# CHANGED BY FRANCESCO MANGIACRAPA #}
{{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='dataset', action='read', id=package.name)) }}
{% endif %}
<!-- End of customizations Acquire Button -->
{% if package.get('state', '').startswith('draft') %}
<span class="label label-info">{{ _('Draft') }}</span>
{% elif package.get('state', '').startswith('deleted') %}
<span class="label label-important">{{ _('Deleted') }}</span>
{% endif %}
{{ h.popular('recent views', package.tracking_summary.recent, min=10) if package.tracking_summary }}
</h3>
{% if banner %}
<span class="banner">{{ _('Popular') }}</span>
{% endif %}
{% if notes %}
<div style="word-wrap:break-word;">{{ notes|urlize(70,target='_blank') }}</div>
{% endif %}
</div>
{% if package.resources and not hide_resources %}
{# CHANGED BY FRANCESCO MANGIACRAPA, see: #7055 #}
<ul class="dataset-resources unstyled">
{% 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) #}
<li>
{% if c.userobj %}
{# USER IS LOGGED #}
{# CHANGED BY FRANCESCO MANGIACRAPA #11178 #}
<a href="{{ resource.url }}" target="_blank" title="{{ resource.name or resource.description }}" class="label" data-format="{{ resource_format.lower() }}">{{ resource_format }}</a>
{% else %}
{# CHANGED BY FRANCESCO MANGIACRAPA #12377 #}
<a class="label" href="javascript:CKAN_D4S_Functions_Util.showPopupD4S(null, 'myPopup_{{resource.id}}');" title="{{ resource.name or resource.description }}" class="label" data-format="{{ resource.format.lower() }}">{{ resource.format }}</a>
<div class="popupD4SNoArrow" style="display: inline;" onclick="CKAN_D4S_Functions_Util.showPopupD4S(event, 'myPopup_{{resource.id}}')">
<span class="popuptext" id="myPopup_{{resource.id}}">The resource: '{{ h.resource_display_name(resource) | truncate(30) }}' is not accessible as guest user. You must login to access it!</span>
</div>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endif %}
{% endblock %}

View File

@ -13,7 +13,7 @@
<ul class="{{ _class }}">
{% for tag in tags %}
<li>
<a class="{% block tag_list_item_class %}tag{% endblock %}" href="{% url_for 'dataset.search', tags=tag.name %}" title="{{ tag.display_name }}">{{ tag.display_name|truncate(22) }}</a>
<a class="{% block tag_list_item_class %}tag{% endblock %}" href="{% url_for 'dataset.search', tags=tag.name %}" title="{{ tag.display_name }}"></a>
</li>
{% endfor %}
</ul>

View File

@ -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 %}
<h2 class="hide-heading">{{ _('Acquired Datasets') }}</h2>
{% if acquired_datasets %}
{% snippet 'snippets/package_list.html', packages=acquired_datasets %}
{% else %}
<p class="empty">
{{ _('You haven\'t acquired any datasets.') }}
{% link_for _('Acquire one now?'), controller='package', action='search' %}
</p>
{% endif %}
{% endblock %}

View File

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

View File

@ -1,3 +1,4 @@
webhelpers2
xmltodict
pyqrcode
sqlalchemy

View File

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