diff --git a/ckanext/ubdc/assets/css/style.css b/ckanext/ubdc/assets/css/style.css index 1de6221..8c570a4 100644 --- a/ckanext/ubdc/assets/css/style.css +++ b/ckanext/ubdc/assets/css/style.css @@ -7131,6 +7131,11 @@ button.close { overflow: hidden; } +input[type=checkbox][name=contactConsent]:focus, +input[type=checkbox][name=consentName]:focus { + outline: 1px solid lightblue; +} + .beta-message { background: #d5580d; color: #fff; @@ -11379,6 +11384,58 @@ br.line-height2 { color: #843534; } +.access-request-response { + position: relative; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + margin-bottom: 15px; +} +.access-request-response .response { + margin: 0; + font-size: 16px; + padding: 10px 20px; + color: #777; + background-color: #f7f7f7; + border-bottom: 1px solid #eee; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} +.access-request-response p { + padding: 20px; +} + +.checkboxes label { + display: flex; + font-weight: 400; +} +.checkboxes label input { + margin: 0; + margin-right: 8px; +} +.checkboxes label::after { + content: ""; + display: block; + clear: both; +} + +.radio-group label { + display: flex; +} +.radio-group label input { + margin: 0; + margin-right: 8px; +} + +.access-request-container { + display: flex; + justify-content: flex-end; + margin: 10px; +} +.access-request-container a { + font-size: 14px; +} + .input-group .form-control { z-index: 0; } diff --git a/ckanext/ubdc/assets/js/access_request.js b/ckanext/ubdc/assets/js/access_request.js new file mode 100644 index 0000000..4294209 --- /dev/null +++ b/ckanext/ubdc/assets/js/access_request.js @@ -0,0 +1,47 @@ +ckan.module("data-service-request-form", function (jQuery) { + return { + initialize: function () { + var message = this._('There are unsaved modifications to this form'); + + $.proxyAll(this, /_on/); + + this.el.incompleteFormWarning(message); + + // Disable the submit button on form submit, to prevent multiple + // consecutive form submissions. + + this.el.on('submit', this._onSubmit); + + this.el.find('.checkboxes label').click(function (event) { + event.stopPropagation(); + var checkbox = $(this).find('input'); + checkbox.prop('checked', !checkbox.prop('checked')); + }); + + this.el.find('.checkboxes label input').click(function (event) { + event.stopPropagation(); + }); + }, + _onSubmit: function () { + // The button is not disabled immediately so that its value can be sent + // the first time the form is submitted, because the "save" field is + // used in the backend. + + var consent = this.el.find('input[name="consent"]') + var contactConsent = this.el.find('input[name="contactConsent"]') + if (!consent.prop('checked')) { + consent.focus(); + this.el.preventDefault() + } + + if (!contactConsent.prop('checked')) { + contactConsent.focus(); + this.el.preventDefault() + } + + setTimeout(function () { + this.el.find('button[name="save"]').attr('disabled', true); + }.bind(this), 0); + } + }; +}); \ No newline at end of file diff --git a/ckanext/ubdc/assets/js/reCaptcha.js b/ckanext/ubdc/assets/js/reCaptcha.js new file mode 100644 index 0000000..c810e93 --- /dev/null +++ b/ckanext/ubdc/assets/js/reCaptcha.js @@ -0,0 +1,35 @@ +ckan.module('reCaptcha', function (jQuery) { + return { + options: { + sitekey: null, + }, + initialize: function () { + var recaptcha = document.createElement('script'); + recaptcha.src = 'https://www.google.com/recaptcha/api.js?render=' + this.options.sitekey; + recaptcha.async = true; + recaptcha.defer = true; + document.body.appendChild(recaptcha); + $.proxyAll(this, /_on/); + + // onClick event + this.el.on('click', this._onClick); + + // hidden input field with the token + this.el.before(''); + + }, + _onClick: function (event) { + event.preventDefault(); + var module = this; + grecaptcha.ready(function (module) { + return function () { + grecaptcha.execute(module.options.sitekey, { action: 'submit' }).then(function (token) { + jQuery('input[name="g-recaptcha-response"]').val(token); + jQuery('#data-service-access-request')[0].reportValidity() + module.el.closest('form').submit(); + }); + }; + }(module)); + } + }; +}); \ No newline at end of file diff --git a/ckanext/ubdc/assets/webassets.yml b/ckanext/ubdc/assets/webassets.yml index 6cec395..3248915 100644 --- a/ckanext/ubdc/assets/webassets.yml +++ b/ckanext/ubdc/assets/webassets.yml @@ -12,6 +12,8 @@ ubdc-cc-js: output: ckanext-ubdc/%(version)s-ubdc-cc.js contents: - js/civic_cookies.js + - js/access_request.js + - js/reCaptcha.js - js/beta_message.js ubdc-css: diff --git a/ckanext/ubdc/helpers.py b/ckanext/ubdc/helpers.py index 01d690a..e0a3a3c 100644 --- a/ckanext/ubdc/helpers.py +++ b/ckanext/ubdc/helpers.py @@ -1,56 +1,71 @@ - from ckan import model from ckan.logic import get_action import ckan.plugins.toolkit as tk +from ckanext.ubdc import view + def popular_datasets(limit=3): """Return a list of the most popular datasets.""" - context = {'model': model, 'session': model.Session} - data_dict = {'sort': 'views_recent desc', 'rows': limit} - return get_action('package_search')(context, data_dict)['results'] + context = {"model": model, "session": model.Session} + data_dict = {"sort": "views_recent desc", "rows": limit} + return get_action("package_search")(context, data_dict)["results"] def resources_count(): """Return the total number of resources.""" - context = {'model': model, 'session': model.Session} - data_dict = {'query': 'name:'} - return get_action('resource_search')(context, data_dict)['count'] + context = {"model": model, "session": model.Session} + data_dict = {"query": "name:"} + return get_action("resource_search")(context, data_dict)["count"] + def tags_count(): """Return the total number of tags.""" - context = {'model': model, 'session': model.Session} - return {'count': len(get_action('tag_list')(context, {})) } + context = {"model": model, "session": model.Session} + return {"count": len(get_action("tag_list")(context, {}))} + def get_gtm_id(): """Return the Google Tag Manager ID.""" - return tk.config.get('ls', '') + return tk.config.get("ls", "") + def get_cookie_control_config(): + cookie_control_config = {} + + api_key = tk.config.get("ckanext.ubdc.cc.api_key", "") + cookie_control_config["api_key"] = api_key - cookie_control_config = {} + license_type = tk.config.get("ckanext.ubdc.cc.license_type", "COMMUNITY") + cookie_control_config["license_type"] = license_type - api_key = tk.config.get( - 'ckanext.ubdc.cc.api_key', '') - cookie_control_config['api_key'] = api_key + popup_position = tk.config.get("ckanext.ubdc.cc.popup_position", "LEFT") + cookie_control_config["popup_position"] = popup_position - license_type = tk.config.get( - 'ckanext.ubdc.cc.license_type', 'COMMUNITY') - cookie_control_config['license_type'] = license_type + theme_color = tk.config.get("ckanext.ubdc.cc.theme_color", "DARK") + cookie_control_config["theme_color"] = theme_color - popup_position = tk.config.get( - 'ckanext.ubdc.cc.popup_position', 'LEFT') - cookie_control_config['popup_position'] = popup_position + initial_state = tk.config.get("ckanext.ubdc.cc.initial_state", "OPEN") + cookie_control_config["initial_state"] = initial_state - theme_color = tk.config.get( - 'ckanext.ubdc.cc.theme_color', 'DARK') - cookie_control_config['theme_color'] = theme_color + gtm_id = tk.config.get("googleanalytics.measurement_id", "") + cookie_control_config["gtm_id"] = gtm_id.replace("G-", "_ga_") - initial_state = tk.config.get( - 'ckanext.ubdc.cc.initial_state', 'OPEN') - cookie_control_config['initial_state'] = initial_state + return cookie_control_config - gtm_id = tk.config.get( - 'googleanalytics.measurement_id', '') - cookie_control_config['gtm_id'] = gtm_id.replace('G-', '_ga_') - return cookie_control_config +def get_field_to_question(value): + get_field_to_question = { + "first_name": "First Name", + "surname": "Surname", + "organization": "Organisation / Institution", + "email": "Email Address", + "telephone": "Telephone Number", + "country": "Country", + "summary_of_project": "Summary of your project or intended use of UBDC data and/or services", + "project_funding": "The UBDC does not provide project funding itself, but our collections and services can be used in projects funded from other sources. Are you applying for funding for this project or work?", + "funding_information": "If yes, please provide additional information (e.g. deadline) and/or a link to the Funding Call.", + "wish_to_use_data": "Please select all data from the UBDC collections you wish to use.", + "document_url": "Optional supporting documentation upload (PDF or DOC only).", + "created": "Date of submission", + } + return get_field_to_question.get(value, value) diff --git a/ckanext/ubdc/logic/action.py b/ckanext/ubdc/logic/action.py new file mode 100644 index 0000000..712a8d6 --- /dev/null +++ b/ckanext/ubdc/logic/action.py @@ -0,0 +1,145 @@ +import ast +import datetime +import logging +import ckan.plugins.toolkit as tk +from ckan.lib.navl.dictization_functions import validate +import ckan.lib.uploader as uploader +from ckan.lib.mailer import mail_recipient +from ckanext.ubdc.logic import schema +from ckanext.ubdc.model.access_request import RequestDataAccess + +log = logging.getLogger(__name__) + + +def _access_request_notification(request_id): + """ + Send notification to admin when a new data access request is submitted + """ + site_title = tk.config.get("ckan.site_title") + site_url = tk.config.get("ckan.site_url") + datetime_now = datetime.datetime.now().strftime("%d/%m/%y %H:%M:%S") + + body_html = f""" +

Dear Admin,

+

A new submission from the Access Our Services form has been received.

+

Please check here to action the request.

+

The data request was submitted on {datetime_now}.

+ +

+

Have a nice day.

+ +

--

+

Message sent by {site_title} ({site_url})

+

This is an automated message, please don't respond to this address.

+ """ + + subject = "New Data Services Enquiry Submitted" + emails = tk.config.get( + "ckanext.ubdc.access_request_email_notification_email_to", "" + ) + + for email in emails.split(" "): + try: + mail_recipient("", email, subject, body="", body_html=body_html) + log.info(f"Email sent to {email} for new data access request") + except Exception as e: + log.error(f"Error sending email to {email} for new data access request") + log.error(e) + + +def request_data_access_create(context, data_dict): + """ + Data access request action + :param first_name: first name + :param surname: surname + :param organization: organisation/institution name + :param email: email address + :param telephone: telephone number + :param country: country + :param summary_of_project: project or intended use of data + :param project_funding: project funding + :param funding_information: additional funding information + :param wish_to_use_data: wish to use data + :param supporting_doc: supporting doc file upload + """ + + if not context.get("for_view", False): + tk.check_access("request_data_access_create", context, data_dict) + + upload = uploader.get_uploader("forms") + + upload.update_data_dict( + data_dict, "document_url", "document_upload", "clear_upload" + ) + + data_dict, errors = validate( + data_dict, schema.request_data_access_base_schema(), context + ) + + if errors: + raise tk.ValidationError(errors) + + upload.upload(10) + + data = RequestDataAccess.create(data_dict) + _access_request_notification(data.id) + return data.as_dict() + + +def request_data_access_update(context, data_dict): + """ + Data access request update action + """ + tk.get_or_bust(data_dict, "id") + + upload = uploader.get_uploader("forms") + + tk.check_access("request_data_access_update", context, data_dict) + + upload.update_data_dict( + data_dict, "document_url", "document_upload", "clear_upload" + ) + + data_dict, errors = validate( + data_dict, schema.request_data_access_base_schema(), context + ) + + if errors: + raise tk.ValidationError(errors) + + upload.upload(10) + + data = RequestDataAccess.update(data_dict) + return data.as_dict() + + +def request_data_access_delete(context, data_dict): + """ + Data access request delete action + """ + tk.check_access("request_data_access_update", context, data_dict) + tk.get_or_bust(data_dict, "id") + RequestDataAccess.delete(data_dict["id"]) + return {"success": True} + + +@tk.side_effect_free +def request_data_access_list(context, data_dict): + """ + Get all data access requests + """ + tk.check_access("request_data_access_list", context, data_dict) + data_dict = [item.as_dict() for item in RequestDataAccess.get_all()] + return data_dict + + +@tk.side_effect_free +def request_data_access_show(context, data_dict): + """ + Get data access request by id + """ + tk.get_or_bust(data_dict, "id") + tk.check_access("request_data_access_show", context, data_dict) + + data_dict = RequestDataAccess.get_by_id(data_dict["id"]) + return data_dict.as_dict() diff --git a/ckanext/ubdc/logic/auth.py b/ckanext/ubdc/logic/auth.py new file mode 100644 index 0000000..5db81bb --- /dev/null +++ b/ckanext/ubdc/logic/auth.py @@ -0,0 +1,21 @@ +import ckan.plugins.toolkit as toolkit + + +def request_data_access_create(context, data_dict): + # sysadmins only + return {"success": False} + + +def request_data_access_list(context, data_dict): + # sysadmins only + return {"success": False} + + +def request_data_access_show(context, data_dict): + # sysadmins only + return {"success": False} + + +def request_data_access_update(context, data_dict): + # sysadmins only + return {"success": False} diff --git a/ckanext/ubdc/logic/schema.py b/ckanext/ubdc/logic/schema.py new file mode 100644 index 0000000..e026af5 --- /dev/null +++ b/ckanext/ubdc/logic/schema.py @@ -0,0 +1,38 @@ +import ckan.plugins.toolkit as toolkit + +if toolkit.check_ckan_version("2.10"): + unicode_safe = toolkit.get_validator("unicode_safe") +else: + unicode_safe = str + +not_empty = toolkit.get_validator("not_empty") +empty = toolkit.get_validator("empty") +if_empty_same_as = toolkit.get_validator("if_empty_same_as") +ignore_missing = toolkit.get_validator("ignore_missing") +ignore = toolkit.get_validator("ignore") +keep_extras = toolkit.get_validator("keep_extras") +boolean_validator = toolkit.get_validator("boolean_validator") +list_of_strings = toolkit.get_validator("list_of_strings") + + +def request_data_access_base_schema(): + schema = { + "id": [ignore_missing], + "first_name": [not_empty, unicode_safe], + "surname": [not_empty, unicode_safe], + "organization": [not_empty, unicode_safe], + "email": [not_empty, unicode_safe], + "telephone": [not_empty, unicode_safe], + "country": [not_empty, unicode_safe], + "summary_of_project": [not_empty, unicode_safe], + "project_funding": [not_empty, unicode_safe, boolean_validator], + "funding_information": [ignore_missing, unicode_safe], + "wish_to_use_data": [ignore_missing, list_of_strings], + "document_url": [ignore_missing, unicode_safe], + "deleted": [ignore_missing], + } + return schema + + +def request_data_access_schema(): + return request_data_access_base_schema() diff --git a/ckanext/ubdc/model/__init__.py b/ckanext/ubdc/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ckanext/ubdc/model/access_request.py b/ckanext/ubdc/model/access_request.py new file mode 100644 index 0000000..f612906 --- /dev/null +++ b/ckanext/ubdc/model/access_request.py @@ -0,0 +1,78 @@ +import datetime + +from sqlalchemy import types, Column, Table, desc +from sqlalchemy.dialects.postgresql import JSONB + +from ckan.model import meta +from ckan.model import core +from ckan.model import types as _types +from ckan.model import domain_object + + +log = __import__("logging").getLogger(__name__) + + +def setup(): + if request_data_access_table.exists(): + log.info("request_data_access table already exists") + return + else: + log.info("Creating request_data_access table") + request_data_access_table.create() + + +request_data_access_table = Table( + "request_data_access", + meta.metadata, + Column("id", types.UnicodeText, primary_key=True, default=_types.make_uuid), + Column("first_name", types.UnicodeText), + Column("surname", types.UnicodeText), + Column("organization", types.UnicodeText), + Column("email", types.UnicodeText), + Column("telephone", types.UnicodeText), + Column("country", types.UnicodeText), + Column("summary_of_project", types.UnicodeText), + Column("project_funding", types.UnicodeText), + Column("funding_information", types.UnicodeText), + Column("wish_to_use_data", types.ARRAY(types.UnicodeText)), + Column("document_url", types.UnicodeText), + Column("created", types.DateTime, default=datetime.datetime.utcnow), + Column("updated", types.DateTime, default=datetime.datetime.utcnow), + Column("deleted", types.Boolean, default=False), +) + + +class RequestDataAccess(core.StatefulObjectMixin, domain_object.DomainObject): + @classmethod + def get_by_id(cls, id): + return meta.Session.query(cls).filter(cls.id == id).first() + + @classmethod + def get_all(cls): + return meta.Session.query(cls).order_by(desc(cls.created)).all() + + @classmethod + def create(cls, data_dict): + data = cls(**data_dict) + meta.Session.add(data) + meta.Session.commit() + return data + + @classmethod + def delete(cls, id): + data = cls.get_by_id(id) + if data: + meta.Session.delete(data) + meta.Session.commit() + return data + + @classmethod + def update(cls, data_dict): + data = cls.get_by_id(data_dict["id"]) + if data: + meta.Session.query(cls).filter(cls.id == data_dict["id"]).update(data_dict) + meta.Session.commit() + return data + + +meta.mapper(RequestDataAccess, request_data_access_table) diff --git a/ckanext/ubdc/plugin.py b/ckanext/ubdc/plugin.py index ebef307..ab96a21 100644 --- a/ckanext/ubdc/plugin.py +++ b/ckanext/ubdc/plugin.py @@ -5,53 +5,83 @@ from ckan.lib.plugins import DefaultTranslation from ckanext.ubdc.view import get_blueprints from ckanext.ubdc.validators import resource_type_validator +from ckanext.ubdc.logic import action +from ckanext.ubdc.logic import auth +from ckanext.ubdc.model import access_request class UbdcPlugin(plugins.SingletonPlugin, DefaultTranslation): plugins.implements(plugins.IConfigurer) + plugins.implements(plugins.IConfigurable) plugins.implements(plugins.ITranslation) plugins.implements(plugins.ITemplateHelpers) plugins.implements(plugins.IValidators) plugins.implements(plugins.IBlueprint) + plugins.implements(plugins.IActions) plugins.implements(plugins.IPackageController, inherit=True) + plugins.implements(plugins.IAuthFunctions) # IConfigurer def update_config(self, config_): - toolkit.add_template_directory(config_, 'templates') - toolkit.add_public_directory(config_, 'public') - toolkit.add_resource('assets', 'ubdc') + toolkit.add_template_directory(config_, "templates") + toolkit.add_public_directory(config_, "public") + toolkit.add_resource("assets", "ubdc") + + def configure(self, config): + access_request.setup() def update_config_schema(self, schema): - ignore_missing = toolkit.get_validator('ignore_missing') - schema.update({ - 'ckanext.ubdc.gtm_id': [ignore_missing], - }) + ignore_missing = toolkit.get_validator("ignore_missing") + schema.update( + { + "ckanext.ubdc.gtm_id": [ignore_missing], + } + ) return schema - - + + # IActions + def get_actions(self): + return { + "request_data_access_create": action.request_data_access_create, + "request_data_access_list": action.request_data_access_list, + "request_data_access_show": action.request_data_access_show, + "request_data_access_update": action.request_data_access_update, + "request_data_access_delete": action.request_data_access_delete, + } + # IPackageController def before_index(self, data_dict): - if data_dict.get('data_schema', False): + if data_dict.get("data_schema", False): # convert the dict to json - data_dict['data_schema'] = json.dumps(data_dict['data_schema']) + data_dict["data_schema"] = json.dumps(data_dict["data_schema"]) return data_dict - # ITemplateHelpers def get_helpers(self): return { - 'popular_datasets': helpers.popular_datasets, - 'resources_count': helpers.resources_count, - 'tags_count': helpers.tags_count, - 'get_gtm_id': helpers.get_gtm_id, - 'get_cookie_control_config': helpers.get_cookie_control_config + "popular_datasets": helpers.popular_datasets, + "resources_count": helpers.resources_count, + "tags_count": helpers.tags_count, + "get_gtm_id": helpers.get_gtm_id, + "get_cookie_control_config": helpers.get_cookie_control_config, + "get_field_to_question": helpers.get_field_to_question, } + # IValidators def get_validators(self): return { - 'resource_type_validator': resource_type_validator, + "resource_type_validator": resource_type_validator, + } + + # IAuthFunctions + def get_auth_functions(self): + return { + "request_data_access_list": auth.request_data_access_list, + "request_data_access_show": auth.request_data_access_show, + "request_data_access_update": auth.request_data_access_update, + "request_data_access_create": auth.request_data_access_create, } # IBlueprint def get_blueprint(self): - return get_blueprints() \ No newline at end of file + return get_blueprints() diff --git a/ckanext/ubdc/public/base/scss/_access_request.scss b/ckanext/ubdc/public/base/scss/_access_request.scss new file mode 100644 index 0000000..a03d0d7 --- /dev/null +++ b/ckanext/ubdc/public/base/scss/_access_request.scss @@ -0,0 +1,56 @@ + +.access-request-response { + position: relative; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + margin-bottom: 15px; + .response { + margin: 0; + font-size: 16px; + padding: 10px 20px; + color: #777; + background-color: #f7f7f7; + border-bottom: 1px solid #eee; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + + } + p { + padding: 20px; + } +} + +.checkboxes { + label { + display: flex; + font-weight: 400; + input { + margin: 0; + margin-right: 8px; + } + &::after { + content: ''; + display: block; + clear: both; + } + } +} +.radio-group { + label { + display: flex; + input { + margin: 0; + margin-right: 8px; + } + } +} + +.access-request-container { + display: flex; + justify-content: flex-end; + margin: 10px; + a { + font-size: 14px; + } +} \ No newline at end of file diff --git a/ckanext/ubdc/public/base/scss/_ckan.scss b/ckanext/ubdc/public/base/scss/_ckan.scss index 0f7c133..78c6bcf 100644 --- a/ckanext/ubdc/public/base/scss/_ckan.scss +++ b/ckanext/ubdc/public/base/scss/_ckan.scss @@ -23,6 +23,7 @@ @import "resource-view.scss"; @import "datapusher.scss"; @import "alerts.scss"; +@import "access_request.scss"; @import "input-groups.scss"; body { diff --git a/ckanext/ubdc/public/base/scss/_custom.scss b/ckanext/ubdc/public/base/scss/_custom.scss index 29ff7a3..41c1435 100644 --- a/ckanext/ubdc/public/base/scss/_custom.scss +++ b/ckanext/ubdc/public/base/scss/_custom.scss @@ -18,6 +18,10 @@ overflow: hidden; } +input[type=checkbox][name="contactConsent"]:focus, +input[type=checkbox][name="consentName"]:focus { + outline: 1px solid lightblue; +} .beta-message { background: #d5580d; color: #fff; diff --git a/ckanext/ubdc/templates/access_form/index.html b/ckanext/ubdc/templates/access_form/index.html new file mode 100644 index 0000000..ee44823 --- /dev/null +++ b/ckanext/ubdc/templates/access_form/index.html @@ -0,0 +1,149 @@ +{% extends "page.html" %} +{% import "macros/form.html" as form %} +{% set wish_list = [ "BGS' Accessing Subsurface Knowledge (ASK) Data", "Cycling Scotland Data", "Goad Plan Experian +Data", "Glasgow Valuation Roll Data", "Huq Data", "iMCD GPS Data", "iMCD Household Survey Data", "iMCD Lifelogging +Data", "iMCD Travel Diary Data", "iMCD Twitter Data", "LiDAR Data", "Lothian Valuation Roll Data", "Met Office Forecasts +Data", "Met Office Hourly Observations Data", "Mobility metrics for Glasgow City Region", "Nestoria Property Data", +"Nightlight Data", "Public Transport Accessibility Indicators Data 2022", "Public Transport Availability Indicators +Data", "Registers of Scotland Data", "Springboard's Footfall Data", "Strava (Glasgow) Data", "Strava (Manchester) Data", +"Strava (Sheffield) Data", "Strava (Scotland) Data", "Strava (Tyne & Wear) Data", "SUDS / Urban Indicators Data", +"Tamoco Data", "Zoopla Property Data" ]%} +{% block subtitle %}{{ _('Register') }}{% endblock %} + +{% block breadcrumb_content %} +
  • {{ h.nav_link(_('Data service access request'), named_route='ubdc.access_request') }}
  • + +{% endblock %} + +{% block primary_content %} +
    +
    + {% block primary_content_inner %} +

    + {% block page_heading %}{{ _('If you are ready to use our Data Services, please complete the UBDC + Application Form below.') }}{% endblock %} +

    +

    We welcome applications to use our Data Collections all year round. If you only need open data, you can have + immediate access to our Open Data Collection.

    + +

    Following submission, we will review your application and contact you within two weeks.

    + +

    Please note: If you choose to request access to data you will need to pass personal data to the Centre + through this web form. This data will be used only for the service you have requested unless you consent + otherwise at the time of submitting the data. For full details, view our Privacy Policy.

    + +
    + {{ form.errors(error_summary) }} + + {{ form.input("first_name", id="field-first_name", label=_("First Name"), + value=data.first_name, error=errors.first_name, classes=["control-medium"], is_required=True) }} + + {{ form.input("surname", id="field-surname", label=_("Surname"), + value=data.surname, error=errors.surname, classes=["control-medium"], is_required=True) }} + + {{ form.input("organization", id="field-organization", label=_("Organization / Institution"), + value=data.organization, error=errors.organization, + classes=["control-medium"], is_required=True) }} + + {{ form.input("email", id="field-email", label=_("Email"), type="email", + value=data.email, error=errors.email, classes=["control-medium"], is_required=True) }} + + {{ form.input("telephone", id="field-telephone", label=_("Telephone Number"), value=data.telephone, error=errors.telephone, classes=["control-medium"], is_required=True) }} + + {{ form.input("country", id="field-country", label=_("Country"), + value=data.country,error=errors.country, classes=["control-medium"], is_required=True) }} + + {{ form.textarea("summary_of_project", id="field-summary_of_project", label=_("Summary of your project or + intended use of UBDCdata and/or services"), value=data.summary_of_project, error=errors.summary_of_project, + classes=["control-medium"], is_required=True) }} + +
    +
    + + +
    + {%- for option in ["Yes", "No" ] -%} +
    + +
    + {%- endfor -%} +
    +
    + +
    + {{ form.textarea("funding_information", id="field-funding_information", + label=_("If yes, please provide additional information (e.g. deadline) and/or a link to the Funding + Call"), value=data.funding_information, + error=errors.funding_information, classes=["control-medium"]) }} +
    + +
    + +
    +
    + +
    + {% for wish in wish_list %} +

    + +

    + {% endfor %} +
    +
    +
    + + {% set is_upload = data.image_url and not data.image_url.startswith('http') %} + {% set is_url = data.image_url and data.image_url.startswith('http') %} + + {{ form.image_upload(data, errors, is_upload_enabled=h.uploads_enabled(), is_url=is_url, + is_upload=is_upload,field_url='document_url', field_upload='document_upload', field_clear='clear_upload', + upload_label=_('Optional supporting documentation upload (PDF or DOC only)'), + url_label=_('Optional supporting documentation URL')) }} + + {{ form.checkbox('consent', label=_('By ticking this box, you agree that i have read privacy cookies'), id='field-consent', value="true", attrs={'required': 'true'} ) }} + {{ form.checkbox('contactConsent', label=_('By ticking this box, I consent to being contacted about events.'), id='field-event-consent', value="true", attrs={'required': 'true'} ) }} +
    + {% block form_actions %} + {% if g.recaptcha_publickey %} + + {% else %} + + {% endif %} + {% endblock %} +
    + +
    + + + {% endblock %} +
    +
    +{% endblock %} + +{% block secondary_content %} +{% block help %} +
    + {% block help_inner %} +

    {{ _('Access our services') }}

    +
    +

    {% trans %}   {% endtrans %}

    +
    + {% endblock %} +
    +{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/ckanext/ubdc/templates/access_form/list.html b/ckanext/ubdc/templates/access_form/list.html new file mode 100644 index 0000000..98fd598 --- /dev/null +++ b/ckanext/ubdc/templates/access_form/list.html @@ -0,0 +1,62 @@ +{% extends "page.html" %} +{% import "macros/form.html" as form %} + +{% block subtitle %}{{ _('Register') }}{% endblock %} + +{% block breadcrumb_content %} +
  • {{ h.nav_link(_('Registration'), named_route='user.register') }}
  • +{% endblock %} + +{% block primary_content %} +
    +
    + {% block primary_content_inner %} +

    + {% block page_heading %}{{ _('Data Service Access Request Responses') }}{% endblock %} +

    + + + + + + + + + + + + {% for item in data %} + + + + + + + + {% endfor %} + +
    {{ _('Name') }}{{ _('Email') }}{{ _('Organization ') }} {{ _('Submitted on') }}
    {{ item.first_name }} {{ item.surname }}{{ item.email }}{{ item.organization }}{{ h.time_ago_from_timestamp(item.created) }} + View + Delete +
    + + + + + {% endblock %} +
    +
    +{% endblock %} + +{% block secondary_content %} +{% block help %} +
    + {% block help_inner %} +

    {{ _('Access our services') }}

    +
    +

    {% trans %}   {% endtrans %}

    +
    + {% endblock %} +
    +{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/ckanext/ubdc/templates/access_form/view.html b/ckanext/ubdc/templates/access_form/view.html new file mode 100644 index 0000000..5f7c157 --- /dev/null +++ b/ckanext/ubdc/templates/access_form/view.html @@ -0,0 +1,55 @@ +{% extends "page.html" %} +{% import "macros/form.html" as form %} + +{% block subtitle %}{{ _('Register') }}{% endblock %} + +{% block breadcrumb_content %} +
  • {{ h.nav_link(_('Registration'), named_route='user.register') }}
  • +{% endblock %} + +{% block primary_content %} +
    +
    + {% block primary_content_inner %} +

    + {% block page_heading %}{{ _('Data Service Access Request Responses') }}{% endblock %} +

    + {% for key, value in data.items() %} +
    +

    {{ h.get_field_to_question(key) }}

    + {% if key == "wish_to_use_data" and value %} + {% for item in value %} +
      +
    • {{ item }}
    • +
    + {% endfor %} + {% elif key == "created" %} +

    + {{ value }} +

    + {% elif key == "document_url" %} +

    {{ value }}

    + {% elif key == "project_funding" %} +

    {{ 'Yes' if value else 'No' }}

    + {% else %} +

    {{ value }}

    + {% endif %} +
    + {% endfor %} + {% endblock %} +
    +
    +{% endblock %} + +{% block secondary_content %} +{% block help %} +
    + {% block help_inner %} +

    {{ _('Access our services') }}

    +
    +

    {% trans %}   {% endtrans %}

    +
    + {% endblock %} +
    +{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/ckanext/ubdc/templates/header.html b/ckanext/ubdc/templates/header.html index deae06f..1a03b31 100644 --- a/ckanext/ubdc/templates/header.html +++ b/ckanext/ubdc/templates/header.html @@ -10,4 +10,13 @@ -{% endblock %} \ No newline at end of file +{% endblock %} + +{% block header_account_logged %} +
  • + + + +
  • + {{ super() }} +{% endblock %} diff --git a/ckanext/ubdc/templates/scheming/package/read.html b/ckanext/ubdc/templates/scheming/package/read.html new file mode 100644 index 0000000..6193d5d --- /dev/null +++ b/ckanext/ubdc/templates/scheming/package/read.html @@ -0,0 +1,15 @@ +{% ckan_extends %} + +{% block package_notes %} +
    + + + {{ _("Access to the data") }} + +
    + {% if pkg.notes %} +
    + {{ h.render_markdown(h.get_translated(pkg, 'notes')) }} +
    + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/ckanext/ubdc/templates/user/new_user_form.html b/ckanext/ubdc/templates/user/new_user_form.html new file mode 100644 index 0000000..d52fd86 --- /dev/null +++ b/ckanext/ubdc/templates/user/new_user_form.html @@ -0,0 +1,23 @@ +{% import "macros/form.html" as form %} + +
    + {{ form.errors(error_summary) }} + {{ form.input("name", id="field-username", label=_("Username"), placeholder=_("username"), value=data.name, error=errors.name, classes=["control-medium"], is_required=True) }} + {{ form.input("fullname", id="field-fullname", label=_("Full Name"), placeholder=_("Joe Bloggs"), value=data.fullname, error=errors.fullname, classes=["control-medium"]) }} + {{ form.input("email", id="field-email", label=_("Email"), type="email", placeholder=_("joe@example.com"), value=data.email, error=errors.email, classes=["control-medium"], is_required=True) }} + {{ form.input("password1", id="field-password", label=_("Password"), type="password", placeholder="••••••••", value=data.password1, error=errors.password1, classes=["control-medium"], is_required=True) }} + {{ form.input("password2", id="field-confirm-password", label=_("Confirm"), type="password", placeholder="••••••••", value=data.password2, error=errors.password2, classes=["control-medium"], is_required=True) }} + + {% set is_upload = data.image_url and not data.image_url.startswith('http') %} + {% set is_url = data.image_url and data.image_url.startswith('http') %} + + {{ form.image_upload(data, errors, is_upload_enabled=h.uploads_enabled(), is_url=is_url, is_upload=is_upload, upload_label=_('Profile picture'), url_label=_('Profile picture URL')) }} + + {{ form.required_message() }} + +
    + {% block form_actions %} + + {% endblock %} +
    +
    diff --git a/ckanext/ubdc/validators.py b/ckanext/ubdc/validators.py index 55516ce..d01ff88 100644 --- a/ckanext/ubdc/validators.py +++ b/ckanext/ubdc/validators.py @@ -1,4 +1,3 @@ - import ckan.plugins.toolkit as tk Invalid = tk.Invalid @@ -7,5 +6,7 @@ def resource_type_validator(value, context): if value not in ["data_and_metadata", "resource"]: - raise Invalid(_(' Resource type must be either "data_and_metadata" or "resource"')) + raise Invalid( + _('Resource type must be either "data_and_metadata" or "resource"') + ) return value diff --git a/ckanext/ubdc/view.py b/ckanext/ubdc/view.py index 1c741e4..bee684a 100644 --- a/ckanext/ubdc/view.py +++ b/ckanext/ubdc/view.py @@ -1,26 +1,160 @@ +from crypt import methods import logging +from flask.views import MethodView +import ckan.logic as logic from flask import Blueprint, redirect - +import ckan.model as model +from ckan.common import asbool from ckan.plugins import toolkit as tk from ckan.views.group import register_group_plugin_rules +import ckan.lib.navl.dictization_functions as dict_fns +import ckan.lib.captcha as captcha + +tuplize_dict = logic.tuplize_dict +clean_dict = logic.clean_dict +parse_params = logic.parse_params + log = logging.getLogger(__name__) -udbc = Blueprint('udbc', __name__) +ubdc = Blueprint("ubdc", __name__) # Redirect organization pages to provider pages -provider = Blueprint('provider', __name__, url_prefix='/provider', - url_defaults={'group_type': 'organization', 'is_organization': True}) +provider = Blueprint( + "provider", + __name__, + url_prefix="/provider", + url_defaults={"group_type": "organization", "is_organization": True}, +) + -@udbc.route('/organization/') +@ubdc.route("/organization/") def org_redirect_root(): - return redirect('/provider') + return redirect("/provider") -@udbc.route('/organization/') + +@ubdc.route("/organization/") def org_redirect(path): - return redirect('/provider/{}'.format(path)) + return redirect("/provider/{}".format(path)) + + +class AccessRequestController(MethodView): + def get(self, data={}, errors={}, error_summary={}): + extra_vars = {"data": data, "errors": errors, "error_summary": error_summary} + return tk.render("access_form/index.html", extra_vars) + + def post(self): + context = {"model": model, "user": tk.c.user, "auth_user_obj": tk.c.userobj} + + data_dict = clean_dict( + dict_fns.unflatten(tuplize_dict(parse_params(tk.request.form))) + ) + + if not asbool(data_dict.get("consent", False)): + error_msg = tk._("Please accept the terms and conditions.") + tk.h.flash_error(error_msg) + return self.get() + + if not asbool(data_dict.get("contactConsent", False)): + error_msg = tk._("Please accept the contact consent.") + tk.h.flash_error(error_msg) + return self.get() + try: + captcha.check_recaptcha(tk.request) + except captcha.CaptchaError: + error_msg = tk._("Bad Captcha. Please try again.") + tk.h.flash_error(error_msg) + return self.get() + + try: + data_dict["document_upload"] = tk.request.files.get("document_upload") + data_dict["project_funding"] = asbool( + data_dict.get("project_funding", False) + ) + if data_dict.get("wish_to_use_data", False): + if not isinstance(data_dict["wish_to_use_data"], list): + data_dict["wish_to_use_data"] = [ + data_dict["wish_to_use_data"], + ] + context["for_view"] = True + + tk.get_action("request_data_access_create")(context, data_dict) + except logic.ValidationError as e: + errors = e.error_dict + error_summary = e.error_summary + return self.get(data_dict, errors, error_summary) + + tk.h.flash_success( + "Your application has been successfully submitted, we will review your request and get back to you soon" + ) + + return tk.redirect_to("ubdc.access_request") + + +ubdc.add_url_rule( + "/data-service/access-request", + view_func=AccessRequestController.as_view("access_request"), +) + + +def access_request_list(): + context = {"model": model, "user": tk.c.user, "auth_user_obj": tk.c.userobj} + try: + result = tk.get_action("request_data_access_list")(context, {}) + except logic.NotAuthorized: + tk.abort(403, tk._("Not authorized to see this page")) + + extra_vars = { + "data": result, + "errors": {}, + "error_summary": {}, + } + return tk.render("access_form/list.html", extra_vars) + + +def access_request_view(id): + context = {"model": model, "user": tk.c.user, "auth_user_obj": tk.c.userobj} + try: + result = tk.get_action("request_data_access_show")(context, {"id": id}) + except logic.NotAuthorized: + tk.abort(403, tk._("Not authorized to see this page")) + + keys_to_exclude = ["id", "updated", "deleted"] + + result = {key: value for key, value in result.items() if key not in keys_to_exclude} + extra_vars = { + "data": result, + "errors": {}, + "error_summary": {}, + } + return tk.render("access_form/view.html", extra_vars) + + +def access_request_delete(id): + context = {"model": model, "user": tk.c.user, "auth_user_obj": tk.c.userobj} + try: + result = tk.get_action("request_data_access_delete")(context, {"id": id}) + except logic.ValidationError as e: + tk.abort(404, e.error_dict["message"]) + + tk.h.flash_success("Access request deleted") + return tk.redirect_to("ubdc.access_request_list") + + +ubdc.add_url_rule("/data-service/access-request/view", view_func=access_request_list) + + +ubdc.add_url_rule( + "/data-service/access-request/view/", view_func=access_request_view +) + +ubdc.add_url_rule( + "/data-service/access-request/view//delete", + methods=["POST"], + view_func=access_request_delete, +) def get_blueprints(): register_group_plugin_rules(provider) - return [udbc, provider] \ No newline at end of file + return [ubdc, provider]