From 4a6c16bec69bd13365723c5edd813eb627fe321c Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Tue, 8 Oct 2024 15:38:23 +0200 Subject: [PATCH 01/27] test footer --- .DS_Store | Bin 0 -> 8196 bytes ckanext/.DS_Store | Bin 0 -> 6148 bytes ckanext/d4science/.DS_Store | Bin 0 -> 8196 bytes ckanext/d4science/helpers.py | 4 ++++ ckanext/d4science/public/batman_logo.png | Bin 0 -> 83744 bytes ckanext/d4science/templates/footer.html | 16 ++++++++++++++++ 6 files changed, 20 insertions(+) create mode 100644 .DS_Store create mode 100644 ckanext/.DS_Store create mode 100644 ckanext/d4science/.DS_Store create mode 100644 ckanext/d4science/public/batman_logo.png create mode 100644 ckanext/d4science/templates/footer.html diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0b344fc38333cc19a8c23d044d922ed2b3ff1836 GIT binary patch literal 8196 zcmeHMU279T6ur|%v!N(KDi%Rl@J(t>Kd6+xq-i1)Ybo7gK`m*LUDKt>ZfrJ2DaAbN zUl0+W6@BzoAAIl!_!s;cdgi0;&c-T45KCuZ=4@u}nb~{p%x>?Nh*+WHP7;j~k%h`J zJA&pHg^%;vSB6y2B_IQz8udx17B*XbmZrlfU=%P47zK<1MuGoD0lc$$v5tA~%Td!B z1&jixQUU&auu(bY74{^GM+X{_0svF!76sQ?egU)<<`woNVg!XrDo|3HN->zEqu*9` zd4)ZRl1@w|A55*xREEOD>X_da=EU+6O=}b|3iK<$bN3WYP@Q~=wEUgrUd6|nVX;_k zgjMueTQ66ieYp2!K_7R2JKoxkeH%Et1uCVfMmtoYfINCa(NZI*X#50=GS*}-wrs}k*a??m!>_fa_vWn`vSb)YZ5YN7@dAqo>{ z`%VhGlF&O%%d|v`RH7AfsH8`S@m&{>&IsMXSz7_)78nk#V->_zXNlAJHg*~_vFcRJZ3ov1>7(EX?kK z6G;WgPuRmF%^nV#ViY*B3Jl9TALIFd>G<#eC)Pi6<3<6az@HUhsiIRX!0zA=)g)yI z9X9F~DlgowCs9z)a1-7y(sAJ7KMZkgtFgSooemtZieFZ9?B~ml?FWN{A4t=w$mui9 zTC*0@Rx=x{k_pUU%TJr$GH=Z@=HzR-nP0GJC8x(HnH!k4OFa=_7bv={EZS~?RnlzB zE%+ifBRjOH7R~8BySu~VWBta+_?5kWV|RRbu-_OR8Qa^7YKi3KYje3xr{uCHB3mH1 z!wG3sSc9jiG~mLvwpC{2eJ!)kx?amI+7dh4dag}x@91ogw*dS~9W zHj5tr&y&nyUfS8BS=(Z6#(qSz0eu*7C8u{B@tdQC`v<1^oih5w4-8_gTOOtEa?!I3 zGwbU<4YG&yrJUa8TOM?)niOhPGk0j>KlugTai^++vlFwvnwx;w-MTewR0FtSj zQjNxICWU0)WiE|Ilc~GPqCp?NvUeZ@8&H8~@B&`KKD>cX@CCk+2v64FWuD)yRRbo?9>UGz8Uw?eOw)mDW zs|MqhSm8H4%NvJgCeQ2t?ujjxq82;RE9?vz2pRYv8Q}YafrVpAVO65oISy==`k=5CG7vKGPcc9uQ<iz#nDc2P|>}ivR!s literal 0 HcmV?d00001 diff --git a/ckanext/d4science/.DS_Store b/ckanext/d4science/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0559bfc7bf7b210a69cafac0f5713c08d8300f67 GIT binary patch literal 8196 zcmeHMy>AmS6n}2h_Cla!0MP|;5)-H@X^RRKOB&Jv6$yz{RN_NvK2Gh4%U#qYL{(KO zFw#GQffWfBB=`pyy0KIYjQjy?3=Hs|?IiY{gJ9yzj_h~w`#s-#zZcuJ3jnOpsm}w< z0Dyy)WHy71p^%;*z<4| zLY)twTpMvZ7l@(_Lyn>HaPV=!gQ6d1(4^Pn=VMP`^bu^~8)~|TJY%Yd3R3rR`WB9) zXjL!YW{6Zbq{s>hRP(Sa(@Z!9{>6T7J7NyApV zt)`qeZ>r6}Z?$u|BQ`dknwXq&rksrPptK!zOYKU#(<)SU?$NjPCtqzY2ejtqwFBv}_nG4E`HXY7{r09esK&Ry-qe7|V~5hKMFa6y|kK zi$p={5gcDbZWOKR`TDt&{WVTQ3_Ug@bWb;ldVqu?$!S{uu^Xs#q!(Fr5Cz8;^|6+Ah{jtW*fs6Ox-?LlElc$8nT{e;DGk pD|LA(dqScH%Wt>{u={^Y-&J`clwCRbSF^UCDoDHkbMF4a<~yZ7F0uds literal 0 HcmV?d00001 diff --git a/ckanext/d4science/helpers.py b/ckanext/d4science/helpers.py index 98b7f2d..fb4fe3d 100644 --- a/ckanext/d4science/helpers.py +++ b/ckanext/d4science/helpers.py @@ -6,4 +6,8 @@ def d4science_hello(): def get_helpers(): return { "d4science_hello": d4science_hello, + "get_deliverable_type": get_deliverable_type } + +def get_deliverable_type(): + return "This is a custom deliverable type!" diff --git a/ckanext/d4science/public/batman_logo.png b/ckanext/d4science/public/batman_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e1175acdb7caa29a3cf26a79025cfa609047a09d GIT binary patch literal 83744 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

X4FR9K71vGj6{vvzW4sC3NDuQ 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 + +{% endblock %} + +{% block footer_links_ckan %} + + {{ super() }} + +

  • Some Link
  • +
  • Another Link
  • + +{% endblock %} \ No newline at end of file From 760cf98b4ddb9047849d0a8ec969b2542a93007d Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Tue, 8 Oct 2024 15:56:35 +0200 Subject: [PATCH 02/27] test footer --- ckanext/d4science/plugin.py | 2 +- ckanext/d4science/templates/footer.html | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ckanext/d4science/plugin.py b/ckanext/d4science/plugin.py index 1d886d8..ed79b52 100644 --- a/ckanext/d4science/plugin.py +++ b/ckanext/d4science/plugin.py @@ -9,7 +9,7 @@ from ckan.model import Package # import ckanext.d4science.cli as cli # import ckanext.d4science.helpers as helpers -# import ckanext.d4science.views as views +import ckanext.d4science.views as views # from ckanext.d4science.logic import ( # action, auth, validators # ) diff --git a/ckanext/d4science/templates/footer.html b/ckanext/d4science/templates/footer.html index fe0c932..7194c52 100644 --- a/ckanext/d4science/templates/footer.html +++ b/ckanext/d4science/templates/footer.html @@ -2,6 +2,7 @@ {% block footer_links %} +

    Footer test!

    test {% endblock %} From 89b1d0a4a337f45b8a87b7fd7ad28123dd6b1ff7 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Tue, 8 Oct 2024 16:15:55 +0200 Subject: [PATCH 03/27] test footer --- ckanext/d4science/templates/footer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckanext/d4science/templates/footer.html b/ckanext/d4science/templates/footer.html index 7194c52..c9623d0 100644 --- a/ckanext/d4science/templates/footer.html +++ b/ckanext/d4science/templates/footer.html @@ -1,6 +1,6 @@ {% ckan_extends %} -{% block footer_links %} +{% block d4science_footer_links %}

    Footer test!

    test From 00c0bedacd0cda69a03f6b46cc7e54f04177197a Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Fri, 11 Oct 2024 10:55:30 +0200 Subject: [PATCH 04/27] convert to python3 --- ckanext/d4science/helpers.py | 763 ++++++++++++++++++++++++++++++++++- ckanext/d4science/plugin.py | 228 ++++++++++- 2 files changed, 975 insertions(+), 16 deletions(-) diff --git a/ckanext/d4science/helpers.py b/ckanext/d4science/helpers.py index fb4fe3d..6ac0738 100644 --- a/ckanext/d4science/helpers.py +++ b/ckanext/d4science/helpers.py @@ -1,13 +1,762 @@ -def d4science_hello(): - return "Hello, d4science!" +import ckan.authz as authz +import ckan.model as model +from webhelpers.html import escape, HTML, literal, url_escape +from webhelpers.text import truncate +import ckan.lib.helpers as h +import ckan.logic as logic +from ckan.common import config +from ckanext.d4science.d4sdiscovery.d4s_namespaces_controller import D4S_Namespaces_Controller +from ckanext.d4science.d4sdiscovery.d4s_namespaces_extras_util import D4S_Namespaces_Extra_Util +from ckanext.d4science.qrcodelink.generate_qrcode import D4S_QrCode +import urllib.request, urllib.error, urllib.parse + +from ckan.common import ( + _, ungettext, g, c, request, session, json, OrderedDict +) + +from flask import Blueprint, render_template, g, request, url_for, current_app + +import random +from operator import itemgetter +from logging import getLogger +import base64 +import sys, os, re +import configparser +import collections + +log = getLogger(__name__) + +systemtype_field = 'systemtypefield' +systemtype_field_default_value = 'system:type' +ic_proxy_url_field = 'ic_proxy_url' +ic_proxy_url_field_default_value = "https://registry.d4science.org/icproxy/gcube/service" +application_token_field = 'application_token' +namespaces_generic_resource_id_default_value = "23d827cd-ba8e-4d8c-9ab4-6303bdb7d1db" +namespaces_gr_id_fieldname = "namespaces_generic_resource_id" +namespaceseparator_field = 'namespace_separator' +namespaceseparator_field_default_value = ':' +systemtype_rgb_colors = ['#c0392b ', '#585858', '#04407C', '#9b59b6', '#2ecc71', '#16a085', '#7f8c8d ', '#2ecc71', + '#FA8072', '#00FFFF', '#C76611', '#f39c12', '#800000'] +systemtype_field_colors = 'systemtype_field_colors' + +systemtype_cms_fields_placeholders = {'prefix': 'system:cm_', 'item_status': 'system:cm_item_status'} + +NOCATEOGORY = 'nocategory' +TRANSLATE_OF_ = 'translate_of_' + +ctg_namespace_ctrl = None -def get_helpers(): +def get_user_role_for_group_or_org(group_id, user_name): + ''' Returns the user's role for the group. (Ignores privileges that cascade + in a group hierarchy.)''' + return authz.users_role_for_group_or_org(group_id, user_name) + +def get_parents_for_group(group_name_or_id): + ''' Returns the user's role for the group. (Ignores privileges that cascade + in a group hierarchy.)''' + group = model.Group.get(group_name_or_id) + if group: + return model.Group.get_parent_group_hierarchy(group) + else: + return None + + +def get_header_param(parameter_name, default=None): + ''' This function allows templates to access header string parameters + from the request. ''' + return request.headers.get(parameter_name, default) + + +def get_request_param(parameter_name, default=None): + ''' This function allows templates to access query string parameters + from the request. ''' + return request.args.get(parameter_name, default) + #return request.params.get(parameter_name, default) + + +def get_cookie_value(cookie_name, default=None): + ''' This function allows templates to access cookie by cookie_name parameter + from the request. ''' + + value = request.cookies.get(cookie_name) + + if value is None: + print('cookie: ' + cookie_name + ', has value None') + else: + print('cookie: ' + cookie_name + ', has value ' + value) + + return value + + +def markdown_extract_html(text, extract_length=190, allow_html=False): + ''' Returns the plain text representation of markdown encoded text. That + is the texted without any html tags. If extract_length is 0 then it + will not be truncated.''' + if not text: + return '' + if allow_html: + plain = h.markdown(text.strip()) + else: + plain = h.RE_MD_HTML_TAGS.sub('', h.markdown(text)) + + if not extract_length or len(plain) < extract_length: + return literal(plain) + return literal(str(truncate(plain, length=extract_length, indicator='...', whole_word=True))) + + +def get_systemtype_field_dict_from_session(): + '''Return the value of 'ckan.d4science_theme.metadatatypefield' + read from production.ini''' + + systemtype_fieldname = session.get(systemtype_field) + + if systemtype_fieldname is None: + log.info(systemtype_field + " not found in session, loading from config") + else: + log.debug(systemtype_field + " found in session having value: %s" % systemtype_fieldname) + return systemtype_fieldname + + systemtype_fieldname = current_app.config.get('ckan.d4science_theme.' + systemtype_field) + #systemtype_fieldname = config.get('ckan.d4science_theme.' + systemtype_field) + + if systemtype_fieldname is None: + log.info( + systemtype_field + " field does not exist in production.ini, returning default value %s" % systemtype_field_default_value) + systemtype_fieldname = systemtype_field_default_value + + separator = get_namespace_separator_from_session() + log.debug("Replacing %s" % separator + " with empty string for key %s" % systemtype_field) + systemtype_fieldname_name = systemtype_fieldname.replace(separator, "") + purgedfieldname = purge_namespace_to_fieldname(systemtype_fieldname) + log.debug("Setting %s" % systemtype_fieldname + " in session for key %s" % systemtype_field) + session[systemtype_field] = {'id': systemtype_fieldname, 'name': systemtype_fieldname_name, + 'title': purgedfieldname} + session.modified = True + #session.save() old pylons(?) + return session[systemtype_field] + + +def get_d4s_namespace_controller(): + '''Instance the D4S_Namespaces_Controller and check that the namespaces are not empty reading it from IS and/or using a Caching system. + The ic-proxy-url is built by reading the configurations from production.ini''' + + d4s_extras_controller = D4S_Namespaces_Controller.getInstance() + global ctg_namespace_ctrl + + if ctg_namespace_ctrl is not None: + log.info("ctg_namespace_ctrl with configurations is NOT None") + the_namespaces = d4s_extras_controller.load_namespaces(ctg_namespace_ctrl['ic_proxy_url'], + ctg_namespace_ctrl['resource_id'], + ctg_namespace_ctrl['application_token']) + log.debug("the_namespaces are %s" % the_namespaces) + + if the_namespaces is None or len(the_namespaces) == 0: + log.info("D4S_Namespaces_Controller obj with none or empty namespaces, going to read them") + else: + log.info("d4s_namespaces_controller found and the namespaces property is not empty: %s" % d4s_extras_controller) + return d4s_extras_controller + else: + log.info("ctg_namespace_ctrl with configurations is None, instancing it") + + ic_proxy_url_value = current_app.config.get('ckan.d4science_theme.' + ic_proxy_url_field) + #ic_proxy_url_value = config.get('ckan.d4science_theme.' + ic_proxy_url_field) old + + if ic_proxy_url_value is None: + log.info( + "ckan.d4science_theme." + ic_proxy_url_field + " field does not exist in production.ini, returning default value %s" % ic_proxy_url_field_default_value) + ic_proxy_url_value = ic_proxy_url_field_default_value + + application_token_fieldname = current_app.config.get('ckan.d4science_theme.' + application_token_field) + #application_token_fieldname = config.get('ckan.d4science_theme.' + application_token_field) + + if application_token_fieldname is None: + log.error("ckan.d4science_theme." + application_token_field + " field does not exist in production.ini!!!") + application_token_fieldname = None + + namespaces_gr_id_fieldname_value = current_app.config.get('ckan.d4science_theme.' + namespaces_gr_id_fieldname) + #namespaces_gr_id_fieldname_value = config.get('ckan.d4science_theme.' + namespaces_gr_id_fieldname) + + if namespaces_gr_id_fieldname_value is None: + log.error("ckan.d4science_theme." + application_token_field + " field does not exist in production.ini!!!") + namespaces_gr_id_fieldname_value = namespaces_generic_resource_id_default_value + + # filling the ctg_namespace_ctrl with IS configurations to perform the query for loading the namespaces from IS + ctg_namespace_ctrl = {'ic_proxy_url': ic_proxy_url_value, + 'application_token': application_token_fieldname, + 'resource_id': namespaces_gr_id_fieldname_value} + + d4s_extras_controller.load_namespaces(ctg_namespace_ctrl['ic_proxy_url'], ctg_namespace_ctrl['resource_id'], + ctg_namespace_ctrl['application_token']) + + return d4s_extras_controller + + +def get_extras_indexed_for_namespaces(extras): + namespace_dict = get_namespaces_dict() + # log.info("my_namespace_dict %s" % namespace_dict) + my_extra = get_extras(extras) + # log.info("my_extra is %s" % my_extra) + # d4s_extras_controller = D4S_Namespaces_Controller.getInstance() + # extras_indexed_for_categories = d4s_extras_controller.get_extras_indexed_for_namespaces(namespace_dict, my_extra) + + extras_indexed_for_categories = D4S_Namespaces_Extra_Util().get_extras_indexed_for_namespaces(namespace_dict, + my_extra) + return extras_indexed_for_categories + + +def get_namespaces_dict(): + d4s_extras_controller = get_d4s_namespace_controller() + + if d4s_extras_controller is not None: + return d4s_extras_controller.get_dict_ctg_namespaces() + else: + log.info("local_extras_controller is null, returning empty dictionary for namespaces") + return {} + + +def get_extra_for_category(extras_indexed_for_categories, key_category): + if key_category in extras_indexed_for_categories: + catalogue_namespace = extras_indexed_for_categories[key_category] + return catalogue_namespace.extras + + return [] + + +def get_systemtype_value_from_extras(package, extras=None): + '''Returns the value of metadata fied read from key 'metadatatype' + stored into extra fields if it exists, 'No Type' otherwise''' + systemtype_dict = get_systemtype_field_dict_from_session() + + no_type = 'No Type' + + if extras is None: + return no_type + + for extra in extras: + k, v = extra['key'], extra['value'] + log.debug("key is %s" % k) + log.debug("value is %s" % v) + if k == str(systemtype_dict['id']): + return v + + return no_type + + +def get_namespace_separator_from_session(): + '''Returns the character used to separate namespace from fieldname''' + + separator = session.get(namespaceseparator_field) + + if separator is None: + log.info(namespaceseparator_field + " not found in session, loading from config") + else: + log.debug(namespaceseparator_field + " found in session: %s" % separator) + return separator + + namespace_sep = config.get('ckan.d4science_theme.' + namespaceseparator_field) + + if namespace_sep is None: + log.info( + namespaceseparator_field + " field does not exist in production.ini, returning default value %s" % namespaceseparator_field_default_value) + namespace_sep = namespaceseparator_field_default_value + + log.debug("Setting %s" % namespace_sep + " in session for key %s" % namespaceseparator_field) + session[namespaceseparator_field] = namespace_sep + return namespace_sep + + +def get_extras(package_extras, auto_clean=False, subs=None, exclude=None): + ''' Used for outputting package extras + + :param package_extras: the package extras + :type package_extras: dict + :param auto_clean: If true capitalize and replace -_ with spaces + :type auto_clean: bool + :param subs: substitutes to use instead of given keys + :type subs: dict {'key': 'replacement'} + :param exclude: keys to exclude + :type exclude: list of strings + ''' + + # If exclude is not supplied use values defined in the config + if not exclude: + exclude = g.package_hide_extras + output = [] + for extra in package_extras: + if extra.get('state') == 'deleted': + continue + k, v = extra['key'], extra['value'] + if k in exclude: + continue + if subs and k in subs: + k = subs[k] + elif auto_clean: + k = k.replace('_', ' ').replace('-', ' ').title() + if isinstance(v, (list, tuple)): + v = ", ".join(map(str, v)) + output.append((k, v)) + return output + + +def purge_namespace_to_fieldname(fieldname): + separator = get_namespace_separator_from_session() + + if fieldname is None: + return "" + + if separator not in fieldname: + return fieldname + + end = fieldname.index(separator) + 1 + max_l = len(fieldname) + if end < max_l: + return fieldname[end:max_l] + return fieldname + + +def purge_namespace_to_string(facet): + if not g.search_facets or \ + not g.search_facets.get(facet) or \ + not g.search_facets.get(facet).get('items'): + return "" + + facet_name = g.search_facets.get(facet) + print("facet_name " + str(facet_name)) + #logging.debug("facet_name " + str(facet_name)) preferibile in flask + end = str(facet_name).index(":") + if end <= len(facet_name): + return facet_name[:end] + return facet_name + + +def count_facet_items_dict(facet, limit=None, exclude_active=False): + if not g.search_facets or \ + not g.search_facets.get(facet) or \ + not g.search_facets.get(facet).get('items'): + return 0 + facets = [] + for facet_item in g.search_facets.get(facet)['items']: + if not len(facet_item['name'].strip()): + continue + if not (facet, facet_item['name']) in list(request.args.items()): + facets.append(dict(active=False, **facet_item)) + elif not exclude_active: + facets.append(dict(active=True, **facet_item)) + + # for count, + # print "facets " + str(facets) + total = len(facets) + log.debug("total facet: %s are %d" % (facet, total)) + return total + + +def random_color(): + rgbl = [255, 0, 0] + random.shuffle(rgbl) + return tuple(rgbl) + + +def check_url(the_url): + try: + urllib.request.urlopen(the_url) + return True + except urllib.error.HTTPError as e: + # print(e.code) + return False + except urllib.error.URLError as e: + # print(e.args) + return False + except Exception as error: + # print(error) + return False + + +def get_color_for_type(systemtype_field_value): + '''Return a color assigned to a system type''' + + systemtypecolors = session.get(systemtype_field_colors) + # log.info("color: getting color for type: %s" %systemtype_field_value) + + if systemtypecolors is None: + log.info("color: " + systemtype_field_colors + " not found in session, creating new one") + systemtypecolors = {} + session[systemtype_field_colors] = systemtypecolors + else: + log.debug("color: " + systemtype_field_colors + " found in session having value: %s" % systemtypecolors) + + e_color = systemtypecolors.get(systemtype_field_value) + + if e_color is None: + usedcolorsLen = len(systemtypecolors) + colorsLen = len(systemtype_rgb_colors) + index = usedcolorsLen if usedcolorsLen < colorsLen else random.randint(0, colorsLen - 1) + e_color = systemtype_rgb_colors[index] + # log.debug("color: adding color %s" %e_color +" index is: "+str(index)) + systemtypecolors[systemtype_field_value] = e_color + session[systemtype_field_colors] = systemtypecolors + + session.modified = True + #session.save() + # log.debug("color: returning color %s" %e_color +" for type: "+systemtype_field_value) + return e_color + + +def ordered_dictionary(list_to_be_sorted, property='name', ordering="asc"): + # print ("dict %s" %list_to_be_sorted) + + ord = False if ordering == "asc" else True + + if list_to_be_sorted: + return sorted(list_to_be_sorted, key=itemgetter(property), reverse=ord) + + return list_to_be_sorted + + +def qrcode_for_url(url): + if url: + try: + qr_code = D4S_QrCode(url) + image_path = qr_code.get_qrcode_path() + with open(image_path, "rb") as image_file: + return base64.b64encode(image_file.read()) + return "" + except Exception as error: + log.error("Error on getting qrcode for url: " + url + "error: %s" % error) + + return "" + + +def get_list_of_organizations(limit=10, sort='packages'): + to_browse_organizations = [] + try: + data = {} + + if sort: + data['sort'] = sort + + data['limit'] = limit + data['all_fields'] = True + ordered_organizations = [] + ordered_organizations = logic.get_action('organization_list')({}, data) + + for organization in ordered_organizations: + try: + to_browse_obj = {} + + if not organization['name']: + continue + + to_browse_obj['name'] = organization['name'] + + if 'package_count' in organization: + to_browse_obj['package_count'] = organization['package_count'] + + if 'display_name' in organization: + to_browse_obj['display_name'] = organization['display_name'] + + image_url = get_url_to_icon_for_ckan_entity(organization['name'], 'organization', False) + + # Using ICON as first option + if image_url: + to_browse_obj['url'] = image_url + # Using object image_url as second one + elif 'image_url' in organization and organization['image_url']: + to_browse_obj['url'] = organization['image_url'] + # Default placeholder + else: + to_browse_obj['url'] = h.url_for('static', filename='images/organisations/icon/placeholder-organization.png') + + to_browse_organizations.append(to_browse_obj) + except (logic.NotFound, logic.ValidationError, logic.NotAuthorized) as error: + # SILENT + log.warn("Error on putting organization: %s" % error) + + log.info("browse %d" % len(ordered_organizations) + " organisation/s") + except (logic.NotFound, logic.ValidationError, logic.NotAuthorized) as error: + log.error("Error on getting organizations: %s" % error) + return [] + + return to_browse_organizations + + +def get_list_of_groups(limit=10, sort='package_count'): + to_browse_groups = [] + try: + data = {} + if sort: + data['sort'] = sort + + data['limit'] = limit + data['all_fields'] = True + ordered_groups = [] + ordered_groups = logic.get_action('group_list')({}, data) + + for group in ordered_groups: + # print "\n\ngroup %s" %group + try: + to_browse_obj = {} + + if not group['name']: + continue + + to_browse_obj['name'] = group['name'] + + if 'package_count' in group: + to_browse_obj['package_count'] = group['package_count'] + + if 'display_name' in group: + to_browse_obj['display_name'] = group['display_name'] + + if 'image_url' in group and group['image_url']: + to_browse_obj['url'] = group['image_url'] + else: + to_browse_obj['url'] = get_url_to_icon_for_ckan_entity(group['name'], 'group') + + to_browse_groups.append(to_browse_obj) + except (logic.NotFound, logic.ValidationError, logic.NotAuthorized) as error: + # SILENT + log.warn("Error on putting group: %s" % error) + + log.info("browse %d" % len(ordered_groups) + " organisation/s") + except (logic.NotFound, logic.ValidationError, logic.NotAuthorized) as error: + log.error("Error on getting group: %s" % error) + return [] + + return to_browse_groups + + +def get_browse_info_for_organisations_or_groups(type='organization', limit=10, sort_field=None): + sort = None + if sort_field: + sort = sort_field + + if type == 'organization': + if sort: + return get_list_of_organizations(limit, sort) + else: + return get_list_of_organizations(limit) + + elif type == 'group': + if sort: + return get_list_of_groups(limit, sort) + else: + return get_list_of_groups(limit) + + return [] + + +def get_image_display_for_group(item_id): + if item_id: + try: + item_obj = model.Group.get(item_id) + + if item_obj and item_obj.image_url: + return item_obj.image_url + else: + return h.url_for('static', filename='images/groups/icon/placeholder-group.png') + + except Exception as error: + log.error("Error on getting item obj: %s" % item_id + "error: %s" % error) + + +def get_application_path(): + if getattr(sys, 'frozen', False): + # If the application is run as a bundle, the pyInstaller bootloader + # extends the sys module by a flag frozen=True and sets the app + # path into variable _MEIPASS'. + application_path = sys._MEIPASS + else: + application_path = os.path.dirname(os.path.abspath(__file__)) + + return application_path + + +''' +Get icon url for input entity type +@:param default_placeholder if True returns the URL of default image, otherwise None. +''' + + +def get_url_to_icon_for_ckan_entity(item_name, entity_type=None, default_placeholder=True): + if not entity_type or not item_name: + return None + + dir_images_full_path = get_application_path() + "/public/images" + dir_images_relative_path = "/images" + + if entity_type == 'group': + dir_images_full_path += "/groups" + dir_images_relative_path += "/groups" + placeholder_icon = "placeholder-group.png" + elif entity_type == 'organization': + dir_images_full_path += "/organisations" + dir_images_relative_path += "/organisations" + placeholder_icon = "placeholder-organization.png" + elif entity_type == 'type': + dir_images_full_path += "/types" + dir_images_relative_path += "/types" + placeholder_icon = "placeholder-type.png" + else: + return None + + icon_path = os.path.join(dir_images_full_path, "icon", item_name.lower() + ".png") + if os.path.isfile(icon_path): + return h.url_for('static', filename=os.path.join(dir_images_relative_path, "icon", item_name.lower() + ".png")) + elif default_placeholder: + return h.url_for('static', filename=os.path.join(dir_images_relative_path, "icon", placeholder_icon)) + + return None + + +def get_user_info(user_id_or_name): + if user_id_or_name: + try: + + item_obj = model.User.get(user_id_or_name) + + if item_obj: + return item_obj + + return None + except Exception as error: + log.error("Error on getting item obj: %s" % user_id_or_name + "error: %s" % error) + + return None + + +''' +Search the value of my_search_string into input file {ckan_po_file} or the default file ckan.po provided as CKAN language +and returns its translate +''' + + +def get_ckan_translate_for(ckan_po_file, my_search_string): + my_translate = session.get(TRANSLATE_OF_ + my_search_string) + + if not my_search_string: + return "" + + if my_translate: + log.info("Translate of '%s' " % my_search_string + " found in session as: %s" % my_translate) + return my_translate + + if not ckan_po_file: + ckan_po_file = "/usr/lib/ckan/default/src/ckan/ckan/i18n/en_gcube/LC_MESSAGES/ckan.po" + + numlines = 0 + numfound = 0 + found = 0 + line_text = "" + + try: + infile = open(ckan_po_file, "r") + + for line in infile: + numlines += 1 + if found > 0: + numfound += 1 + line_text += str(line) + found = 0 # reset found + + # found += line.upper().count(my_search_string.upper()) + found += line.count(my_search_string) + + if found > 0: + log.debug("The search string '%s'" % my_search_string + " was found. Read the line: %s" % str(line)) + + infile.close() + + except Exception as e: + print("Exception during parsing the file %s" % ckan_po_file, e) + + log.info("Recap: '%s' was found" % my_search_string + " %i times " % numfound + "in %i lines" % numlines) + log.debug("Line text is: %s" % line_text) + + pattern = '"([A-Za-z0-9_ \./\\-]*)"' + m = re.search(pattern, line_text) + + try: + my_translate = m.group() + except Exception as e: + print("Pattern %s" % my_search_string + " not found ", e) + + if my_translate: + log.debug("Replacing quotas...") + my_translate = my_translate.replace("\"", "") + + log.info("Found the string '%s'" % my_translate + " that translating '%s'" % my_search_string) + + session[TRANSLATE_OF_ + my_search_string] = my_translate + session.modified = True + #session.save() capire se serve, approccio standard con modified, save forza il salvataggio esplicito della sessione + + return my_translate + + +def get_location_to_bboxes(): + config = configparser.ConfigParser() + config.optionxform = str + location_to_bboxes = {} + try: + bboxes_file = get_application_path() + "/public/location_to_bboxes.ini" + log.debug("bboxes_file is: '%s'" % bboxes_file) + config.read(bboxes_file) + for section_name in config.sections(): + log.debug('Location to bboxes Section: ' + section_name) + # print ' Options:', parser.options(section_name) + for name, value in config.items(section_name): + location_to_bboxes[name] = value.replace(",", "%2C") + + ordDictBboxes = collections.OrderedDict(sorted(location_to_bboxes.items())) + log.debug("Ordered 'bboxes_file' dict: '%s'" % ordDictBboxes) + return ordDictBboxes + except Exception as error: + log.error("Error on reading file: %s" % bboxes_file + "error: %s" % error) + +def get_content_moderator_system_placeholder(): + return systemtype_cms_fields_placeholders + + +#ITemplateHelpers +def get_helpers(self): + log.info("get_helpers called...") + '''Register functions as a template + helper function. + ''' + # Template helper function names should begin with the name of the + # extension they belong to, to avoid clashing with functions from + # other extensions. return { - "d4science_hello": d4science_hello, - "get_deliverable_type": get_deliverable_type + 'd4science_theme_get_user_role_for_group_or_org': get_user_role_for_group_or_org, + 'd4science_theme_get_parents_for_group': get_parents_for_group, + 'get_header_param': get_header_param, + 'get_request_param': get_request_param, + 'get_cookie_value': get_cookie_value, + 'd4science_theme_markdown_extract_html' : markdown_extract_html, + 'd4science_theme_get_systemtype_value_from_extras' : get_systemtype_value_from_extras, + 'd4science_theme_get_systemtype_field_dict_from_session' : get_systemtype_field_dict_from_session, + 'd4science_theme_get_namespace_separator_from_session' : get_namespace_separator_from_session, + 'd4science_theme_get_extras' : get_extras, + 'd4science_theme_count_facet_items_dict' : count_facet_items_dict, + 'd4science_theme_purge_namespace_to_facet': purge_namespace_to_fieldname, + 'd4science_get_color_for_type': get_color_for_type, + 'd4science_get_d4s_namespace_controller': get_d4s_namespace_controller, + 'd4science_get_extras_indexed_for_namespaces': get_extras_indexed_for_namespaces, + 'd4science_get_namespaces_dict': get_namespaces_dict, + 'd4science_get_extra_for_category' : get_extra_for_category, + 'd4science_get_ordered_dictionary': ordered_dictionary, + 'd4science_get_qrcode_for_url': qrcode_for_url, + 'd4science_get_list_of_organizations': get_list_of_organizations, + 'd4science_get_image_display_for_group': get_image_display_for_group, + 'd4science_get_list_of_groups': get_list_of_groups, + 'd4science_get_browse_info_for_organisations_or_groups': get_browse_info_for_organisations_or_groups, + 'd4science_get_user_info': get_user_info, + 'd4science_get_url_to_icon_for_ckan_entity' : get_url_to_icon_for_ckan_entity, + 'd4science_get_ckan_translate_for' : get_ckan_translate_for, + 'd4science_get_location_to_bboxes' : get_location_to_bboxes, + 'd4science_get_content_moderator_system_placeholder': get_content_moderator_system_placeholder, } -def get_deliverable_type(): - return "This is a custom deliverable type!" + diff --git a/ckanext/d4science/plugin.py b/ckanext/d4science/plugin.py index ed79b52..80cf3f0 100644 --- a/ckanext/d4science/plugin.py +++ b/ckanext/d4science/plugin.py @@ -1,10 +1,203 @@ import ckan.plugins as plugins import ckan.plugins.toolkit as toolkit +import ckan.lib.dictization.model_save as model_save +import sqlalchemy as sa from ckan.views.resource import Blueprint from ckanext.d4science import helpers from ckanext.d4science.logic import action, auth from ckanext.d4science.logic import validators +from ckan.config.middleware.common_middleware import TrackingMiddleware from ckan.model import Package +from logging import getLogger + +from ckan.common import ( + g +) + +d4s_ctg_namespaces_controller = None + +log = getLogger(__name__) + +def remove_check_replicated_custom_key(schema): + if schema is not None: + schema.pop('__before', None) + + return schema + +def _package_extras_save(extra_dicts, obj, context): + ''' It can save repeated extras as key-value ''' + 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"] + + log.debug("extra_dicts: "+unicode(str(extra_dicts)).encode('utf-8')) + #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) + + #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: "+unicode(extra_dict["key"]).encode('utf-8')) + #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"]) + + log.debug("new_extras: "+unicode(str(new_extras)).encode('utf-8')) + #print "new_extras: "+str(new_extras) + + #new + for key in set(new_extras.keys()) - set(old_extras.keys()): + state = 'active' + log.debug("adding key: "+unicode(key).encode('utf-8')) + #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) + + #deleted + for key in set(old_extras.keys()) - set(new_extras.keys()): + log.debug("deleting key: "+unicode(key).encode('utf-8')) + #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: "+unicode(value).encode('utf-8') + ", new_occur: "+unicode(new_occur).encode('utf-8')+ ", old_occur: "+unicode(old_occur).encode('utf-8')) + #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: "+unicode(value).encode('utf-8') +", 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) + for extra in extra_values: + state = 'active' + extra.state = state + session.add(extra) + #print "extra updated: "+str(extra) + log.debug("extra updated: "+unicode(extra).encode('utf-8')) + + 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: "+unicode(value).encode('utf-8') +", is present into new list, adding it to old list") + 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: "+unicode(old_extras[key]).encode('utf-8')) + #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: "+unicode(value).encode('utf-8')+", is not present into new list, removing "+unicode(countDelete).encode('utf-8')+" 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: "+unicode(extra.value).encode('utf-8')) + #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: "+unicode(extra.value).encode('utf-8')) + state = 'active' + extra.state = state + session.add(extra) + + else: + #print "extra new value: "+str(value) + log.debug("extra new value: "+unicode(value).encode('utf-8')) + 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: "+unicode(extra).encode('utf-8')) + state = 'deleted' + extra.state = state + +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 + + +#OVERRIDING BASE SQL ALCHEMY ENGINE INSTANCE +#gestisce le connessioni al db relazionale utilizzato da ckan +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) # import ckanext.d4science.cli as cli @@ -56,28 +249,45 @@ class D4SciencePlugin(plugins.SingletonPlugin): def is_fallback(self): # Indica che questo plugin può essere usato come fallback se un tipo specifico non è specificato return False + + #IDatasetForm + def package_types(self): + # This plugin doesn't handle any special package types, it just + # registers itself as the default (above). + return [] def create_package_schema(self): schema = super(D4SciencePlugin, self).create_package_schema() - schema.update({ - 'deliverable_type': [ignore_missing, unicode], - }) + schema = remove_check_replicated_custom_key(schema) + #schema.update({ + # 'deliverable_type': [ignore_missing, unicode], + #}) return schema def update_package_schema(self): schema = super(D4SciencePlugin, self).update_package_schema() - schema.update({ - 'deliverable_type': [ignore_missing, unicode], - }) + schema = remove_check_replicated_custom_key(schema) + #schema.update({ + # 'deliverable_type': [ignore_missing, unicode], + #}) return schema def show_package_schema(self): schema = super(D4SciencePlugin, self).show_package_schema() - schema.update({ - 'deliverable_type': [ignore_missing, unicode], - }) + schema = remove_check_replicated_custom_key(schema) + #schema.update({ + # 'deliverable_type': [ignore_missing, unicode], + #}) return schema + #override + model_save.package_extras_save = _package_extras_save + + #OVERRIDING BASE SQL ALCHEMY ENGINE INSTANCE + TrackingMiddleware.__init__ = _init_TrackingMiddleware + + global d4s_ctg_namespaces_controller + # IBlueprint # def get_blueprint(self): From ab3693873c4953ab78b273eae0031af448b4c188 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Fri, 11 Oct 2024 10:55:46 +0200 Subject: [PATCH 05/27] generate qr code --- ckanext/d4science/controllers/__init__.py | 4 + ckanext/d4science/d4sdiscovery/__init__.py | 0 ckanext/d4science/qrcodelink/__init__.py | 0 .../d4science/qrcodelink/generate_qrcode.py | 76 +++++++++++++++++++ 4 files changed, 80 insertions(+) create mode 100644 ckanext/d4science/controllers/__init__.py create mode 100644 ckanext/d4science/d4sdiscovery/__init__.py create mode 100644 ckanext/d4science/qrcodelink/__init__.py create mode 100644 ckanext/d4science/qrcodelink/generate_qrcode.py diff --git a/ckanext/d4science/controllers/__init__.py b/ckanext/d4science/controllers/__init__.py new file mode 100644 index 0000000..68f2e01 --- /dev/null +++ b/ckanext/d4science/controllers/__init__.py @@ -0,0 +1,4 @@ +#The __init__.py files are required to make Python +#treat the directories as containing packages; this is done to prevent directories with a common name, such as string, from unintentionally hiding valid modules that occur later on the module search path. +#See: https://docs.python.org/3/tutorial/modules.html#packages + diff --git a/ckanext/d4science/d4sdiscovery/__init__.py b/ckanext/d4science/d4sdiscovery/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ckanext/d4science/qrcodelink/__init__.py b/ckanext/d4science/qrcodelink/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ckanext/d4science/qrcodelink/generate_qrcode.py b/ckanext/d4science/qrcodelink/generate_qrcode.py new file mode 100644 index 0000000..24cf3a3 --- /dev/null +++ b/ckanext/d4science/qrcodelink/generate_qrcode.py @@ -0,0 +1,76 @@ +import os +import tempfile +import logging +import pyqrcode +import time + +CATALINA_HOME = 'CATALINA_HOME' +log = logging.getLogger(__name__) + +temp_dir = None +qr_code_dir = None +qr_code_dir_name = "qr_code_for_catalogue" + + +class D4S_QrCode(): + def __init__(self, qr_code_url=None): + self._qr_code_url = qr_code_url + global temp_dir + if temp_dir is None: + D4S_QrCode.init_temp_dir() + if temp_dir is None: + raise Exception('No temp directory found!') + + def get_qrcode_path(self): + image_name = self._qr_code_url.rsplit('/', 1)[-1] + image_name+=".svg" + image_path = os.path.join(qr_code_dir, image_name) + # ONLY IF QRCODE DOES NOT EXIST THEN IT WILL BE CREATED + if not os.path.isfile(image_path): + url = pyqrcode.create(self._qr_code_url) + url.svg(image_path, scale=3) + log.debug("Created QRCode image: " + image_name) + + attempt = 0 + while not os.path.exists(image_path) and attempt < 3: + time.sleep(1) + attempt += 1 + + if os.path.isfile(image_path): + log.info("QRcode image exists at: " + image_path) + else: + log.error("%s isn't a file!" % image_path) + + return image_path + + @classmethod + def init_temp_dir(cls): + global temp_dir + global qr_code_dir_name + global qr_code_dir + try: + temp_dir = str(os.environ[CATALINA_HOME]) + temp_dir = os.path.join(temp_dir, "temp") + except KeyError as error: + log.error("No environment variable for: %s" % CATALINA_HOME) + + if temp_dir is None: + temp_dir = tempfile.gettempdir() # using system tmp dir + + log.debug("Temp dir is: %s" % temp_dir) + + qr_code_dir = os.path.join(temp_dir, qr_code_dir_name) + + if not os.path.exists(qr_code_dir): + os.makedirs(qr_code_dir) + + def get_temp_directory(self): + return temp_dir + + def get_qr_code_dir(self): + return qr_code_dir + + +# D4S_QrCode.init_temp_dir() +#qr_code = D4S_QrCode("http://data.d4science.org/ctlg/BiodiversityLab/distribution_of_the_giant_squid_architeuthis") +#print qr_code.get_qrcode_path() From b6c9e8399ffad9215f5ee37ac9b6606134186e57 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Fri, 11 Oct 2024 10:56:02 +0200 Subject: [PATCH 06/27] add controllers --- ckanext/d4science/controllers/home.py | 71 +++++++++ ckanext/d4science/controllers/organization.py | 135 ++++++++++++++++++ ckanext/d4science/controllers/systemtype.py | 88 ++++++++++++ .../d4sdiscovery/d4s_cache_controller.py | 106 ++++++++++++++ ckanext/d4science/d4sdiscovery/d4s_extras.py | 31 ++++ .../d4science/d4sdiscovery/d4s_namespaces.py | 39 +++++ .../d4sdiscovery/d4s_namespaces_controller.py | 130 +++++++++++++++++ .../d4s_namespaces_extras_util.py | 89 ++++++++++++ .../d4sdiscovery/icproxycontroller.py | 110 ++++++++++++++ 9 files changed, 799 insertions(+) create mode 100644 ckanext/d4science/controllers/home.py create mode 100644 ckanext/d4science/controllers/organization.py create mode 100644 ckanext/d4science/controllers/systemtype.py create mode 100644 ckanext/d4science/d4sdiscovery/d4s_cache_controller.py create mode 100644 ckanext/d4science/d4sdiscovery/d4s_extras.py create mode 100644 ckanext/d4science/d4sdiscovery/d4s_namespaces.py create mode 100644 ckanext/d4science/d4sdiscovery/d4s_namespaces_controller.py create mode 100644 ckanext/d4science/d4sdiscovery/d4s_namespaces_extras_util.py create mode 100644 ckanext/d4science/d4sdiscovery/icproxycontroller.py diff --git a/ckanext/d4science/controllers/home.py b/ckanext/d4science/controllers/home.py new file mode 100644 index 0000000..f91cbfa --- /dev/null +++ b/ckanext/d4science/controllers/home.py @@ -0,0 +1,71 @@ +import logging +from ckan.controllers.home import HomeController +import ckan.plugins as p +from ckan.common import OrderedDict, _, g, c +import ckan.lib.search as search +import ckan.model as model +import ckan.logic as logic +import ckan.lib.maintain as maintain +import ckan.lib.base as base +import ckan.lib.helpers as h + +class d4SHomeController(): + + #Overriding controllers.HomeController.index method + def index(self): + try: + # package search + context = {'model': model, 'session': model.Session,'user': c.user, 'auth_user_obj': c.userobj} + + facets = OrderedDict() + + default_facet_titles = { + 'organization': _('Organizations'), + 'groups': _('Groups'), + 'tags': _('Tags'), + 'res_format': _('Formats'), + 'license_id': _('Licenses'), + } + + for facet in g.facets: + if facet in default_facet_titles: + facets[facet] = default_facet_titles[facet] + else: + facets[facet] = facet + + # Facet titles + for plugin in p.PluginImplementations(p.IFacets): + facets = plugin.dataset_facets(facets, 'dataset') + + c.facet_titles = facets + + data_dict = { + 'q': '*:*', + 'facet.field': list(facets.keys()), + 'rows': 4, + 'start': 0, + 'sort': 'views_recent desc', + 'fq': 'capacity:"public"' + } + query = logic.get_action('package_search')(context, data_dict) + c.search_facets = query['search_facets'] + c.package_count = query['count'] + c.datasets = query['results'] + + #print "c.search_facets: " + #print " ".join(c.search_facets) + + except search.SearchError: + c.package_count = 0 + + if c.userobj and not c.userobj.email: + url = h.url_for(controller='user', action='edit') + msg = _('Please update your profile' + ' and add your email address. ') % url + \ + _('%s uses your email address' + ' if you need to reset your password.') \ + % g.site_title + h.flash_notice(msg, allow_html=True) + + return base.render('home/index.html', cache_force=True) + diff --git a/ckanext/d4science/controllers/organization.py b/ckanext/d4science/controllers/organization.py new file mode 100644 index 0000000..c453872 --- /dev/null +++ b/ckanext/d4science/controllers/organization.py @@ -0,0 +1,135 @@ +# encoding: utf-8 + +import re + +import ckan.controllers.group as group +import ckan.plugins as plugins +import logging +import datetime +from urllib.parse import urlencode + +from pylons.i18n import get_lang + +import ckan.lib.base as base +import ckan.lib.helpers as h +import ckan.lib.maintain as maintain +import ckan.lib.navl.dictization_functions as dict_fns +import ckan.logic as logic +import ckan.lib.search as search +import ckan.model as model +import ckan.authz as authz +import ckan.lib.plugins +import ckan.plugins as plugins +from ckan.common import OrderedDict, c, g, request, _ + + +''' +Created by Francesco Mangiacrapa, see: #8964 +''' +class OrganizationVREController(group.GroupController): + ''' The organization controller is for Organizations, which are implemented + as Groups with is_organization=True and group_type='organization'. It works + the same as the group controller apart from: + * templates and logic action/auth functions are sometimes customized + (switched using _replace_group_org) + * 'bulk_process' action only works for organizations + + Nearly all the code for both is in the GroupController (for historical + reasons). + ''' + + group_types = ['organization'] + + def _guess_group_type(self, expecting_name=False): + return 'organization' + + def _replace_group_org(self, string): + ''' substitute organization for group if this is an org''' + return re.sub('^group', 'organization', string) + + def _update_facet_titles(self, facets, group_type): + for plugin in plugins.PluginImplementations(plugins.IFacets): + facets = plugin.organization_facets( + facets, group_type, None) + + def index(self): + group_type = self._guess_group_type() + + page = h.get_page_number(request.params) or 1 + items_per_page = 21 + + context = {'model': model, 'session': model.Session, + 'user': c.user, 'for_view': True, + 'with_private': False} + + q = c.q = request.params.get('q', '') + sort_by = c.sort_by_selected = request.params.get('sort') + try: + self._check_access('site_read', context) + self._check_access('group_list', context) + except NotAuthorized: + abort(403, _('Not authorized to see this page')) + + # pass user info to context as needed to view private datasets of + # orgs correctly + if c.userobj: + context['user_id'] = c.userobj.id + context['user_is_admin'] = c.userobj.sysadmin + + data_dict_global_results = { + 'all_fields': False, + 'q': q, + 'sort': sort_by, + 'type': group_type or 'group', + } + global_results = self._action('group_list')(context, + data_dict_global_results) + + data_dict_page_results = { + 'all_fields': True, + 'q': q, + 'sort': sort_by, + 'type': group_type or 'group', + 'limit': items_per_page, + 'offset': items_per_page * (page - 1), + } + page_results = self._action('group_list')(context, + data_dict_page_results) + + c.page = h.Page( + collection=global_results, + page=page, + url=h.pager_url, + items_per_page=items_per_page, + ) + + c.page.items = page_results + return base.render('organization_vre/index.html', + extra_vars={'group_type': group_type}) + + + def read(self, id, limit=20): + group_type = self._ensure_controller_matches_group_type( + id.split('@')[0]) + + context = {'model': model, 'session': model.Session, + 'user': c.user, + 'schema': self._db_to_form_schema(group_type=group_type), + 'for_view': True} + data_dict = {'id': id, 'type': group_type} + + # unicode format (decoded from utf8) + c.q = request.params.get('q', '') + + try: + # Do not query for the group datasets when dictizing, as they will + # be ignored and get requested on the controller anyway + data_dict['include_datasets'] = False + c.group_dict = self._action('group_show')(context, data_dict) + c.group = context['group'] + except (NotFound, NotAuthorized): + abort(404, _('Group not found')) + + self._read(id, limit, group_type) + return base.render('organization_vre/read.html', + extra_vars={'group_type': group_type}) \ No newline at end of file diff --git a/ckanext/d4science/controllers/systemtype.py b/ckanext/d4science/controllers/systemtype.py new file mode 100644 index 0000000..02a38f5 --- /dev/null +++ b/ckanext/d4science/controllers/systemtype.py @@ -0,0 +1,88 @@ +import logging +import ckan.plugins as p +from ckan.common import OrderedDict, _, g, c +import ckan.lib.search as search +import ckan.model as model +import ckan.logic as logic +import ckan.lib.maintain as maintain +import ckan.lib.base as base +import ckan.lib.helpers as h + +from urllib.parse import urlencode + +#from pylons.i18n import get_lang +from flask import Blueprint, render_template, g, request + +import ckan.lib.base as base +import ckan.lib.navl.dictization_functions as dict_fns +import ckan.authz as authz + + +class d4STypeController(base.BaseController): + + #Overriding controllers.HomeController.index method + def index(self): + try: + # package search + context = {'model': model, 'session': model.Session,'user': g.user, 'auth_user_obj': g.userobj} + + facets = OrderedDict() + + default_facet_titles = { + 'organization': _('Organizations'), + 'groups': _('Groups'), + 'tags': _('Tags'), + 'res_format': _('Formats'), + 'license_id': _('Licenses'), + } + + for facet in g.facets: + if facet in default_facet_titles: + facets[facet] = default_facet_titles[facet] + else: + facets[facet] = facet + + # Facet titles + for plugin in p.PluginImplementations(p.IFacets): + facets = plugin.dataset_facets(facets, 'dataset') + + g.facet_titles = facets + + data_dict = { + 'q': '*:*', + 'facet.field': list(facets.keys()), + 'rows': 4, + 'start': 0, + 'sort': 'views_recent desc', + 'fq': 'capacity:"public"' + } + query = logic.get_action('package_search')(context, data_dict) + g.search_facets = query['search_facets'] + g.package_count = query['count'] + g.datasets = query['results'] + + #print "c.search_facets: " + #print " ".join(c.search_facets) + + except search.SearchError: + g.package_count = 0 + + if g.userobj and not g.userobj.email: + #url = h.url_for(controller='user', action='edit') pylons + url = h.url_for('user.edit') + msg = _('Please update your profile' + ' and add your email address. ') % url + \ + _('%s uses your email address' + ' if you need to reset your password.') \ + % g.site_title + h.flash_notice(msg, allow_html=True) + + #return base.render('type/index.html', cache_force=True) pylons + return render_template('type/index.html', cache_force=True) + +d4s_type_blueprint = Blueprint('d4s_type', __name__) + +@d4s_type_blueprint.route('/') +def index(): + controller = d4STypeController() + return controller.index() diff --git a/ckanext/d4science/d4sdiscovery/d4s_cache_controller.py b/ckanext/d4science/d4sdiscovery/d4s_cache_controller.py new file mode 100644 index 0000000..0462d7d --- /dev/null +++ b/ckanext/d4science/d4sdiscovery/d4s_cache_controller.py @@ -0,0 +1,106 @@ +import datetime +import logging +import os +import tempfile +import csv + +from .icproxycontroller import NAMESPACE_ID_LABEL + +log = logging.getLogger(__name__) + +CATALINA_HOME = 'CATALINA_HOME' +temp_dir = None +namespaces_dir = None +NAMESPACES_DIR_NAME = "namespaces_for_catalogue" +NAMESPACES_CACHE_FILENAME = "Namespaces_Catalogue_Categories.csv" + +# Created by Francesco Mangiacrapa +# francesco.mangiacrapa@isti.cnr.it +# ISTI-CNR Pisa (ITALY) + + +# D4S_Cache_Controller +class D4S_Cache_Controller(): + namespaces_cache_path = None + __scheduler = None + + def __init__(self): + """ Virtually private constructor. """ + log.debug("__init__ D4S_Cache_Controller") + self._check_cache() + + def _check_cache(self): + + if self.namespaces_cache_path is None: + self.init_temp_dir() + self.namespaces_cache_path = os.path.join(namespaces_dir, NAMESPACES_CACHE_FILENAME) + log.info("The namespaces cache is located at: %s" % self.namespaces_cache_path) + + if not os.path.exists(self.namespaces_cache_path): + log.info("File does not exists creating it") + try: + with open(self.namespaces_cache_path, mode='w') as namespaces_file: + csv.writer(namespaces_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) + log.info("Cache created at %s" % self.namespaces_cache_path) + except Exception as e: + print(e) + + ''' Write the list of dictionary with namespaces''' + def write_namespaces(self, namespace_list_of_dict): + # Insert Data + with open(self.namespaces_cache_path, 'w') as namespaces_file: + writer = csv.writer(namespaces_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) + writer.writerow([NAMESPACE_ID_LABEL, 'name', 'title', 'description']) + for namespace_dict in namespace_list_of_dict: + #print("namespace %s" % namespace_dict) + writer.writerow([namespace_dict[NAMESPACE_ID_LABEL], namespace_dict['name'], namespace_dict['title'], namespace_dict['description']]) + + log.info("Inserted %d namespaces in the Cache" % len(namespace_list_of_dict)) + + '''Returns the list of dictionary with namespaces''' + def read_namespaces(self): + # Read Data + namespace_list_of_dict = [] + try: + with open(self.namespaces_cache_path, 'r') as namespaces_file: + reader = csv.DictReader(namespaces_file) + for row in reader: + #print("read namespace %s" % row) + namespace_list_of_dict.append(dict(row)) + + log.debug("from Cache returning namespace_list_of_dict %s: " % namespace_list_of_dict) + log.info("from Cache read namespace_list_of_dict with %d item/s " % len(namespace_list_of_dict)) + return namespace_list_of_dict + except Exception as e: + print(e) + + log.info("no namespace in the Cache returning empty list of dict") + return namespace_list_of_dict + + @property + def get_namespaces_cache_path(self): + return self.namespaces_cache_path + + @classmethod + def init_temp_dir(cls): + global temp_dir + global NAMESPACES_DIR_NAME + global namespaces_dir + try: + temp_dir = str(os.environ[CATALINA_HOME]) + temp_dir = os.path.join(temp_dir, "temp") + except KeyError as error: + log.error("No environment variable for: %s" % CATALINA_HOME) + + if temp_dir is None: + temp_dir = tempfile.gettempdir() # using system tmp dir + + log.debug("Temp dir is: %s" % temp_dir) + + namespaces_dir = os.path.join(temp_dir, NAMESPACES_DIR_NAME) + + if not os.path.exists(namespaces_dir): + os.makedirs(namespaces_dir) + + + diff --git a/ckanext/d4science/d4sdiscovery/d4s_extras.py b/ckanext/d4science/d4sdiscovery/d4s_extras.py new file mode 100644 index 0000000..af3e9ce --- /dev/null +++ b/ckanext/d4science/d4sdiscovery/d4s_extras.py @@ -0,0 +1,31 @@ +# Created by Francesco Mangiacrapa +# francesco.mangiacrapa@isti.cnr.it +# ISTI-CNR Pisa (ITALY) + +## questo file va bene anche in p3 ## +import logging +log = logging.getLogger(__name__) + +class D4S_Extras(): + + def __init__(self, category_dict={}, extras=[]): + self._category = category_dict + self._extras = extras + + def append_extra(self, k, v): + #print ("self._extras: %s" %self._extras) + if k is not None: + self._extras.append({k:v}) + + @property + def category(self): + return self._category + + @property + def extras(self): + return self._extras + + def __repr__(self): + return 'category: %s'%self._category+' ' \ + 'extras: %s'%self._extras + diff --git a/ckanext/d4science/d4sdiscovery/d4s_namespaces.py b/ckanext/d4science/d4sdiscovery/d4s_namespaces.py new file mode 100644 index 0000000..54f1e90 --- /dev/null +++ b/ckanext/d4science/d4sdiscovery/d4s_namespaces.py @@ -0,0 +1,39 @@ +# Created by Francesco Mangiacrapa +# francesco.mangiacrapa@isti.cnr.it +# ISTI-CNR Pisa (ITALY) + +#OrderedDict([(u'@id', u'extra_information'), (u'name', u'Extra Information'), (u'title', u'Extras'), (u'description', u'This section is about Extra(s)')]), u'contact': OrderedDict([(u'@id', u'contact'), (u'name', u'Contact'), (u'title', u'Contact Title'), (u'description', u'This section is about Contact(s)')]), u'developer_information': OrderedDict([(u'@id', u'developer_information'), (u'name', u'Developer'), (u'title', u'Developer Information'), (u'description', u'This section is about Developer(s)')])} + +import logging +log = logging.getLogger(__name__) + +class D4S_Namespaces(): + + def __init__(self, id=None, name=None, title=None, description=None): + self._id = id + self._name = name + self._title = title + self._description = description + + @property + def id(self): + return self._id + + @property + def name(self): + return self._name + + @property + def title(self): + return self._title + + + @property + def description(self): + return self._description + + def __repr__(self): + return '{id: %s'%self.id+', ' \ + 'name: %s'%self.name+ ', ' \ + 'title: %s'%self.title+ ', ' \ + 'description: %s'%self.description+ '}' diff --git a/ckanext/d4science/d4sdiscovery/d4s_namespaces_controller.py b/ckanext/d4science/d4sdiscovery/d4s_namespaces_controller.py new file mode 100644 index 0000000..afb88df --- /dev/null +++ b/ckanext/d4science/d4sdiscovery/d4s_namespaces_controller.py @@ -0,0 +1,130 @@ +import logging +import time + +from .d4s_cache_controller import D4S_Cache_Controller +from .icproxycontroller import D4S_IS_DiscoveryCatalogueNamespaces +from threading import Event, Thread + +CATEGORY = 'category' +NOCATEOGORY = 'nocategory' + +log = logging.getLogger(__name__) + +cancel_future_calls = None + +# Refreshing time for namespaces cache in secs. +NAMESPACES_CACHE_REFRESHING_TIME = 60 * 60 + + +# Funtion to call repeatedly another function +def call_repeatedly(interval, func, *args): + log.info("call_repeatedly called on func '{}' with interval {} sec".format(func.__name__, interval)) + stopped = Event() + + def loop(): + while not stopped.wait(interval): # the first call is in `interval` secs + func(*args) + + th = Thread(name='daemon_caching_namespaces', target=loop) + th.setDaemon(True) + th.start() + return stopped.set + + +def reload_namespaces_from_IS(urlICProxy, resourceID, gcubeToken): + log.info("_reload_namespaces_from_IS called") + try: + discovery_ctg_namespaces = D4S_IS_DiscoveryCatalogueNamespaces(urlICProxy, resourceID, gcubeToken) + namespaces_list_of_dict = discovery_ctg_namespaces.getNamespacesDictFromResource() + + if namespaces_list_of_dict is not None and len(namespaces_list_of_dict) > 0: + log.debug("namespaces read from IS are: %s" % namespaces_list_of_dict) + D4S_Cache_Controller().write_namespaces(namespaces_list_of_dict) + else: + log.info("namespaces list read from IS is empty. Skipping caching update") + + except Exception as e: + print("Error occurred on reading namespaces from IS and refilling the cache!") + print(e) + + +# Created by Francesco Mangiacrapa +# francesco.mangiacrapa@isti.cnr.it +# ISTI-CNR Pisa (ITALY) + + +# D4S_IS_DiscoveryCatalogueNamespacesController is used to discovery namespaces for Catalogue Categories (implemented as a Singleton) +# @param: urlICProxy is the URI of IC proxy rest-full service provided by IS +# @param: resourceID is the resource ID of the Generic Resource: "Namespaces Catalogue Categories" +# @param: gcubeToken the gcube token used to contact the IC proxy +class D4S_Namespaces_Controller(): + __instance = None + + @staticmethod + def getInstance(): + """ Static access method. """ + if D4S_Namespaces_Controller.__instance is None: + D4S_Namespaces_Controller() + + return D4S_Namespaces_Controller.__instance + + def __init__(self): + """ Virtually private constructor. """ + log.debug("__init__ D4S_Namespaces_Controller") + + if D4S_Namespaces_Controller.__instance is not None: + raise Exception("This class is a singleton!") + else: + D4S_Namespaces_Controller.__instance = self + + self._d4s_cache_controller = D4S_Cache_Controller() + self._urlICProxy = None + self._resourceID = None + self._gcubeToken = None + + def load_namespaces(self, urlICProxy, resourceID, gcubeToken): + log.debug("readNamespaces called") + self._urlICProxy = urlICProxy + self._resourceID = resourceID + self._gcubeToken = gcubeToken + return self._check_namespaces() + + def _read_namespaces(self): + return self._d4s_cache_controller.read_namespaces() + + def _check_namespaces(self): + log.debug("_check_namespaces called") + + if self._d4s_cache_controller is None: + self._d4s_cache_controller = D4S_Cache_Controller() + + namespace_list = self._read_namespaces() + + # when the Cache is empty + if namespace_list is None or not namespace_list: + # reading namespaces from IS and filling the DB + log.info("The Cache is empty. Reading the namespace from IS and filling the Cache") + reload_namespaces_from_IS(self._urlICProxy, self._resourceID, self._gcubeToken) + # reloading the namespaces from the cache + namespace_list = self._read_namespaces() + + # starting Thread daemon for refreshing the namespaces Cache + global cancel_future_calls + if cancel_future_calls is None: + cancel_future_calls = call_repeatedly(NAMESPACES_CACHE_REFRESHING_TIME, reload_namespaces_from_IS, + self._urlICProxy, + self._resourceID, + self._gcubeToken) + + return namespace_list + + def get_dict_ctg_namespaces(self): + log.debug("get_dict_ctg_namespaces called") + namespace_list_of_dict = self._check_namespaces() + return self.convert_namespaces_to_d4s_namespacedict(namespace_list_of_dict) + + # Private method + @staticmethod + def convert_namespaces_to_d4s_namespacedict(namespace_list_of_dict): + log.debug("convert_namespaces_to_d4s_namespacedict called on %s" % namespace_list_of_dict) + return D4S_IS_DiscoveryCatalogueNamespaces.to_namespaces_dict_index_for_id(namespace_list_of_dict) diff --git a/ckanext/d4science/d4sdiscovery/d4s_namespaces_extras_util.py b/ckanext/d4science/d4sdiscovery/d4s_namespaces_extras_util.py new file mode 100644 index 0000000..edb0bbf --- /dev/null +++ b/ckanext/d4science/d4sdiscovery/d4s_namespaces_extras_util.py @@ -0,0 +1,89 @@ +import logging +import collections +from .d4s_extras import D4S_Extras + +CATEGORY = 'category' +NOCATEOGORY = 'nocategory' + +log = logging.getLogger(__name__) + +# Created by Francesco Mangiacrapa +# francesco.mangiacrapa@isti.cnr.it +# ISTI-CNR Pisa (ITALY) + + +# D4S_Namespaces_Extra_Util is used to get the extra fields indexed for D4Science namespaces +# @param: namespace_dict is the namespace dict of D4Science namespaces (defined in the Generic Resource: "Namespaces Catalogue Categories") +# @param: extras is the dictionary of extra fields for a certain item +class D4S_Namespaces_Extra_Util(): + + def get_extras_indexed_for_namespaces(self, namespace_dict, extras): + extras_for_categories = collections.OrderedDict() + + # ADDING ALL EXTRAS WITH NAMESPACE + for namespaceid in list(namespace_dict.keys()): + dict_extras = None + nms = namespaceid + ":" + #has_namespace_ref = None + for key, value in extras: + k = key + v = value + # print "key: " + k + # print "value: " + v + if k.startswith(nms): + + if namespaceid not in extras_for_categories: + extras_for_categories[namespaceid] = collections.OrderedDict() + + dict_extras = extras_for_categories[namespaceid] + log.debug("dict_extras %s "%dict_extras) + + if (dict_extras is None) or (not dict_extras): + dict_extras = D4S_Extras(namespace_dict.get(namespaceid), []) + log.debug("dict_extras after init %s " % dict_extras) + + #print ("dict_extras after init %s " % dict_extras) + log.debug("replacing namespace into key %s " % k +" with empty string") + nms = namespaceid + ":" + k = k.replace(nms, "") + dict_extras.append_extra(k, v) + extras_for_categories[namespaceid] = dict_extras + log.debug("adding d4s_extra: %s " % dict_extras+ " - to namespace id: %s" %namespaceid) + #has_namespace_ref = True + #break + + #ADDING ALL EXTRAS WITHOUT NAMESPACE + for key, value in extras: + k = key + v = value + + has_namespace_ref = None + for namespaceid in list(namespace_dict.keys()): + nms = namespaceid + ":" + #IF KEY NOT STARTING WITH NAMESPACE + if k.startswith(nms): + has_namespace_ref = True + log.debug("key: %s " % k + " - have namespace: %s" % nms) + break + + if has_namespace_ref is None: + log.debug("key: %s " % k + " - have not namespace") + if NOCATEOGORY not in extras_for_categories: + extras_for_categories[NOCATEOGORY] = collections.OrderedDict() + + dict_extras_no_cat = extras_for_categories[NOCATEOGORY] + #print ("dict_extras_no_cat %s " % dict_extras_no_cat) + + if (dict_extras_no_cat is None) or (not dict_extras_no_cat): + dict_extras_no_cat = D4S_Extras(NOCATEOGORY, []) + + #print ("adding key: %s "%k+" - value: %s"%v) + log.debug("NOCATEOGORY adding key: %s " % k + " - value: %s" % v) + + dict_extras_no_cat.append_extra(k, v) + log.debug("dict_extras_no_cat %s " % dict_extras_no_cat) + extras_for_categories[NOCATEOGORY] = dict_extras_no_cat + log.debug("extras_for_categories NOCATEOGORY %s " % extras_for_categories) + + return extras_for_categories + diff --git a/ckanext/d4science/d4sdiscovery/icproxycontroller.py b/ckanext/d4science/d4sdiscovery/icproxycontroller.py new file mode 100644 index 0000000..f1c03ce --- /dev/null +++ b/ckanext/d4science/d4sdiscovery/icproxycontroller.py @@ -0,0 +1,110 @@ +import logging +import urllib.request, urllib.error, urllib.parse +from lxml import etree + +import xmltodict +import collections + +from .d4s_namespaces import D4S_Namespaces + +XPATH_NAMESPACES = "/Resource/Profile/Body/namespaces" +gcubeTokenParam = "gcube-token" +NAMESPACE_ID_LABEL = '@id' + +log = logging.getLogger(__name__) + + +# Created by Francesco Mangiacrapa +# francesco.mangiacrapa@isti.cnr.it +# ISTI-CNR Pisa (ITALY) + +def getResponseBody(uri): + req = urllib.request.Request(uri) + try: + resp = urllib.request.urlopen(req, timeout=20) + except urllib.error.HTTPError as e: + log.error("Error on contacting URI: %s" % uri) + log.error("HTTPError: %d" % e.code) + return None + except urllib.error.URLError as e: + # Not an HTTP-specific error (e.g. connection refused) + log.error("URLError - Input URI: %s " % uri + " is not valid!!") + return None + else: + # 200 + body = resp.read() + return body + + +# D4S_IS_DiscoveryCatalogueNamespaces is used to discovery namespaces for Catalogue Categories. +# @param: urlICProxy is the URI of IC proxy rest-full service provided by IS +# @param: resourceID is the resource ID of the Generic Resource: "Namespaces Catalogue Categories" +# @param: gcubeToken the gcube token used to contact the IC proxy +class D4S_IS_DiscoveryCatalogueNamespaces(): + + def __init__(self, urlICProxy, resourceID, gcubeToken): + self.urlICProxy = urlICProxy + self.resourceID = resourceID + self.gcubeToken = gcubeToken + + def getNamespacesDictFromResource(self): + + doc = {} + namespace_list = [] + + try: + # print("proxy: "+self.urlICProxy) + # print("resourceID: " + self.resourceID) + # print("gcubeTokenParam: " + gcubeTokenParam) + # print("gcubeToken: " + self.gcubeToken) + + uri = self.urlICProxy + "/" + self.resourceID + "?" + gcubeTokenParam + "=" + self.gcubeToken + log.info("Contacting URL: %s" % uri) + theResource = getResponseBody(uri) + log.debug("Resource returned %s " % theResource) + theResourceXML = etree.XML(theResource) + theNamespaces = theResourceXML.xpath(XPATH_NAMESPACES) + log.debug("The body %s" % etree.tostring(theNamespaces[0])) + + if theNamespaces is not None and theNamespaces[0] is not None: + bodyToString = etree.tostring(theNamespaces[0]) + doc = xmltodict.parse(bodyToString) + else: + log.warn("No Namespace for Catalogue Categories found, returning None") + except Exception as inst: + log.error("Error on getting catalogue namespaces: " + str(inst)) + log.info("Returning empty list of namespaces") + return namespace_list + + log.debug("IS namespaces resource to dict is: %s" % doc) + + + if ('namespaces' in doc): + # log.debug('Namespaces obj %s:' % doc['namespaces']) + namespaces = doc['namespaces'] + if doc is not None and 'namespace' in namespaces: + namespace_list = namespaces['namespace'] + + log.info("Loaded %d namespaces from IS resource" % len(namespace_list)) + return namespace_list + + @staticmethod + def to_namespaces_dict_index_for_id(namespace_list): + namespace_dict = collections.OrderedDict() + log.debug("namespaces to dict: %s" % namespace_list) + try: + if namespace_list is not None and len(namespace_list) > 0: + for namespace in namespace_list: + try: + if NAMESPACE_ID_LABEL in namespace: + namespace_dict[namespace[NAMESPACE_ID_LABEL]] = D4S_Namespaces( + namespace[NAMESPACE_ID_LABEL], + namespace['name'], + namespace['title'], + namespace['description']) + except Exception as inst: + log.error("Error on converting catalogue namespaces: " + str(inst)) + except Exception as inst: + log.error("Error on checking namespace_list: " + str(inst)) + # print "namespace_dict to Nam: %s"%namespace_dict + return namespace_dict From e37a17838d7aba8210473589f0b01d3dfd492736 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Fri, 11 Oct 2024 10:56:13 +0200 Subject: [PATCH 07/27] add test html template --- ckanext/d4science/templates/footer.html | 1 + 1 file changed, 1 insertion(+) diff --git a/ckanext/d4science/templates/footer.html b/ckanext/d4science/templates/footer.html index c9623d0..2a91e5a 100644 --- a/ckanext/d4science/templates/footer.html +++ b/ckanext/d4science/templates/footer.html @@ -2,6 +2,7 @@ {% block d4science_footer_links %} + {{ super() }}

    Footer test!

    test From ec62dc0c7619b996e8e00558af2e517ba8cba3a5 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Fri, 11 Oct 2024 16:00:20 +0200 Subject: [PATCH 08/27] use blueprint instead of controllers --- ckanext/d4science/controllers/home.py | 113 ++++----- ckanext/d4science/controllers/organization.py | 225 ++++++++---------- ckanext/d4science/controllers/systemtype.py | 139 +++++------ .../d4sdiscovery/d4s_cache_controller.py | 4 - ckanext/d4science/d4sdiscovery/d4s_extras.py | 5 - .../d4sdiscovery/d4s_namespaces_controller.py | 9 +- .../d4s_namespaces_extras_util.py | 9 +- .../d4sdiscovery/icproxycontroller.py | 15 +- ckanext/d4science/plugin.py | 103 ++++---- 9 files changed, 287 insertions(+), 335 deletions(-) diff --git a/ckanext/d4science/controllers/home.py b/ckanext/d4science/controllers/home.py index f91cbfa..369b277 100644 --- a/ckanext/d4science/controllers/home.py +++ b/ckanext/d4science/controllers/home.py @@ -1,5 +1,6 @@ -import logging -from ckan.controllers.home import HomeController +#import logging +#from ckan.controllers.home import HomeController +from flask import Blueprint, render_template, g import ckan.plugins as p from ckan.common import OrderedDict, _, g, c import ckan.lib.search as search @@ -9,63 +10,65 @@ import ckan.lib.maintain as maintain import ckan.lib.base as base import ckan.lib.helpers as h -class d4SHomeController(): +#blueprint definition +d4science_home = Blueprint("d4science_home", __name__) - #Overriding controllers.HomeController.index method - def index(self): - try: - # package search - context = {'model': model, 'session': model.Session,'user': c.user, 'auth_user_obj': c.userobj} +#@d4science_home.route("/catalog") +@d4science_home.route("/") +def index(): + try: + # package search + context = {'model': model, 'session': model.Session,'user': g.user, 'auth_user_obj': g.userobj} - facets = OrderedDict() + facets = OrderedDict() - default_facet_titles = { - 'organization': _('Organizations'), - 'groups': _('Groups'), - 'tags': _('Tags'), - 'res_format': _('Formats'), - 'license_id': _('Licenses'), - } - - for facet in g.facets: - if facet in default_facet_titles: - facets[facet] = default_facet_titles[facet] - else: - facets[facet] = facet - - # Facet titles - for plugin in p.PluginImplementations(p.IFacets): - facets = plugin.dataset_facets(facets, 'dataset') - - c.facet_titles = facets - - data_dict = { - 'q': '*:*', - 'facet.field': list(facets.keys()), - 'rows': 4, - 'start': 0, - 'sort': 'views_recent desc', - 'fq': 'capacity:"public"' + default_facet_titles = { + 'organization': _('Organizations'), + 'groups': _('Groups'), + 'tags': _('Tags'), + 'res_format': _('Formats'), + 'license_id': _('Licenses'), } - query = logic.get_action('package_search')(context, data_dict) - c.search_facets = query['search_facets'] - c.package_count = query['count'] - c.datasets = query['results'] - - #print "c.search_facets: " - #print " ".join(c.search_facets) - - except search.SearchError: - c.package_count = 0 - if c.userobj and not c.userobj.email: - url = h.url_for(controller='user', action='edit') - msg = _('Please update your profile' - ' and add your email address. ') % url + \ - _('%s uses your email address' - ' if you need to reset your password.') \ - % g.site_title - h.flash_notice(msg, allow_html=True) + for facet in g.facets: + if facet in default_facet_titles: + facets[facet] = default_facet_titles[facet] + else: + facets[facet] = facet - return base.render('home/index.html', cache_force=True) + # gestion filtri/facets + for plugin in p.PluginImplementations(p.IFacets): + facets = plugin.dataset_facets(facets, 'dataset') + + g.facet_titles = facets + + data_dict = { + 'q': '*:*', + 'facet.field': list(facets.keys()), + 'rows': 4, + 'start': 0, + 'sort': 'views_recent desc', + 'fq': 'capacity:"public"' + } + query = logic.get_action('package_search')(context, data_dict) + g.search_facets = query['search_facets'] + g.package_count = query['count'] + g.datasets = query['results'] + + #print "c.search_facets: " + #print " ".join(c.search_facets) + + except search.SearchError: + g.package_count = 0 + + if g.userobj and not g.userobj.email: + url = h.url_for('user.edit') + msg = _('Please update your profile' + ' and add your email address. ') % url + \ + _('%s uses your email address' + ' if you need to reset your password.') \ + % g.site_title + h.flash_notice(msg, allow_html=True) + + return render_template('home/index.html', cache_force=True) diff --git a/ckanext/d4science/controllers/organization.py b/ckanext/d4science/controllers/organization.py index c453872..23f7837 100644 --- a/ckanext/d4science/controllers/organization.py +++ b/ckanext/d4science/controllers/organization.py @@ -1,135 +1,118 @@ # encoding: utf-8 - import re - -import ckan.controllers.group as group -import ckan.plugins as plugins import logging -import datetime -from urllib.parse import urlencode - -from pylons.i18n import get_lang - -import ckan.lib.base as base -import ckan.lib.helpers as h -import ckan.lib.maintain as maintain -import ckan.lib.navl.dictization_functions as dict_fns -import ckan.logic as logic -import ckan.lib.search as search -import ckan.model as model -import ckan.authz as authz -import ckan.lib.plugins +from flask import Blueprint, g, request, abort, render_template import ckan.plugins as plugins -from ckan.common import OrderedDict, c, g, request, _ +import ckan.logic as logic +import ckan.model as model +import ckan.lib.helpers as h +import ckan.lib.search as search +from ckan.common import OrderedDict, _, NotAuthorized, NotFound +organization_vre = Blueprint("organization_vre", __name__) +''' The organization controller is for Organizations, which are implemented +as Groups with is_organization=True and group_type='organization'. It works +the same as the group controller apart from: +* templates and logic action/auth functions are sometimes customized + (switched using _replace_group_org) +* 'bulk_process' action only works for organizations + +Nearly all the code for both is in the GroupController (for historical +reasons). ''' -Created by Francesco Mangiacrapa, see: #8964 -''' -class OrganizationVREController(group.GroupController): - ''' The organization controller is for Organizations, which are implemented - as Groups with is_organization=True and group_type='organization'. It works - the same as the group controller apart from: - * templates and logic action/auth functions are sometimes customized - (switched using _replace_group_org) - * 'bulk_process' action only works for organizations - Nearly all the code for both is in the GroupController (for historical - reasons). - ''' +group_types = ['organization'] - group_types = ['organization'] +def _guess_group_type(expecting_name=False): + return 'organization' - def _guess_group_type(self, expecting_name=False): - return 'organization' +def _replace_group_org( string): + ''' substitute organization for group if this is an org''' + return re.sub('^group', 'organization', string) - def _replace_group_org(self, string): - ''' substitute organization for group if this is an org''' - return re.sub('^group', 'organization', string) +def _update_facet_titles(facets, group_type): + for plugin in plugins.PluginImplementations(plugins.IFacets): + facets = plugin.organization_facets( + facets, group_type, None) - def _update_facet_titles(self, facets, group_type): - for plugin in plugins.PluginImplementations(plugins.IFacets): - facets = plugin.organization_facets( - facets, group_type, None) +@organization_vre.route('/organization_vre') +def index(): + group_type = _guess_group_type() + page = h.get_page_number(request.args) or 1 + items_per_page = 21 + context = {'model': model, 'session': model.Session, + 'user': g.user, 'for_view': True, + 'with_private': False} + + q = g.q = request.params.get('q', '') + sort_by = g.sort_by_selected = request.args.get('sort') + + try: + logic.check_access('site_read', context) + logic.check_access('group_list', context) + except NotAuthorized: + abort(403, _('Not authorized to see this page')) + # pass user info to context as needed to view private datasets of + # orgs correctly + + if g.userobj: + context['user_id'] = g.userobj.id + context['user_is_admin'] = g.userobj.sysadmin + + data_dict_global_results = { + 'all_fields': False, + 'q': q, + 'sort': sort_by, + 'type': group_type or 'group', + } + global_results = logic.get_action('group_list')(context,data_dict_global_results) + + data_dict_page_results = { + 'all_fields': True, + 'q': q, + 'sort': sort_by, + 'type': group_type or 'group', + 'limit': items_per_page, + 'offset': items_per_page * (page - 1), + } + page_results = logic.get_action('group_list')(context, data_dict_page_results) + + g.page = h.Page( + collection=global_results, + page=page, + url=h.pager_url, + items_per_page=items_per_page, + ) + g.page.items = page_results + return render_template('organization_vre/index.html', + extra_vars={'group_type': group_type}) - def index(self): - group_type = self._guess_group_type() - page = h.get_page_number(request.params) or 1 - items_per_page = 21 - - context = {'model': model, 'session': model.Session, - 'user': c.user, 'for_view': True, - 'with_private': False} - - q = c.q = request.params.get('q', '') - sort_by = c.sort_by_selected = request.params.get('sort') - try: - self._check_access('site_read', context) - self._check_access('group_list', context) - except NotAuthorized: - abort(403, _('Not authorized to see this page')) - - # pass user info to context as needed to view private datasets of - # orgs correctly - if c.userobj: - context['user_id'] = c.userobj.id - context['user_is_admin'] = c.userobj.sysadmin - - data_dict_global_results = { - 'all_fields': False, - 'q': q, - 'sort': sort_by, - 'type': group_type or 'group', - } - global_results = self._action('group_list')(context, - data_dict_global_results) - - data_dict_page_results = { - 'all_fields': True, - 'q': q, - 'sort': sort_by, - 'type': group_type or 'group', - 'limit': items_per_page, - 'offset': items_per_page * (page - 1), - } - page_results = self._action('group_list')(context, - data_dict_page_results) - - c.page = h.Page( - collection=global_results, - page=page, - url=h.pager_url, - items_per_page=items_per_page, - ) - - c.page.items = page_results - return base.render('organization_vre/index.html', - extra_vars={'group_type': group_type}) - - - def read(self, id, limit=20): - group_type = self._ensure_controller_matches_group_type( - id.split('@')[0]) - - context = {'model': model, 'session': model.Session, - 'user': c.user, - 'schema': self._db_to_form_schema(group_type=group_type), - 'for_view': True} - data_dict = {'id': id, 'type': group_type} - - # unicode format (decoded from utf8) - c.q = request.params.get('q', '') - - try: - # Do not query for the group datasets when dictizing, as they will - # be ignored and get requested on the controller anyway - data_dict['include_datasets'] = False - c.group_dict = self._action('group_show')(context, data_dict) - c.group = context['group'] - except (NotFound, NotAuthorized): - abort(404, _('Group not found')) - - self._read(id, limit, group_type) - return base.render('organization_vre/read.html', - extra_vars={'group_type': group_type}) \ No newline at end of file +@organization_vre.route('/organization_vre/') +def read(id, limit=20): + #group_type = self._ensure_controller_matches_group_type( + # id.split('@')[0]) + group_type = 'organization' + + context = {'model': model, 'session': model.Session, + 'user': g.user, + 'schema': logic.schema.group_form_schema(), + 'for_view': True} + data_dict = {'id': id, 'type': group_type} + + # recupero eventuale query di ricerca + g.q = request.args.get('q', '') + + try: + #i dataset non si includono nel risultato + data_dict['include_datasets'] = False + g.group_dict = logic.get_action('group_show')(context, data_dict) + g.group = context['group'] + except (NotFound, NotAuthorized): + abort(404, _('Group not found')) + + #read(id, limit, group_type) + return render_template('organization_vre/read.html', + extra_vars={'group_type': group_type}) + \ No newline at end of file diff --git a/ckanext/d4science/controllers/systemtype.py b/ckanext/d4science/controllers/systemtype.py index 02a38f5..a6a33c7 100644 --- a/ckanext/d4science/controllers/systemtype.py +++ b/ckanext/d4science/controllers/systemtype.py @@ -1,88 +1,73 @@ import logging import ckan.plugins as p -from ckan.common import OrderedDict, _, g, c +from ckan.common import OrderedDict, _ import ckan.lib.search as search import ckan.model as model import ckan.logic as logic -import ckan.lib.maintain as maintain -import ckan.lib.base as base import ckan.lib.helpers as h - +from flask import Blueprint, render_template, request, g +from ckan.lib.search import SearchError from urllib.parse import urlencode -#from pylons.i18n import get_lang -from flask import Blueprint, render_template, g, request - -import ckan.lib.base as base -import ckan.lib.navl.dictization_functions as dict_fns -import ckan.authz as authz - - -class d4STypeController(base.BaseController): - - #Overriding controllers.HomeController.index method - def index(self): - try: - # package search - context = {'model': model, 'session': model.Session,'user': g.user, 'auth_user_obj': g.userobj} - - facets = OrderedDict() - - default_facet_titles = { - 'organization': _('Organizations'), - 'groups': _('Groups'), - 'tags': _('Tags'), - 'res_format': _('Formats'), - 'license_id': _('Licenses'), - } - - for facet in g.facets: - if facet in default_facet_titles: - facets[facet] = default_facet_titles[facet] - else: - facets[facet] = facet - - # Facet titles - for plugin in p.PluginImplementations(p.IFacets): - facets = plugin.dataset_facets(facets, 'dataset') - - g.facet_titles = facets - - data_dict = { - 'q': '*:*', - 'facet.field': list(facets.keys()), - 'rows': 4, - 'start': 0, - 'sort': 'views_recent desc', - 'fq': 'capacity:"public"' - } - query = logic.get_action('package_search')(context, data_dict) - g.search_facets = query['search_facets'] - g.package_count = query['count'] - g.datasets = query['results'] - - #print "c.search_facets: " - #print " ".join(c.search_facets) - - except search.SearchError: - g.package_count = 0 - - if g.userobj and not g.userobj.email: - #url = h.url_for(controller='user', action='edit') pylons - url = h.url_for('user.edit') - msg = _('Please update your profile' - ' and add your email address. ') % url + \ - _('%s uses your email address' - ' if you need to reset your password.') \ - % g.site_title - h.flash_notice(msg, allow_html=True) - - #return base.render('type/index.html', cache_force=True) pylons - return render_template('type/index.html', cache_force=True) - d4s_type_blueprint = Blueprint('d4s_type', __name__) - + @d4s_type_blueprint.route('/') def index(): - controller = d4STypeController() - return controller.index() + try: + # package search + context = {'model': model, 'session': model.Session,'user': g.user, 'auth_user_obj': g.userobj} + + facets = OrderedDict() + + default_facet_titles = { + 'organization': _('Organizations'), + 'groups': _('Groups'), + 'tags': _('Tags'), + 'res_format': _('Formats'), + 'license_id': _('Licenses'), + } + + for facet in g.facets: + if facet in default_facet_titles: + facets[facet] = default_facet_titles[facet] + else: + facets[facet] = facet + + # Facet titles + for plugin in p.PluginImplementations(p.IFacets): + facets = plugin.dataset_facets(facets, 'dataset') + + g.facet_titles = facets + + data_dict = { + 'q': '*:*', + 'facet.field': list(facets.keys()), + 'rows': 4, + 'start': 0, + 'sort': 'views_recent desc', + 'fq': 'capacity:"public"' + } + query = logic.get_action('package_search')(context, data_dict) + g.search_facets = query['search_facets'] + g.package_count = query['count'] + g.datasets = query['results'] + + #print "c.search_facets: " + #print " ".join(c.search_facets) + + except search.SearchError: + g.package_count = 0 + + if g.userobj and not g.userobj.email: + #url = h.url_for(controller='user', action='edit') pylons + url = h.url_for('user.edit') + msg = _('Please update your profile' + ' and add your email address. ') % url + \ + _('%s uses your email address' + ' if you need to reset your password.') \ + % g.site_title + h.flash_notice(msg, allow_html=True) + + #return base.render('type/index.html', cache_force=True) pylons + return render_template('type/index.html', cache_force=True) + diff --git a/ckanext/d4science/d4sdiscovery/d4s_cache_controller.py b/ckanext/d4science/d4sdiscovery/d4s_cache_controller.py index 0462d7d..2c90297 100644 --- a/ckanext/d4science/d4sdiscovery/d4s_cache_controller.py +++ b/ckanext/d4science/d4sdiscovery/d4s_cache_controller.py @@ -14,10 +14,6 @@ namespaces_dir = None NAMESPACES_DIR_NAME = "namespaces_for_catalogue" NAMESPACES_CACHE_FILENAME = "Namespaces_Catalogue_Categories.csv" -# Created by Francesco Mangiacrapa -# francesco.mangiacrapa@isti.cnr.it -# ISTI-CNR Pisa (ITALY) - # D4S_Cache_Controller class D4S_Cache_Controller(): diff --git a/ckanext/d4science/d4sdiscovery/d4s_extras.py b/ckanext/d4science/d4sdiscovery/d4s_extras.py index af3e9ce..86bfd60 100644 --- a/ckanext/d4science/d4sdiscovery/d4s_extras.py +++ b/ckanext/d4science/d4sdiscovery/d4s_extras.py @@ -1,8 +1,3 @@ -# Created by Francesco Mangiacrapa -# francesco.mangiacrapa@isti.cnr.it -# ISTI-CNR Pisa (ITALY) - -## questo file va bene anche in p3 ## import logging log = logging.getLogger(__name__) diff --git a/ckanext/d4science/d4sdiscovery/d4s_namespaces_controller.py b/ckanext/d4science/d4sdiscovery/d4s_namespaces_controller.py index afb88df..b187908 100644 --- a/ckanext/d4science/d4sdiscovery/d4s_namespaces_controller.py +++ b/ckanext/d4science/d4sdiscovery/d4s_namespaces_controller.py @@ -44,13 +44,8 @@ def reload_namespaces_from_IS(urlICProxy, resourceID, gcubeToken): log.info("namespaces list read from IS is empty. Skipping caching update") except Exception as e: - print("Error occurred on reading namespaces from IS and refilling the cache!") - print(e) - - -# Created by Francesco Mangiacrapa -# francesco.mangiacrapa@isti.cnr.it -# ISTI-CNR Pisa (ITALY) + log.error("Error occurred on reading namespaces from IS and refilling the cache!") + log.error(e) # D4S_IS_DiscoveryCatalogueNamespacesController is used to discovery namespaces for Catalogue Categories (implemented as a Singleton) diff --git a/ckanext/d4science/d4sdiscovery/d4s_namespaces_extras_util.py b/ckanext/d4science/d4sdiscovery/d4s_namespaces_extras_util.py index edb0bbf..5a47849 100644 --- a/ckanext/d4science/d4sdiscovery/d4s_namespaces_extras_util.py +++ b/ckanext/d4science/d4sdiscovery/d4s_namespaces_extras_util.py @@ -7,11 +7,6 @@ NOCATEOGORY = 'nocategory' log = logging.getLogger(__name__) -# Created by Francesco Mangiacrapa -# francesco.mangiacrapa@isti.cnr.it -# ISTI-CNR Pisa (ITALY) - - # D4S_Namespaces_Extra_Util is used to get the extra fields indexed for D4Science namespaces # @param: namespace_dict is the namespace dict of D4Science namespaces (defined in the Generic Resource: "Namespaces Catalogue Categories") # @param: extras is the dictionary of extra fields for a certain item @@ -25,7 +20,7 @@ class D4S_Namespaces_Extra_Util(): dict_extras = None nms = namespaceid + ":" #has_namespace_ref = None - for key, value in extras: + for key, value in extras.items(): k = key v = value # print "key: " + k @@ -53,7 +48,7 @@ class D4S_Namespaces_Extra_Util(): #break #ADDING ALL EXTRAS WITHOUT NAMESPACE - for key, value in extras: + for key, value in extras.items(): k = key v = value diff --git a/ckanext/d4science/d4sdiscovery/icproxycontroller.py b/ckanext/d4science/d4sdiscovery/icproxycontroller.py index f1c03ce..99a6ef9 100644 --- a/ckanext/d4science/d4sdiscovery/icproxycontroller.py +++ b/ckanext/d4science/d4sdiscovery/icproxycontroller.py @@ -13,11 +13,6 @@ NAMESPACE_ID_LABEL = '@id' log = logging.getLogger(__name__) - -# Created by Francesco Mangiacrapa -# francesco.mangiacrapa@isti.cnr.it -# ISTI-CNR Pisa (ITALY) - def getResponseBody(uri): req = urllib.request.Request(uri) try: @@ -43,6 +38,12 @@ def getResponseBody(uri): class D4S_IS_DiscoveryCatalogueNamespaces(): def __init__(self, urlICProxy, resourceID, gcubeToken): + if not isinstance(urlICProxy, str): + raise ValueError("urlICProxy must be a string") + if not isinstance(resourceID, str): + raise ValueError("resourceID must be a string") + if not isinstance(gcubeToken, str): + raise ValueError("gcubeToken must be a string") self.urlICProxy = urlICProxy self.resourceID = resourceID self.gcubeToken = gcubeToken @@ -53,10 +54,6 @@ class D4S_IS_DiscoveryCatalogueNamespaces(): namespace_list = [] try: - # print("proxy: "+self.urlICProxy) - # print("resourceID: " + self.resourceID) - # print("gcubeTokenParam: " + gcubeTokenParam) - # print("gcubeToken: " + self.gcubeToken) uri = self.urlICProxy + "/" + self.resourceID + "?" + gcubeTokenParam + "=" + self.gcubeToken log.info("Contacting URL: %s" % uri) diff --git a/ckanext/d4science/plugin.py b/ckanext/d4science/plugin.py index 80cf3f0..13b45ef 100644 --- a/ckanext/d4science/plugin.py +++ b/ckanext/d4science/plugin.py @@ -26,14 +26,14 @@ def remove_check_replicated_custom_key(schema): def _package_extras_save(extra_dicts, obj, context): ''' It can save repeated extras as key-value ''' - allow_partial_update = context.get("allow_partial_update", False) - if extra_dicts is None and allow_partial_update: + #allow_partial_update = context.get("allow_partial_update", False) potrebbe non servire + if extra_dicts is None: #and allow_partial_update: return model = context["model"] session = context["session"] - log.debug("extra_dicts: "+unicode(str(extra_dicts)).encode('utf-8')) + log.debug("extra_dicts: "+ str(extra_dicts)) #print "extra_dicts: "+str(extra_dicts) extras_list = obj.extras_list @@ -59,118 +59,121 @@ def _package_extras_save(extra_dicts, obj, context): if extra_dict['value'] is not None: new_extras.setdefault(extra_dict["key"], []).append(extra_dict["value"]) - log.debug("new_extras: "+unicode(str(new_extras)).encode('utf-8')) + log.debug("new_extras: "+ str(new_extras)) #print "new_extras: "+str(new_extras) - #new + #aggiunta di nuove chiavi for key in set(new_extras.keys()) - set(old_extras.keys()): - state = 'active' - log.debug("adding key: "+unicode(key).encode('utf-8')) + #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='active', key=key, value=extra) session.add(extra) extras_list.append(extra) - #deleted + #gestione chiavi eliminate for key in set(old_extras.keys()) - set(new_extras.keys()): - log.debug("deleting key: "+unicode(key).encode('utf-8')) + 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 + #state = 'deleted' + extra.state = 'deleted' extras_list.remove(extra) - #changed + #gestione chiavi aggiornate 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: "+unicode(value).encode('utf-8') + ", new_occur: "+unicode(new_occur).encode('utf-8')+ ", old_occur: "+unicode(old_occur).encode('utf-8')) + 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: "+unicode(value).encode('utf-8') +", 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) for extra in extra_values: - state = 'active' - extra.state = state + #state = 'active' + extra.state = 'active' session.add(extra) #print "extra updated: "+str(extra) - log.debug("extra updated: "+unicode(extra).encode('utf-8')) + 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: "+unicode(value).encode('utf-8') +", is present into new list, adding it to old list") - state = 'active' - extra = model.PackageExtra(state=state, key=key, value=value) - extra.state = state + 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='active', key=key, value=value) + #extra.state = state + #extra.state = 'active' non dovrebbe servire session.add(extra) extras_list.append(extra) old_extras[key].append(value) - log.debug("old extra values updated: "+unicode(old_extras[key]).encode('utf-8')) + 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: "+unicode(value).encode('utf-8')+", is not present into new list, removing "+unicode(countDelete).encode('utf-8')+" occurrence/s from old list") + log.debug("extra - occurrence of: "+ str(value).encode('utf-8') + ", is not present into new list, removing "+ str(countDelete).encode('utf-8')+" 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: "+unicode(extra.value).encode('utf-8')) + log.debug("pkg extra deleting: "+ str(extra.value)) #print "pkg extra deleting: "+str(extra.value) - state = 'deleted' - extra.state = state + #state = 'deleted' + extra.state = 'deleted' else: #print "pkg extra reactivating: "+str(extra.value) - log.debug("pkg extra reactivating: "+unicode(extra.value).encode('utf-8')) - state = 'active' - extra.state = state + log.debug("pkg extra reactivating: "+ str(extra.value)) + #state = 'active' + extra.state = 'active' session.add(extra) else: #print "extra new value: "+str(value) - log.debug("extra new value: "+unicode(value).encode('utf-8')) - state = 'active' - extra = model.PackageExtra(state=state, key=key, value=value) - extra.state = state + log.debug("extra new value: " + str(value)) + #state = 'active' + extra = model.PackageExtra(state='active', key=key, value=value) + #extra.state = state + #extra.state = 'active' session.add(extra) extras_list.append(extra) - #for each value of old list + #chiavi vecchie non presenti 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: "+unicode(extra).encode('utf-8')) - state = 'deleted' - extra.state = state + log.debug("not present extra deleting: "+ str(extra)) + #state = 'deleted' + extra.state = 'deleted' 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] + #lst = [] + #for x in list_package: + # if x.value == value: + # lst.append(x) + # else: + # return lst + # + #return lst #OVERRIDING BASE SQL ALCHEMY ENGINE INSTANCE @@ -189,7 +192,7 @@ def _init_TrackingMiddleware(self, app, config): sqlalchemy_overflow = config.get('sqlalchemy.max_overflow') if sqlalchemy_overflow is None: - sqlalchemy_overflow = 10; + sqlalchemy_overflow = 10 log.debug('sqlalchemy_overflow read: '+str(sqlalchemy_overflow)) @@ -244,7 +247,7 @@ class D4SciencePlugin(plugins.SingletonPlugin): def package_types(self): # Aggiunta del tipo di dato personalizzato deliverable - return ['deliverable_type'] + return [] def is_fallback(self): # Indica che questo plugin può essere usato come fallback se un tipo specifico non è specificato From 01a6f32403c9605ce89e61105f1fced686f4ae8c Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Fri, 11 Oct 2024 16:53:28 +0200 Subject: [PATCH 09/27] code cleaning --- .../d4sdiscovery/icproxycontroller.py | 2 +- ckanext/d4science/helpers.py | 17 +++----------- ckanext/d4science/plugin.py | 23 +++---------------- ckanext/d4science/views.py | 7 +++++- 4 files changed, 13 insertions(+), 36 deletions(-) diff --git a/ckanext/d4science/d4sdiscovery/icproxycontroller.py b/ckanext/d4science/d4sdiscovery/icproxycontroller.py index 99a6ef9..3c2ce9b 100644 --- a/ckanext/d4science/d4sdiscovery/icproxycontroller.py +++ b/ckanext/d4science/d4sdiscovery/icproxycontroller.py @@ -67,7 +67,7 @@ class D4S_IS_DiscoveryCatalogueNamespaces(): bodyToString = etree.tostring(theNamespaces[0]) doc = xmltodict.parse(bodyToString) else: - log.warn("No Namespace for Catalogue Categories found, returning None") + log.warning("No Namespace for Catalogue Categories found, returning None") except Exception as inst: log.error("Error on getting catalogue namespaces: " + str(inst)) log.info("Returning empty list of namespaces") diff --git a/ckanext/d4science/helpers.py b/ckanext/d4science/helpers.py index 6ac0738..7fd40aa 100644 --- a/ckanext/d4science/helpers.py +++ b/ckanext/d4science/helpers.py @@ -323,8 +323,6 @@ def purge_namespace_to_string(facet): return "" facet_name = g.search_facets.get(facet) - print("facet_name " + str(facet_name)) - #logging.debug("facet_name " + str(facet_name)) preferibile in flask end = str(facet_name).index(":") if end <= len(facet_name): return facet_name[:end] @@ -345,8 +343,7 @@ def count_facet_items_dict(facet, limit=None, exclude_active=False): elif not exclude_active: facets.append(dict(active=True, **facet_item)) - # for count, - # print "facets " + str(facets) + # for count, total = len(facets) log.debug("total facet: %s are %d" % (facet, total)) return total @@ -363,13 +360,10 @@ def check_url(the_url): urllib.request.urlopen(the_url) return True except urllib.error.HTTPError as e: - # print(e.code) return False except urllib.error.URLError as e: - # print(e.args) return False except Exception as error: - # print(error) return False @@ -377,7 +371,6 @@ def get_color_for_type(systemtype_field_value): '''Return a color assigned to a system type''' systemtypecolors = session.get(systemtype_field_colors) - # log.info("color: getting color for type: %s" %systemtype_field_value) if systemtypecolors is None: log.info("color: " + systemtype_field_colors + " not found in session, creating new one") @@ -404,7 +397,6 @@ def get_color_for_type(systemtype_field_value): def ordered_dictionary(list_to_be_sorted, property='name', ordering="asc"): - # print ("dict %s" %list_to_be_sorted) ord = False if ordering == "asc" else True @@ -470,8 +462,7 @@ def get_list_of_organizations(limit=10, sort='packages'): to_browse_organizations.append(to_browse_obj) except (logic.NotFound, logic.ValidationError, logic.NotAuthorized) as error: - # SILENT - log.warn("Error on putting organization: %s" % error) + log.warning("Error on putting organization: %s" % error) log.info("browse %d" % len(ordered_organizations) + " organisation/s") except (logic.NotFound, logic.ValidationError, logic.NotAuthorized) as error: @@ -494,7 +485,6 @@ def get_list_of_groups(limit=10, sort='package_count'): ordered_groups = logic.get_action('group_list')({}, data) for group in ordered_groups: - # print "\n\ngroup %s" %group try: to_browse_obj = {} @@ -516,8 +506,7 @@ def get_list_of_groups(limit=10, sort='package_count'): to_browse_groups.append(to_browse_obj) except (logic.NotFound, logic.ValidationError, logic.NotAuthorized) as error: - # SILENT - log.warn("Error on putting group: %s" % error) + log.warning("Error on putting group: %s" % error) log.info("browse %d" % len(ordered_groups) + " organisation/s") except (logic.NotFound, logic.ValidationError, logic.NotAuthorized) as error: diff --git a/ckanext/d4science/plugin.py b/ckanext/d4science/plugin.py index 13b45ef..b07ddcf 100644 --- a/ckanext/d4science/plugin.py +++ b/ckanext/d4science/plugin.py @@ -9,12 +9,7 @@ from ckanext.d4science.logic import validators from ckan.config.middleware.common_middleware import TrackingMiddleware from ckan.model import Package from logging import getLogger - -from ckan.common import ( - g -) - -d4s_ctg_namespaces_controller = None +from flask import g log = getLogger(__name__) @@ -51,7 +46,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: "+unicode(extra_dict["key"]).encode('utf-8')) + log.debug("extra_dict deleted: "+str(extra_dict["key"])) #print 'extra_dict deleted: '+extra_dict["key"] continue @@ -289,22 +284,10 @@ class D4SciencePlugin(plugins.SingletonPlugin): #OVERRIDING BASE SQL ALCHEMY ENGINE INSTANCE TrackingMiddleware.__init__ = _init_TrackingMiddleware - global d4s_ctg_namespaces_controller - # IBlueprint - # def get_blueprint(self): - # return views.get_blueprints() - def get_blueprint(self): - blueprint = Blueprint('foo', self.__module__) - # rules = [ - # ('/group', 'group_index', custom_group_index), - # ] - # for rule in rules: - # blueprint.add_url_rule(*rule) - - return blueprint + return views.get_blueprints() # IClick diff --git a/ckanext/d4science/views.py b/ckanext/d4science/views.py index 7192356..1dbc3c5 100644 --- a/ckanext/d4science/views.py +++ b/ckanext/d4science/views.py @@ -1,5 +1,9 @@ from flask import Blueprint +from ckanext.d4science.controllers.home import d4science_home +from ckanext.d4science.controllers.organization import organization_vre +from ckanext.d4science.controllers.systemtype import d4s_type_blueprint + d4science = Blueprint( "d4science", __name__) @@ -14,4 +18,5 @@ d4science.add_url_rule( def get_blueprints(): - return [d4science] + all_blueprints = [d4science, d4science_home, organization_vre, d4s_type_blueprint] + return all_blueprints From 9f329aee71e76a23152e14eed9c64d654ad7d7d6 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Mon, 14 Oct 2024 09:44:58 +0200 Subject: [PATCH 10/27] code refactoring --- ckanext/d4science/views.py | 6 +++--- ckanext/d4science/{controllers => views}/__init__.py | 0 ckanext/d4science/{controllers => views}/home.py | 4 +--- ckanext/d4science/{controllers => views}/organization.py | 0 ckanext/d4science/{controllers => views}/systemtype.py | 0 5 files changed, 4 insertions(+), 6 deletions(-) rename ckanext/d4science/{controllers => views}/__init__.py (100%) rename ckanext/d4science/{controllers => views}/home.py (95%) rename ckanext/d4science/{controllers => views}/organization.py (100%) rename ckanext/d4science/{controllers => views}/systemtype.py (100%) diff --git a/ckanext/d4science/views.py b/ckanext/d4science/views.py index 1dbc3c5..04bad84 100644 --- a/ckanext/d4science/views.py +++ b/ckanext/d4science/views.py @@ -1,8 +1,8 @@ from flask import Blueprint -from ckanext.d4science.controllers.home import d4science_home -from ckanext.d4science.controllers.organization import organization_vre -from ckanext.d4science.controllers.systemtype import d4s_type_blueprint +from ckanext.d4science.views.home import d4science_home +from ckanext.d4science.views.organization import organization_vre +from ckanext.d4science.views.systemtype import d4s_type_blueprint d4science = Blueprint( diff --git a/ckanext/d4science/controllers/__init__.py b/ckanext/d4science/views/__init__.py similarity index 100% rename from ckanext/d4science/controllers/__init__.py rename to ckanext/d4science/views/__init__.py diff --git a/ckanext/d4science/controllers/home.py b/ckanext/d4science/views/home.py similarity index 95% rename from ckanext/d4science/controllers/home.py rename to ckanext/d4science/views/home.py index 369b277..cc46afd 100644 --- a/ckanext/d4science/controllers/home.py +++ b/ckanext/d4science/views/home.py @@ -2,12 +2,10 @@ #from ckan.controllers.home import HomeController from flask import Blueprint, render_template, g import ckan.plugins as p -from ckan.common import OrderedDict, _, g, c +from ckan.common import OrderedDict, _, g import ckan.lib.search as search import ckan.model as model import ckan.logic as logic -import ckan.lib.maintain as maintain -import ckan.lib.base as base import ckan.lib.helpers as h #blueprint definition diff --git a/ckanext/d4science/controllers/organization.py b/ckanext/d4science/views/organization.py similarity index 100% rename from ckanext/d4science/controllers/organization.py rename to ckanext/d4science/views/organization.py diff --git a/ckanext/d4science/controllers/systemtype.py b/ckanext/d4science/views/systemtype.py similarity index 100% rename from ckanext/d4science/controllers/systemtype.py rename to ckanext/d4science/views/systemtype.py From f625b530fb94e99a7c7e70aa38a8f3cf1bcf569f Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Mon, 14 Oct 2024 10:18:04 +0200 Subject: [PATCH 11/27] add css --- ckanext/d4science/fanstatic/.gitignore | 0 .../d4science/fanstatic/d4science_scripts.js | 305 +++++++ .../d4science/fanstatic/d4science_theme.css | 844 ++++++++++++++++++ 3 files changed, 1149 insertions(+) create mode 100644 ckanext/d4science/fanstatic/.gitignore create mode 100644 ckanext/d4science/fanstatic/d4science_scripts.js create mode 100644 ckanext/d4science/fanstatic/d4science_theme.css diff --git a/ckanext/d4science/fanstatic/.gitignore b/ckanext/d4science/fanstatic/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/ckanext/d4science/fanstatic/d4science_scripts.js b/ckanext/d4science/fanstatic/d4science_scripts.js new file mode 100644 index 0000000..eb44b31 --- /dev/null +++ b/ckanext/d4science/fanstatic/d4science_scripts.js @@ -0,0 +1,305 @@ +/* ===================================================== + JavaScript used by CKAN plugin: 'd4science_theme' + Created by Francesco Mangiacrapa ISTI-CNR Pisa, Italy + ===================================================== */ + +/* +questa pagina incapsula ckan in un iframe della pagina del sito d4science??? +*/ +CKAN_D4S_Breadcrumb_Manager = { + + breadcrumbShow : function (show) { + + var breadcrumb = document.getElementById('ckan-breadcrumb'); + console.log('breadcrumb is '+breadcrumb) + if(breadcrumb){ + if(show){ + breadcrumb.style.display = 'block'; + this.organizationTreeShow(true); + } else{ + breadcrumb.style.display = 'none'; + this.organizationTreeShow(false); + } + } + + //var elements = document.getElementsByTagName('a'); + //for(var i = 0, len = elements.length; i < len; i++) { + // elements[i].onclick = function () { + // //alert("You clicked an external link to: " + this.href); + // //window.parent.add_hide_breadcrumb_to_dom(false); + // this.add_hide_breadcrumb_to_dom(false); + // } + //} + }, + + organizationTreeShow : function (show) { + var trees = document.getElementsByClassName("hierarchy-tree-top"); + + if (trees){ + for (i = 0; i < trees.length; i++) { + if(show){ + trees[i].style.display = 'block'; + } else{ + trees[i].style.display = 'none'; + } + } + } + }, + + checkBreadcrumbShow : function () { + + var showBdc = this.getSessionStorageItem("showbreadcrumb") + //console.log("showBdc is: "+showBdc) + if(showBdc != undefined && showBdc=="false"){ + console.log("Show breadcrumb false"); + this.breadcrumbShow(false); + }else{ + console.log("Show breadcrumb true"); + this.breadcrumbShow(true); + } + }, + + + setSessionStorageItem : function (item_key, item_value) { + + // Check browser support + if (typeof(Storage) !== "undefined") { + // Store + sessionStorage.setItem(item_key, item_value); + return true; + } else { + console.log("Sorry, your browser does not support Web Storage..."); + return false; + } + }, + + + getSessionStorageItem : function (item_key) { + + // Check browser support + if (typeof(Storage) !== "undefined") { + // Store + return sessionStorage.getItem(item_key); + } else { + console.log("Sorry, your browser does not support Web Storage..."); + return undefined; + } + } + +} + + +CKAN_D4S_Functions_Util = { + + getPosition : function(canvas, event){ + var x = new Number(); + var y = new Number(); + try { + if (event.clientX != undefined && event.clientY != undefined) + { + + x = event.clientX; + y = event.clientY; + } + else // Firefox method to get the position + { + x = event.clientX + document.body.scrollLeft + + document.documentElement.scrollLeft; + y = event.clientY + document.body.scrollTop + + document.documentElement.scrollTop; + } + x -= canvas.offsetLeft; + y -= canvas.offsetTop; + }catch (err) { + //silent error + } + return '{"posX": "'+x+'", "posY": "'+y+'"}'; + }, + + // When the user clicks on div, open the popup + showPopupD4S : function(event, my_div, my_popup_left_position) { + var popup = document.getElementById(my_div); + var clickPosition = this.getPosition(my_div, event) + var myPosition = JSON.parse(clickPosition); + this.closePopups(my_div); + // When the user clicks anywhere outside of the modal, close it + /*window.onclick = function(event) { + if (event.target != popup) { + popup.style.display = "none"; + } + }*/ + popup.classList.toggle("show"); + + if(my_popup_left_position){ + popup.style.left = my_popup_left_position; + } + else if (myPosition.posX){ + popup.style.left = myPosition.posX + "px"; + } + }, + + closePopups : function ($target) { + var popups = document.getElementsByClassName('popuptext'); + for (i = 0; i < popups.length; i++) { + if (popups[i].getAttribute('id') != $target) { + popups[i].classList.remove('show'); + } + } + }, + + checkURL : function (url) { + //console.log('checking url: '+url) + var regex = new RegExp('^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/|www\.|ftp:\/\/)+[^ "]+$'); + if (regex.test(url)) { + return true; + } + return false; + } + +} + +CKAN_D4S_HTMLMessage_Util = { + + postHeightToPortlet : function (selectedProduct, product) { + var h = document.body.scrollHeight + "px"; + var p = ""; + var msg = ""; + //WORK AROUND IF TWO MESSAGES ARE SENT FROM A PAGE OF A PRODUCT + //THE MESSAGE WITH 'NULL' PRODUCT IS SKIPPED + //console.log("window.location.pathname? "+window.location.pathname); + var pathArray = window.location.pathname.split('/'); + var productContext = "dataset"; + if(pathArray.length>1){ + //console.log("pathArray is: "+pathArray); + var secondLevelLocation = pathArray[1]; //it is the second level location + //console.log("secondLevelLocation is: "+secondLevelLocation); + //console.log("h is: "+h); + if(secondLevelLocation == productContext){ //is it product context? + if(product !== 'undefined' && product !== null){ + p = product; + //console.log("product selected is: "+p); + }else{ + //console.log("product is null or undefined, passing only height"); + msg = "{\"height\": \""+h+"\"}"; + //window.postMessage(msg,'*'); + this.postMessageToParentWindow(msg); + return; + } + } + } + + //msg = "{'height': '"+h+"', 'product': '"+p+"'}"; + msg = "{\"height\": \""+h+"\", \"product\": \""+p+"\"}"; + //window.postMessage(msg,'*'); + //console.log("posting message in the window: "+msg); + this.postMessageToParentWindow(msg); + }, + + postMessageToParentWindow : function (msg) { + //window.postMessage(msg,'*'); + //console.log("posting message in the window: "+msg); + if(window.parent!=null){ + console.log("posting message in the parent window: "+msg); + window.parent.postMessage(msg,'*'); + } + return; + } + +} + +CKAN_D4S_JSON_Util = { + + + //ADDED by Francesco Mangiacrapa + appendHTMLToElement : function(containerID, elementHTML){ + + var divContainer = document.getElementById(containerID); + divContainer.innerHTML = ""; + divContainer.appendChild(elementHTML); + }, + + //ADDED by Francesco Mangiacrapa + jsonToHTML : function(containerID, cssClassToTable) { + + try + { + var jsonTxt = document.getElementById(containerID).innerHTML; + var jsonObj = JSON.parse(jsonTxt); + //console.log(jsonObj.length) + + if(jsonObj.length==undefined) + jsonObj = [jsonObj] + //console.log(jsonObj.length) + + // EXTRACT VALUE FOR HTML HEADER. + var col = []; + for (var i = 0; i < jsonObj.length; i++) { + for (var key in jsonObj[i]) { + //console.log('key json' +key) + if (col.indexOf(key) === -1) { + col.push(key); + } + } + } + + // CREATE DYNAMIC TABLE. + var table = document.createElement("table"); + var addDefaultCss = "json-to-html-table-column"; + if(cssClassToTable){ + addDefaultCss = cssClassToTable; + } + try{ + table.classList.add(addDefaultCss); + }catch(e){ + console.log('invalid css add', e); + } + + // ADD JSON DATA TO THE TABLE AS ROWS. + for (var i = 0; i < col.length; i++) { + + tr = table.insertRow(-1); + var firstCell = tr.insertCell(-1); + //firstCell.style.cssText="font-weight: bold; text-align: center; vertical-align: middle;"; + firstCell.innerHTML = col[i]; + for (var j = 0; j < jsonObj.length; j++) { + var tabCell = tr.insertCell(-1); + var theValue = jsonObj[j][col[i]]; + /* console.log(theValue + ' is url? '+isUrl);*/ + if(CKAN_D4S_Functions_Util.checkURL(theValue)){ + theValue = ''+theValue+''; + } + + tabCell.innerHTML = theValue; + } + } + + // FINALLY ADD THE NEWLY CREATED TABLE WITH JSON DATA TO A CONTAINER. + this.appendHTMLToElement(containerID, table); + + } + catch(e){ + console.log('invalid json', e); + } + } +} + + +//Task #8032 +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; + },false); + \ No newline at end of file diff --git a/ckanext/d4science/fanstatic/d4science_theme.css b/ckanext/d4science/fanstatic/d4science_theme.css new file mode 100644 index 0000000..9e416b1 --- /dev/null +++ b/ckanext/d4science/fanstatic/d4science_theme.css @@ -0,0 +1,844 @@ +/* ===================================================== + The "account masthead" bar across the top of the site + ===================================================== */ + +.account-masthead { + background-color: #ccc; +} +/* The "bubble" containing the number of new notifications. */ +.account-masthead .account .notifications a span { + background-color: #9fa0a2; +} +/* The text and icons in the user account info. */ +.account-masthead .account ul li a { + color: rgba(255, 255, 255, 0.6); +} +/* The user account info text and icons, when the user's pointer is hovering + over them. */ +.account-masthead .account ul li a:hover { + color: rgba(255, 255, 255, 0.7); +/* background-color: black;*/ +} + + +/* ======================================================================== + The main masthead bar that contains the site logo, nav links, and search + ======================================================================== */ + +.masthead { + background: #eee url("/bg-noise.png") repeat scroll 0 0; + border-top: 1px solid #555; + padding-top: 5px; + padding-bottom: 15px !important; + border-bottom: 1px solid #999; +/* background-image: url("/bg-pattern.min.svg") !important; */ +} + +.masthead .navigation .nav-pills li a{ + color: #187794; +} + +a.logo > img{ + margin-bottom: 5px; +} + +/* The "navigation pills" in the masthead (the links to Datasets, + Organizations, etc) when the user's pointer hovers over them. */ +.masthead .navigation .nav-pills li a:hover { +/* background-color: rgb(48, 48, 48);*/ + color: white; +} +/* The "active" navigation pill (for example, when you're on the /dataset page + the "Datasets" link is active). */ +.masthead .navigation .nav-pills li.active a { + background-color: #d2d2d5; +} +/* The "box shadow" effect that appears around the search box when it + has the keyboard cursor's focus. */ +.masthead input[type="text"]:focus { + -webkit-box-shadow: inset 0px 0px 2px 0px rgba(0, 0, 0, 0.7); + box-shadow: inset 0px 0px 2px 0px rgba(0, 0, 0, 0.7); +} + + +/* =========================================== + The content in the middle of the front page + =========================================== */ + +/* Remove the "box shadow" effect around various boxes on the page. */ +.box { + box-shadow: none; +} +.hero { + background: #FEFEFE repeat scroll 0 0 !important; +} +/* Remove the borders around the "Welcome to CKAN" and "Search Your Data" + boxes. */ +.hero .box { + /*border: none;*/ + margin-top: 10px !important; +} +/* Change the colors of the "Search Your Data" box. */ +.homepage .module-search .module-content { + color: rgb(68, 68, 68); + background-color: white; +} +/* Change the background color of the "Popular Tags" box. */ +.homepage .module-search .tags { + background-color: #fcfcfc; +} + +.homepage-title{ + font-size: 20px; + font-weight: bold; + color: #202020; + margin-bottom: 20px; +} + +/* Change the background color of the "Popular Tags" box. */ +.homepage .module-search h3{ + color: #444; +} + +/* Remove some padding. This makes the bottom edges of the "Welcome to CKAN" + and "Search Your Data" boxes line up. */ +.module-content:last-child { + /*padding-bottom: 0px;*/ +} +.homepage .module-search { + padding: 0px; +} +/* Add a border line between the top and bottom halves of the front page. */ +.homepage [role="main"] { + border-bottom: 1px solid #bbb; + padding: 10px 0; +} + +.homepage .stats ul li a b{ + font-size: 30px !important; +} + +[role="main"], .main { +/* background: #f5f6fa url("/bg-pattern.min.svg") repeat; scroll 0 0;*/ + /*background: #fafafa url("/bg-pattern.svg") repeat; scroll 0 0;*/ + background: #fdfdfd none repeat scroll 0 0; + min-height: 0px !important; +} + +.media-item-homepage { + background-color: white; + border-radius: 3px; + float: left; + margin: 15px 0 0 15px; + overflow: hidden; + padding-left: 10px; + padding-right: 10px; + position: relative; + text-align: center; + width: 150px; +} + +.media-heading-homepage { + font-size: 16px; + hyphens: auto; + line-height: 1.3; + margin: 5px 0; +} + +.media-grid-homepage { + -moz-border-bottom-colors: none; + -moz-border-left-colors: none; + -moz-border-right-colors: none; + -moz-border-top-colors: none; +/* background: #fbfbfb url("../../../base/images/bg.png") repeat scroll 0 0; + border-color: #dddddd; + border-image: none; + border-style: solid; + border-width: 1px 0;*/ + list-style: outside none none; + margin: 0 -10px; + padding-bottom: 15px; +} +.media-grid-homepage::before, .media-grid::after { + content: ""; + display: table; + line-height: 0; +} +.media-grid-homepage::after { + clear: both; +} + +.background-circle{ + padding: 10px 10px; + display: inline-block !important; + -webkit-border-radius: 90px; + -moz-border-radius: 90px; + border-radius: 90px; + background-color: #4679b2; + text-decoration: none !important; +} + +.color-white{ + color: white !important; +} + +.badge-circle { + border-radius: 50% 50% 50% 50% !important; + height: 60px; + text-align: center; + vertical-align: middle; + width: 65px; + background-color: #4679b2; + display: inline-block !important; + padding-top: 5px; + text-decoration: none !important; +} + +/* ==================================== + The footer at the bottom of the site + ==================================== */ + +.site-footer, body { + background-color: #bbb; + font-family: "Lato","Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 16px; +} +/* The text in the footer. */ +.site-footer, +.site-footer label, +.site-footer small { + color: rgba(255, 255, 255, 0.6); +} +/* The link texts in the footer. */ +.site-footer a { + color: rgba(255, 255, 255, 0.6); +} + +.site-footer-internal{ + min-height: 10px; + padding: 2px 0; + font-size: 12px; +} + +.site-footer-internal { + /*background-color: rgba(255, 255, 255, 0.6);*/ + text-align: center; + /*display: inline-block;*/ +} + +.site-footer-internal, +.site-footer-internal label, +.site-footer-internal small { + +} + +.site-footer-internal a { + display: inline-block; +} + +.d4s-hide-text { + background-color: transparent; + border: 0 none; + color: transparent; + font: 0px/0 a; + text-shadow: none; +} + +.d4science-footer-logo { + background: url("/gCube_70.png") no-repeat scroll left top rgba(0, 0, 0, 0); + height: 32px; + margin-top: 2px; + text-indent: -900em; + width: 75px; +} + +.d4s-ckan-footer-logo { + background: rgba(0, 0, 0, 0) url("/ckan-logo-footer.png") no-repeat scroll left top; + height: 21px; + margin-top: 2px; + text-indent: -900em; + width: 69px; +} + +.site-footer-d4science { + font-size: 14px; + color: #f5f5f5; + text-align: center; + height: 25px; + padding-top: 5px; + background-color: #7F7F7F; +} + +.site-footer-d4science a { + font-weight: bold; + text-decoration: none; + color: white; +} + + +/* ==================================== + Base elements of the site + ==================================== */ + +div .principaltitle { + color: inherit; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 20px; + font-weight: bold; + line-height: 1.2; + margin: 15px 0; + text-rendering: optimizelegibility; + word-break: break-all; + padding-bottom: 10px; + padding-top: 5px; + border-bottom: 1px solid #eee; +} + +div .notes { + color: #444444; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 14px; + line-height: 1.3; + text-align: justify; + word-break: break-all; +} + +div .infotitle { + font-size: 15px; + hyphens: auto; + line-height: 1.3; + word-break: break-all; + font-weight: bold; +} + +.toolbar .breadcrumb{ + font-size: 16px !important; +} + +.box{ + border: 0px !important; +} + +div .sectiontitle{ + color: #9F9F9F; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 17px; + font-weight: bold; + margin: 20px 0; + margin-top: 20px; + margin-bottom: 10px; + text-rendering: optimizelegibility; +} + +section .well { + background-color: #fdfdfd !important; + border: 1px solid #e3e3e3; + border-radius: 4px; + box-shadow: none !important; + margin-bottom: 20px; + min-height: 20px; + +} + +.page-heading { + font-size: 18px; + line-height: 1.2; + margin-top: 20px; + margin-bottom: 0px; +} + +#dataset-resources .resource-list{ + background-color: #fdfdfd !important; + border: 1px solid #e3e3e3; + border-radius: 4px; + box-shadow: none !important; + margin: -1px 0 !important; +} + +.wrapper{ + border: 1px solid #d0d0d0; + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05); + border-radius: 3px +} + +.home-popular{ + padding-top: 25px; +} + +.logo-homepage{ + max-height: 60px; +} + +.statistics-show{ + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + color: #444444; + text-decoration: none; +} + +.d4s-center-cropped{ + text-align: center; + background-color: #eee; + border: 1px solid #ddd; + padding-bottom: 10px; + padding-top: 10px; +} + +.tag-list { + font-size: 14px; +} + + +/* ==================================== + Acquired Dataset + ==================================== */ +.label-acquired { + background-color: #55a1ce; +} + +.label-owner { + background-color: #e0051e; +} + +.divider { + margin-left:10px; + height:auto; + display:inline-block; +} + +/* ==================================== + List Dataset + ==================================== */ + +/*LEFT +.show_meatadatatype { + color: white; + display: inline-block; // Inline elements with width and height. TL;DR they make the icon buttons stack from left-to-right instead of top-to-bottom + position: relative; // All 'absolute'ly positioned elements are relative to this one + margin-bottom: 20px; + margin-left: 25px; +} +*/ + +/*RIGHT*/ +.show_meatadatatype { + color: white; + display: inline-block; + float: right; + margin-right: 2px; + margin-top: -20px; + position: relative; +} + + + +/* LEFT + * Position the badge within the relatively positioned button +.button__badge { + background-color: #fa3e3e; + border-radius: 2px; + color: white; + + padding: 1px 6px; + font-size: 10px; + + position: absolute; + top: 0; + right: 0; +}*/ + + + +/* RIGTH */ +.button__badge { + color: #808080; + padding: 0px 2px; + font-size: 10px; + top: 0; + right: 0; + font-family: sans-serif, times, georgia; +} + +/* ==================================== + Modal Popup + ==================================== */ + +/* Popup container - can be anything you want */ +/* The Modal (background) */ +.d4s_modal { + display: none; /* Hidden by default */ + position: fixed; /* Stay in place */ + z-index: 10001; /* Sit on top (NB. At 1000 there is the zoom in/out of the Map Widget)*/ + /*padding-top: 100px;*/ /* Location of the box */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background-color: rgb(0,0,0); /* Fallback color */ + background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ +} + +/* Modal Content */ +.d4s_modal-content { + background-color: #fefefe; + /*margin: auto;*/ + padding: 20px; + border: 1px solid #888; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + position: absolute; + left: 50%; + margin-left: -225px; + width: 450px; +} + +/* The Close Button */ +.d4s_close { + color: #aaaaaa; + float: right; + font-size: 28px; + font-weight: bold; + padding-left: 20px; +} + +.d4s_close:hover, +.d4s_close:focus { + color: #000; + text-decoration: none; + cursor: pointer; +} + +.d4s_div_clickable{ + cursor: pointer; +} + +/*==================================== +D4S POPUP +======================================*/ + +/* Popup container - can be anything you want */ +.popupD4SNoArrow { + position: relative; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/* The actual popup */ +.popupD4SNoArrow .popuptext { + visibility: hidden; + width: 300px; + background-color: #555; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 8px; + position: absolute; + z-index: 1; + bottom: 125%; + left: 50%; + margin-left: -150px; +} + +/* Toggle this class - hide and show the popup */ +.popupD4SNoArrow .show { + visibility: visible; + -webkit-animation: fadeIn 1s; + animation: fadeIn 1s; +} + + +/* Popup container - can be anything you want */ +.popupD4S { + position: relative; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/* The actual popup */ +.popupD4S .popuptext { + visibility: hidden; + width: 300px; + background-color: #555; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 8px; + position: absolute; + z-index: 1; + bottom: 125%; + left: 50%; + margin-left: -150px; +} + +/* Popup arrow */ +.popupD4S .popuptext::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #555 transparent transparent transparent; +} + +/* Toggle this class - hide and show the popup */ +.popupD4S .show { + visibility: visible; + -webkit-animation: fadeIn 1s; + animation: fadeIn 1s; +} + +/* Add animation (fade in the popup) */ +@-webkit-keyframes fadeIn { + from {opacity: 0;} + to {opacity: 1;} +} + +@keyframes fadeIn { + from {opacity: 0;} + to {opacity:1 ;} +} + +/*==================================== +D4S PACKAGE +======================================*/ + +.graphic-preview-style { + text-align: center; + border-top: 1px dotted #DDD; + padding-top: 10px; + padding-bottom: 0px; + margin-top: 15px; +} + +.graphic-preview-style a{ + font-size: 13px; +} + +.graphic-preview-style img{ + max-width: 100% !important; + height: auto; + +} + +.graphic-preview-style #graphic-title{ + font-size: 13px; + +} + +.nav-item{ + word-wrap:break-word; + } + +/*==================================== +RESOURCE_LIST RESOURCE_ITEM INTO PACKAGE +======================================*/ + +.required-access { + font-style: italic; + font-weight: bold; + padding: 5px; +} + +/*==================================== +LINK TO RESOURCES FROM PACKAGE LIST +======================================*/ + +.dataset-resources li a { + background-color: #187794; +} + +.label[data-format="csw"], .label[data-format*="csw"] { + background-color: #e6b800; +} + +/*==================================== +CSS APPLIED TO Similar GRSF Records +======================================*/ + +.my-grsf-table{ + word-break: break-all; +} + +.my-grsf-table tr td{ + width: inherit; +} + +.my-grsf-table tr td:first-child{ + width: 82px !important; +} + +/*==================================== +CSS APPLIED in base.html +======================================*/ + +#ckan-page-loading { + display: none; + position: fixed; + top: 50%; + left: 50%; + margin-top: -130px; + margin-left: -130px; + width: 260px; + height: 260px; + z-index: 100000; + background-image: url("/pageloading.gif"); + background-repeat: no-repeat; + background-position: center; +} + +/*==================================== +CSS APPLIED in search_for_location.html +======================================*/ + +div#search-for-location { + +} + +div#search-for-location #dataset-map { + position: relative !important; + top: +0px !important; +} + +div#search-for-location #dataset-map-container { + height: 300px; +} + + +div#search-for-location .module-heading { + display: none; +} + +div#search-for-extent{ + padding-top: 10px; +} + +/*==================================== +CSS APPLIED in additional_info.html +======================================*/ +.qr-code-table { + width: 100%; +} + +.qr-code-table td { + width: 85%; + border: 1px solid #e3e3e3; +} + +.qr-code-table td:first-child { + padding-left: 10px; + border-right-style: none; + +} + +.qr-code-table td:last-child { + width: 105px; + text-align: center; + border-left-style: none; + +} + +/* MAX-WITH APPIED TO QR_CODE */ +.qr-code-table img { + max-width: 100px; + height: auto; +} + + +/*==================================== +CSS APPLIED FROM JSON TO HTML TABLE +======================================*/ + +.json-to-html-table-column{ + word-break: break-all; +} + +.json-to-html-table-column tr td{ + width: inherit; +} + +.json-to-html-table-column tr td:first-child{ + font-weight: bold; + color: #5a5a5a; +} + +/*==================================== +CSS APPLIED into custom_form_fields +======================================*/ +.disabled-div{ + pointer-events: none; + opacity: 0.5; +} + +.disabled-div input[type="text"]{ + background: #f1f1f1; +} + +/*==================================== +CSS APPLIED into extra_table.html +======================================*/ + +.read-more-state { + display: none; +} + +.read-more-target { + opacity: 0; + max-height: 0; + font-size: 0; + transition: .25s ease; +} + +.read-more-state:checked ~ .read-more-wrap .read-more-target { + opacity: 1; + font-size: inherit; + max-height: 999em; + content: ""; +} + +.read-more-state ~ .read-more-trigger:before { + content: 'Show more'; +} + +.read-more-state:checked ~ .read-more-trigger:before { + content: 'Show less'; +} + +.read-more-state:checked ~ .read-more-wrap::after { + content: ""; +} + +.read-more-trigger { + cursor: pointer; + display: inline-block; + padding: 0 .5em; + color: #187794; + font-size: .9em; + line-height: 2; + border: 1px solid #ddd; + border-radius: .25em; + font-weight: normal; +} + +.read-more-trigger::after { + content: ""; +} + +.read-more-wrap { + margin-bottom: 2px; +} + +.read-more-wrap::after{ + content: " ..."; + +} \ No newline at end of file From 6be6dab9f356fb8c95f9ce6e77dc17a608c863e4 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Mon, 14 Oct 2024 10:22:19 +0200 Subject: [PATCH 12/27] add .project --- .project | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .project diff --git a/.project b/.project new file mode 100644 index 0000000..2c4c958 --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + ckanext-d4science_theme + + + + + + org.python.pydev.PyDevBuilder + + + + + + org.python.pydev.pythonNature + + From 0dba267cd679a39e8362ef6fedef47064bb07877 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Tue, 15 Oct 2024 10:45:42 +0200 Subject: [PATCH 13/27] add requirement.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index e69de29..dc0837d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1 @@ +WebHelpers \ No newline at end of file From 2c3c2be8a87d847b06f40f02a5e1638fa338b24b Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Tue, 15 Oct 2024 10:49:28 +0200 Subject: [PATCH 14/27] add dev-requiremnts --- dev-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index eac82b4..cd552b5 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1 +1,2 @@ pytest-ckan +WebHelpers \ No newline at end of file From 8aa11f1725c82a864590c722f3fd6637676b8244 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Tue, 15 Oct 2024 11:01:33 +0200 Subject: [PATCH 15/27] fix urllib dependency --- .project | 2 +- ckanext/d4science/helpers.py | 3 ++- dev-requirements.txt | 3 ++- requirements.txt | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.project b/.project index 2c4c958..66687f6 100644 --- a/.project +++ b/.project @@ -1,6 +1,6 @@ - ckanext-d4science_theme + ckanext-d4science diff --git a/ckanext/d4science/helpers.py b/ckanext/d4science/helpers.py index 7fd40aa..866479d 100644 --- a/ckanext/d4science/helpers.py +++ b/ckanext/d4science/helpers.py @@ -9,7 +9,8 @@ from ckan.common import config from ckanext.d4science.d4sdiscovery.d4s_namespaces_controller import D4S_Namespaces_Controller from ckanext.d4science.d4sdiscovery.d4s_namespaces_extras_util import D4S_Namespaces_Extra_Util from ckanext.d4science.qrcodelink.generate_qrcode import D4S_QrCode -import urllib.request, urllib.error, urllib.parse +import urllib.request, urllib.error +from urllib.parse import quote from ckan.common import ( _, ungettext, g, c, request, session, json, OrderedDict diff --git a/dev-requirements.txt b/dev-requirements.txt index cd552b5..56ba103 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,2 +1,3 @@ pytest-ckan -WebHelpers \ No newline at end of file +WebHelpers +WebHelpers2 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index dc0837d..c1e8eef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -WebHelpers \ No newline at end of file +WebHelpers +WebHelpers2 \ No newline at end of file From 3b90bef228521a9c027700ef67ec1ba444f869f7 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Tue, 15 Oct 2024 11:08:03 +0200 Subject: [PATCH 16/27] removed import --- ckanext/d4science/helpers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ckanext/d4science/helpers.py b/ckanext/d4science/helpers.py index 866479d..327b840 100644 --- a/ckanext/d4science/helpers.py +++ b/ckanext/d4science/helpers.py @@ -10,7 +10,6 @@ from ckanext.d4science.d4sdiscovery.d4s_namespaces_controller import D4S_Namespa from ckanext.d4science.d4sdiscovery.d4s_namespaces_extras_util import D4S_Namespaces_Extra_Util from ckanext.d4science.qrcodelink.generate_qrcode import D4S_QrCode import urllib.request, urllib.error -from urllib.parse import quote from ckan.common import ( _, ungettext, g, c, request, session, json, OrderedDict From c90115cc8dde46aad0692739d0f50df04e745b2f Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Tue, 15 Oct 2024 11:14:17 +0200 Subject: [PATCH 17/27] removed dependency --- ckanext/d4science/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckanext/d4science/helpers.py b/ckanext/d4science/helpers.py index 327b840..5f58363 100644 --- a/ckanext/d4science/helpers.py +++ b/ckanext/d4science/helpers.py @@ -1,7 +1,7 @@ import ckan.authz as authz import ckan.model as model -from webhelpers.html import escape, HTML, literal, url_escape +from webhelpers.html import escape, HTML, literal from webhelpers.text import truncate import ckan.lib.helpers as h import ckan.logic as logic From 14fd146c72e1308a346b09c24fa4c8a6272575ca Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Tue, 15 Oct 2024 11:17:09 +0200 Subject: [PATCH 18/27] library issue --- ckanext/d4science/helpers.py | 4 ++-- dev-requirements.txt | 1 - requirements.txt | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/ckanext/d4science/helpers.py b/ckanext/d4science/helpers.py index 5f58363..4b35f23 100644 --- a/ckanext/d4science/helpers.py +++ b/ckanext/d4science/helpers.py @@ -1,8 +1,8 @@ import ckan.authz as authz import ckan.model as model -from webhelpers.html import escape, HTML, literal -from webhelpers.text import truncate +from webhelpers2.html import escape, HTML, literal +from webhelpers2.text import truncate import ckan.lib.helpers as h import ckan.logic as logic from ckan.common import config diff --git a/dev-requirements.txt b/dev-requirements.txt index 56ba103..87eb0fd 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,2 @@ pytest-ckan -WebHelpers WebHelpers2 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c1e8eef..ab6aa31 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -WebHelpers WebHelpers2 \ No newline at end of file From 9bfb69dd05390f20d6d511f6ac81ad7e82435993 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Tue, 15 Oct 2024 11:19:45 +0200 Subject: [PATCH 19/27] add xmltodict --- dev-requirements.txt | 3 ++- requirements.txt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 87eb0fd..a0c0fdc 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,2 +1,3 @@ pytest-ckan -WebHelpers2 \ No newline at end of file +WebHelpers2 +xmltodict \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index ab6aa31..235a30a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -WebHelpers2 \ No newline at end of file +WebHelpers2 +xmltodict \ No newline at end of file From 78ecaa49cb9af2b54f2e430f23f8de237c36d974 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Tue, 15 Oct 2024 11:30:19 +0200 Subject: [PATCH 20/27] fix orderdict import --- ckanext/d4science/d4sdiscovery/d4s_namespaces_extras_util.py | 1 + ckanext/d4science/helpers.py | 3 ++- dev-requirements.txt | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ckanext/d4science/d4sdiscovery/d4s_namespaces_extras_util.py b/ckanext/d4science/d4sdiscovery/d4s_namespaces_extras_util.py index 5a47849..3073528 100644 --- a/ckanext/d4science/d4sdiscovery/d4s_namespaces_extras_util.py +++ b/ckanext/d4science/d4sdiscovery/d4s_namespaces_extras_util.py @@ -1,5 +1,6 @@ import logging import collections +from collections import OrderedDict from .d4s_extras import D4S_Extras CATEGORY = 'category' diff --git a/ckanext/d4science/helpers.py b/ckanext/d4science/helpers.py index 4b35f23..c20e2ad 100644 --- a/ckanext/d4science/helpers.py +++ b/ckanext/d4science/helpers.py @@ -12,7 +12,7 @@ from ckanext.d4science.qrcodelink.generate_qrcode import D4S_QrCode import urllib.request, urllib.error from ckan.common import ( - _, ungettext, g, c, request, session, json, OrderedDict + _, ungettext, g, c, request, session, json ) from flask import Blueprint, render_template, g, request, url_for, current_app @@ -24,6 +24,7 @@ import base64 import sys, os, re import configparser import collections +from collections import OrderedDict log = getLogger(__name__) diff --git a/dev-requirements.txt b/dev-requirements.txt index a0c0fdc..89469bb 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,4 @@ pytest-ckan WebHelpers2 -xmltodict \ No newline at end of file +xmltodict +pyqrcode \ No newline at end of file From 84ddb7317e7d8bad0bc83afc0821c0df8d43140b Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Tue, 15 Oct 2024 11:36:15 +0200 Subject: [PATCH 21/27] fix param error --- ckanext/d4science/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckanext/d4science/plugin.py b/ckanext/d4science/plugin.py index b07ddcf..66dea92 100644 --- a/ckanext/d4science/plugin.py +++ b/ckanext/d4science/plugin.py @@ -297,7 +297,7 @@ class D4SciencePlugin(plugins.SingletonPlugin): # ITemplateHelpers def get_helpers(self): - return helpers.get_helpers() + return helpers.get_helpers(self) # IValidators From 295692fff97c8dace0409b90eb6688f564c757bb Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Tue, 15 Oct 2024 11:45:41 +0200 Subject: [PATCH 22/27] refactoring views --- ckanext/d4science/plugin.py | 2 +- ckanext/d4science/views.py | 6 +++--- ckanext/d4science/{views => views_routes}/__init__.py | 0 ckanext/d4science/{views => views_routes}/home.py | 0 ckanext/d4science/{views => views_routes}/organization.py | 0 ckanext/d4science/{views => views_routes}/systemtype.py | 0 6 files changed, 4 insertions(+), 4 deletions(-) rename ckanext/d4science/{views => views_routes}/__init__.py (100%) rename ckanext/d4science/{views => views_routes}/home.py (100%) rename ckanext/d4science/{views => views_routes}/organization.py (100%) rename ckanext/d4science/{views => views_routes}/systemtype.py (100%) diff --git a/ckanext/d4science/plugin.py b/ckanext/d4science/plugin.py index 66dea92..b07ddcf 100644 --- a/ckanext/d4science/plugin.py +++ b/ckanext/d4science/plugin.py @@ -297,7 +297,7 @@ class D4SciencePlugin(plugins.SingletonPlugin): # ITemplateHelpers def get_helpers(self): - return helpers.get_helpers(self) + return helpers.get_helpers() # IValidators diff --git a/ckanext/d4science/views.py b/ckanext/d4science/views.py index 04bad84..ce17a6a 100644 --- a/ckanext/d4science/views.py +++ b/ckanext/d4science/views.py @@ -1,8 +1,8 @@ from flask import Blueprint -from ckanext.d4science.views.home import d4science_home -from ckanext.d4science.views.organization import organization_vre -from ckanext.d4science.views.systemtype import d4s_type_blueprint +from ckanext.d4science.views_routes.home import d4science_home +from ckanext.d4science.views_routes.organization import organization_vre +from ckanext.d4science.views_routes.systemtype import d4s_type_blueprint d4science = Blueprint( diff --git a/ckanext/d4science/views/__init__.py b/ckanext/d4science/views_routes/__init__.py similarity index 100% rename from ckanext/d4science/views/__init__.py rename to ckanext/d4science/views_routes/__init__.py diff --git a/ckanext/d4science/views/home.py b/ckanext/d4science/views_routes/home.py similarity index 100% rename from ckanext/d4science/views/home.py rename to ckanext/d4science/views_routes/home.py diff --git a/ckanext/d4science/views/organization.py b/ckanext/d4science/views_routes/organization.py similarity index 100% rename from ckanext/d4science/views/organization.py rename to ckanext/d4science/views_routes/organization.py diff --git a/ckanext/d4science/views/systemtype.py b/ckanext/d4science/views_routes/systemtype.py similarity index 100% rename from ckanext/d4science/views/systemtype.py rename to ckanext/d4science/views_routes/systemtype.py From e41e3bafa40157c1aa4bfcf97d00971fda56caaa Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Tue, 15 Oct 2024 11:48:56 +0200 Subject: [PATCH 23/27] add requirements --- dev-requirements.txt | 3 ++- requirements.txt | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 89469bb..4a4335f 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,5 @@ pytest-ckan WebHelpers2 xmltodict -pyqrcode \ No newline at end of file +pyqrcode +collections \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 235a30a..0cdad96 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ WebHelpers2 -xmltodict \ No newline at end of file +xmltodict +pyqrcode +collections \ No newline at end of file From dfd994954bc451a9c0ecab7c7662ef63b6ef3e34 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Tue, 15 Oct 2024 11:59:59 +0200 Subject: [PATCH 24/27] webhelpers2 --- dev-requirements.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 4a4335f..5aadff4 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,5 @@ pytest-ckan -WebHelpers2 +webhelpers2 xmltodict pyqrcode collections \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0cdad96..c094e61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -WebHelpers2 +webhelpers2 xmltodict pyqrcode collections \ No newline at end of file From 5f7bec6f6ba59320dab94cdc1686da61be0dc2b4 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Tue, 15 Oct 2024 12:12:08 +0200 Subject: [PATCH 25/27] add version to webhelpers2 --- dev-requirements.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 5aadff4..eba64ef 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,5 @@ pytest-ckan -webhelpers2 +webhelpers2==2.1 xmltodict pyqrcode collections \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c094e61..949d55f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -webhelpers2 +webhelpers2==2.1 xmltodict pyqrcode collections \ No newline at end of file From 66adb8289dfba2b34d547586e625384494ee06d5 Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Tue, 15 Oct 2024 12:20:44 +0200 Subject: [PATCH 26/27] fix req files --- dev-requirements.txt | 3 +-- requirements.txt | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index eba64ef..e83a3c8 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,4 @@ pytest-ckan webhelpers2==2.1 xmltodict -pyqrcode -collections \ No newline at end of file +pyqrcode \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 949d55f..8193538 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ webhelpers2==2.1 xmltodict -pyqrcode -collections \ No newline at end of file +pyqrcode \ No newline at end of file From 93704ee4d56308c98134d8799f48b465d9ea1d2a Mon Sep 17 00:00:00 2001 From: Alessio Fabrizio Date: Tue, 15 Oct 2024 12:25:37 +0200 Subject: [PATCH 27/27] readd collections to req files --- ckanext/d4science/helpers.py | 1 + dev-requirements.txt | 3 ++- requirements.txt | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ckanext/d4science/helpers.py b/ckanext/d4science/helpers.py index c20e2ad..157c5f4 100644 --- a/ckanext/d4science/helpers.py +++ b/ckanext/d4science/helpers.py @@ -18,6 +18,7 @@ from ckan.common import ( from flask import Blueprint, render_template, g, request, url_for, current_app import random +import webhelpers2 from operator import itemgetter from logging import getLogger import base64 diff --git a/dev-requirements.txt b/dev-requirements.txt index e83a3c8..eba64ef 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,5 @@ pytest-ckan webhelpers2==2.1 xmltodict -pyqrcode \ No newline at end of file +pyqrcode +collections \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8193538..949d55f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ webhelpers2==2.1 xmltodict -pyqrcode \ No newline at end of file +pyqrcode +collections \ No newline at end of file