From e9823b512bfc5f40b029e3fddfc950c72447160d Mon Sep 17 00:00:00 2001 From: Awang Date: Wed, 31 Jul 2024 13:54:29 +0700 Subject: [PATCH 01/15] [QCDP24-16 QCDP24-24] added create data request form and its listing (#3) --- ckanext/datarequests/actions.py | 65 ++++++++----- ckanext/datarequests/auth.py | 23 ++++- ckanext/datarequests/constants.py | 1 + .../controllers/controller_functions.py | 74 ++++++++++++--- ckanext/datarequests/db.py | 67 ++++++++++++-- .../templates/datarequests/base.html | 4 +- .../templates/datarequests/edit.html | 2 +- .../templates/datarequests/index.html | 13 +-- .../templates/datarequests/new.html | 22 ++++- .../snippets/datarequest_form.html | 91 +++++++++++++++---- .../templates/organization/datarequests.html | 5 - .../templates/package/read_base.html | 6 ++ ckanext/datarequests/validator.py | 75 ++++++++++++--- 13 files changed, 354 insertions(+), 94 deletions(-) create mode 100644 ckanext/datarequests/templates/package/read_base.html diff --git a/ckanext/datarequests/actions.py b/ckanext/datarequests/actions.py index 284211c7..925c9202 100644 --- a/ckanext/datarequests/actions.py +++ b/ckanext/datarequests/actions.py @@ -94,7 +94,14 @@ def _dictize_datarequest(datarequest): 'user': _get_user(datarequest.user_id), 'organization': None, 'accepted_dataset': None, - 'followers': 0 + 'followers': 0, + 'data_use_type': datarequest.data_use_type, + 'who_will_access_this_data': datarequest.who_will_access_this_data, + 'requesting_organisation': datarequest.requesting_organisation, + 'data_storage_environment': datarequest.data_storage_environment, + 'data_outputs_type': datarequest.data_outputs_type, + 'data_outputs_description': datarequest.data_outputs_description, + 'status': datarequest.status } if datarequest.organization_id: @@ -120,6 +127,14 @@ def _undictize_datarequest_basic(datarequest, data_dict): datarequest.organization_id = organization if organization else None _undictize_datarequest_closing_circumstances(datarequest, data_dict) + datarequest.data_use_type = data_dict['data_use_type'] + datarequest.who_will_access_this_data = data_dict['who_will_access_this_data'] + datarequest.requesting_organisation = data_dict['requesting_organisation'] + datarequest.data_storage_environment = data_dict['data_storage_environment'] + datarequest.data_outputs_type = data_dict['data_outputs_type'] + datarequest.data_outputs_description = data_dict['data_outputs_description'] + datarequest.status = data_dict['status'] + def _undictize_datarequest_closing_circumstances(datarequest, data_dict): if h.closing_circumstances_enabled: @@ -460,8 +475,8 @@ def list_datarequests(context, data_dict): # Get user ID (user name is received sometimes) user_id = user_show({'ignore_auth': True}, {'id': user_id}).get('id') - # Filter by state - closed = data_dict.get('closed', None) + # Filter by status + status = data_dict.get('status', None) # Free text filter q = data_dict.get('q', None) @@ -475,7 +490,7 @@ def list_datarequests(context, data_dict): # Call the function db_datarequests = db.DataRequest.get_ordered_by_date(organization_id=organization_id, - user_id=user_id, closed=closed, + user_id=user_id, status=status, q=q, desc=desc) # Dictize the results @@ -487,18 +502,22 @@ def list_datarequests(context, data_dict): # Facets no_processed_organization_facet = {} - CLOSED = 'Closed' - OPEN = 'Open' - no_processed_state_facet = {CLOSED: 0, OPEN: 0} + no_processed_status_facet = { + 'Assigned': 0, + 'Processing': 0, + 'Finalised - Approved': 0, + 'Finalised - Not Approved': 0, + 'Assign to Internal Data Catalogue Support': 0 + } for data_req in db_datarequests: - if data_req.organization_id: - # Facets - if data_req.organization_id in no_processed_organization_facet: - no_processed_organization_facet[data_req.organization_id] += 1 - else: - no_processed_organization_facet[data_req.organization_id] = 1 + organization_id = data_req.organization_id + status = data_req.status + + if organization_id: + no_processed_organization_facet[organization_id] = no_processed_organization_facet.get(organization_id, 0) + 1 - no_processed_state_facet[CLOSED if data_req.closed else OPEN] += 1 + if status in no_processed_status_facet: + no_processed_status_facet[status] += 1 # Format facets organization_facet = [] @@ -513,13 +532,13 @@ def list_datarequests(context, data_dict): except Exception: pass - state_facet = [] - for state in no_processed_state_facet: - if no_processed_state_facet[state]: - state_facet.append({ - 'name': state.lower(), - 'display_name': tk._(state), - 'count': no_processed_state_facet[state] + status_facet = [] + for status in no_processed_status_facet: + if no_processed_status_facet[status]: + status_facet.append({ + 'name': status, + 'display_name': tk._(status), + 'count': no_processed_status_facet[status] }) result = { @@ -532,8 +551,8 @@ def list_datarequests(context, data_dict): if organization_facet: result['facets']['organization'] = {'items': organization_facet} - if state_facet: - result['facets']['state'] = {'items': state_facet} + if status_facet: + result['facets']['status'] = {'items': status_facet} return result diff --git a/ckanext/datarequests/auth.py b/ckanext/datarequests/auth.py index 2690be3b..827b937c 100644 --- a/ckanext/datarequests/auth.py +++ b/ckanext/datarequests/auth.py @@ -18,6 +18,7 @@ # along with CKAN Data Requests Extension. If not, see . from ckan import authz +from ckan.plugins.toolkit import current_user from ckan.plugins.toolkit import asbool, auth_allow_anonymous_access, config, get_action from . import constants @@ -53,8 +54,28 @@ def auth_if_creator(context, data_dict, show_function): return {'success': data_dict['user_id'] == context.get('auth_user_obj').id} +def auth_if_editor_or_admin(context, data_dict, show_function): + # Sometimes data_dict only contains the 'id' + if 'user_id' not in data_dict: + function = get_action(show_function) + data_dict = function({'ignore_auth': True}, {'id': data_dict.get('id')}) + + is_editor_or_admin = False + current_user_id = current_user.id if current_user else None + for user in data_dict['organization']['users']: + if user['id'] == current_user_id and user['capacity'] in ['editor', 'admin']: + is_editor_or_admin = True + break + + return {'success': is_editor_or_admin} + + def update_datarequest(context, data_dict): - return auth_if_creator(context, data_dict, constants.SHOW_DATAREQUEST) + is_current_creator = auth_if_creator(context, data_dict, constants.SHOW_DATAREQUEST) + if (is_current_creator['success'] is True): + return is_current_creator + + return auth_if_editor_or_admin(context, data_dict, constants.SHOW_DATAREQUEST) @auth_allow_anonymous_access diff --git a/ckanext/datarequests/constants.py b/ckanext/datarequests/constants.py index 37698200..dcbeddb2 100644 --- a/ckanext/datarequests/constants.py +++ b/ckanext/datarequests/constants.py @@ -37,3 +37,4 @@ COMMENT_MAX_LENGTH = DESCRIPTION_MAX_LENGTH DATAREQUESTS_PER_PAGE = 10 CLOSE_CIRCUMSTANCE_MAX_LENGTH = 255 +MAX_LENGTH_255 = 255 diff --git a/ckanext/datarequests/controllers/controller_functions.py b/ckanext/datarequests/controllers/controller_functions.py index e7ec72c0..0f5ca130 100644 --- a/ckanext/datarequests/controllers/controller_functions.py +++ b/ckanext/datarequests/controllers/controller_functions.py @@ -10,7 +10,7 @@ from ckan import model from ckan.lib import helpers, captcha from ckan.plugins import toolkit as tk -from ckan.plugins.toolkit import c, h, request, _ +from ckan.plugins.toolkit import c, h, request, _, current_user from ckanext.datarequests import constants, request_helpers @@ -59,14 +59,14 @@ def _get_context(): def _show_index(user_id, organization_id, include_organization_facet, url_func, file_to_render, extra_vars=None): - def pager_url(state=None, sort=None, q=None, page=None): + def pager_url(status=None, sort=None, q=None, page=None): params = [] if q: params.append(('q', q)) - if state is not None: - params.append(('state', state)) + if status is not None: + params.append(('status', status)) params.append(('sort', sort)) params.append(('page', page)) @@ -80,9 +80,9 @@ def pager_url(state=None, sort=None, q=None, page=None): offset = (page - 1) * constants.DATAREQUESTS_PER_PAGE data_dict = {'offset': offset, 'limit': limit} - state = request_helpers.get_first_query_param('state', None) - if state: - data_dict['closed'] = True if state == 'closed' else False + status = request_helpers.get_first_query_param('status', None) + if status: + data_dict['status'] = status q = request_helpers.get_first_query_param('q', '') if q: @@ -106,19 +106,19 @@ def pager_url(state=None, sort=None, q=None, page=None): c.sort = sort c.q = q c.organization = organization_id - c.state = state + c.status = status c.datarequest_count = datarequests_list['count'] c.datarequests = datarequests_list['result'] c.search_facets = datarequests_list['facets'] c.page = helpers.Page( collection=datarequests_list['result'], page=page, - url=functools.partial(pager_url, state, sort), + url=functools.partial(pager_url, status, sort), item_count=datarequests_list['count'], items_per_page=limit ) c.facet_titles = { - 'state': tk._('State'), + 'status': tk._('Status'), } # Organization facet cannot be shown when the user is viewing an org @@ -131,7 +131,7 @@ def pager_url(state=None, sort=None, q=None, page=None): extra_vars['sort'] = c.sort extra_vars['q'] = c.q extra_vars['organization'] = c.organization - extra_vars['state'] = c.state + extra_vars['status'] = c.status extra_vars['datarequest_count'] = c.datarequest_count extra_vars['datarequests'] = c.datarequests extra_vars['search_facets'] = c.search_facets @@ -165,6 +165,14 @@ def _process_post(action, context): data_dict['description'] = request_helpers.get_first_post_param('description', '') data_dict['organization_id'] = request_helpers.get_first_post_param('organization_id', '') + data_dict['data_use_type'] = request_helpers.get_first_post_param('data_use_type', '') + data_dict['who_will_access_this_data'] = request_helpers.get_first_post_param('who_will_access_this_data', '') + data_dict['requesting_organisation'] = request_helpers.get_first_post_param('requesting_organisation', '') + data_dict['data_storage_environment'] = request_helpers.get_first_post_param('data_storage_environment', '') + data_dict['data_outputs_type'] = request_helpers.get_first_post_param('data_outputs_type', '') + data_dict['data_outputs_description'] = request_helpers.get_first_post_param('data_outputs_description', '') + data_dict['status'] = request_helpers.get_first_post_param('status', '') + if action == constants.UPDATE_DATAREQUEST: data_dict['id'] = request_helpers.get_first_post_param('id', '') @@ -180,7 +188,14 @@ def _process_post(action, context): 'id': data_dict.get('id', ''), 'title': data_dict.get('title', ''), 'description': data_dict.get('description', ''), - 'organization_id': data_dict.get('organization_id', '') + 'organization_id': data_dict.get('organization_id', ''), + 'data_use_type': data_dict.get('data_use_type', ''), + 'who_will_access_this_data': data_dict.get('who_will_access_this_data', ''), + 'requesting_organisation': data_dict.get('requesting_organisation', ''), + 'data_storage_environment': data_dict.get('data_storage_environment', ''), + 'data_outputs_type': data_dict.get('data_outputs_type', ''), + 'data_outputs_description': data_dict.get('data_outputs_description', ''), + 'status': data_dict.get('status', '') } c.errors = e.error_dict c.errors_summary = _get_errors_summary(c.errors) @@ -192,7 +207,14 @@ def _process_post(action, context): 'id': data_dict.get('id', ''), 'title': data_dict.get('title', ''), 'description': data_dict.get('description', ''), - 'organization_id': data_dict.get('organization_id', '') + 'organization_id': data_dict.get('organization_id', ''), + 'data_use_type': data_dict.get('data_use_type', ''), + 'who_will_access_this_data': data_dict.get('who_will_access_this_data', ''), + 'requesting_organisation': data_dict.get('requesting_organisation', ''), + 'data_storage_environment': data_dict.get('data_storage_environment', ''), + 'data_outputs_type': data_dict.get('data_outputs_type', ''), + 'data_outputs_description': data_dict.get('data_outputs_description', ''), + 'status': data_dict.get('status', '') } @@ -203,11 +225,23 @@ def new(): c.datarequest = {} c.errors = {} c.errors_summary = {} + c.requesting_organisation_options = [] # Check access try: tk.check_access(constants.CREATE_DATAREQUEST, context, None) post_result = _process_post(constants.CREATE_DATAREQUEST, context) + + dataset_id = request.args.get('id') + if dataset_id: + dataset = tk.get_action('package_show')(context, {'id': dataset_id}) + c.datarequest['title'] = dataset.get('title', '') + c.datarequest['organization_id'] = dataset.get('organization', {}).get('id') + + # Get organizations, with empty value for first option + organizations = h.organizations_available('read') + c.requesting_organisation_options = [{'value': '', 'text': ''}] + [{'value': org['id'], 'text': org['name']} for org in organizations] + return post_result or tk.render('datarequests/new.html') except tk.NotAuthorized as e: log.warning(e) @@ -241,12 +275,26 @@ def update(id): c.datarequest = {} c.errors = {} c.errors_summary = {} + c.requesting_organisation_options = [] + c.access_to_status_field = True if current_user.sysadmin else False try: tk.check_access(constants.UPDATE_DATAREQUEST, context, data_dict) c.datarequest = tk.get_action(constants.SHOW_DATAREQUEST)(context, data_dict) c.original_title = c.datarequest.get('title') post_result = _process_post(constants.UPDATE_DATAREQUEST, context) + + # Get organizations, with empty value for first option + organizations = h.organizations_available('read') + c.requesting_organisation_options = [{'value': '', 'text': ''}] + [{'value': org['id'], 'text': org['name']} for org in organizations] + + current_user_id = current_user.id if current_user else None + if c.datarequest.get('organization') is not None: + for user in c.datarequest['organization'].get('users', []): + if user['id'] == current_user_id and user['capacity'] in ['editor', 'admin']: + c.access_to_status_field = True + break + return post_result or tk.render('datarequests/edit.html') except tk.ObjectNotFound as e: log.warning(e) diff --git a/ckanext/datarequests/db.py b/ckanext/datarequests/db.py index d1b6e7fe..cc530512 100644 --- a/ckanext/datarequests/db.py +++ b/ckanext/datarequests/db.py @@ -22,9 +22,11 @@ import logging from ckan import model +from ckan.plugins.toolkit import current_user from ckanext.datarequests import constants from sqlalchemy import func, MetaData, DDL +from sqlalchemy.sql import case from sqlalchemy.sql.expression import or_ from . import common @@ -51,7 +53,7 @@ def datarequest_exists(cls, title): return query.filter(func.lower(cls.title) == func.lower(title)).first() is not None @classmethod - def get_ordered_by_date(cls, organization_id=None, user_id=None, closed=None, q=None, desc=False): + def get_ordered_by_date(cls, organization_id=None, user_id=None, closed=None, q=None, desc=False, status=None): '''Personalized query''' query = model.Session.query(cls).autoflush(False) @@ -66,13 +68,30 @@ def get_ordered_by_date(cls, organization_id=None, user_id=None, closed=None, q= if closed is not None: params['closed'] = closed + if status is not None: + params['status'] = status + if q is not None: search_expr = '%{0}%'.format(q) query = query.filter(or_(cls.title.ilike(search_expr), cls.description.ilike(search_expr))) + query = query.filter_by(**params) + order_by_filter = cls.open_time.desc() if desc else cls.open_time.asc() - return query.filter_by(**params).order_by(order_by_filter).all() + current_user_id = current_user.id if current_user else None + + if current_user_id: + current_user_order = case( + [(cls.user_id == current_user_id, 1)], + else_=0 + ).label('current_user_order') + + query = query.order_by(current_user_order.desc(), order_by_filter) + else: + query = query.order_by(order_by_filter) + + return query.all() @classmethod def get_open_datarequests_number(cls): @@ -132,11 +151,16 @@ def get_datarequest_followers_number(cls, **kw): sa.Column('accepted_dataset_id', sa.types.UnicodeText, primary_key=False, default=None), sa.Column('close_time', sa.types.DateTime, primary_key=False, default=None), sa.Column('closed', sa.types.Boolean, primary_key=False, default=False), - sa.Column('close_circumstance', sa.types.Unicode(constants.CLOSE_CIRCUMSTANCE_MAX_LENGTH), primary_key=False, default=u'') - if closing_circumstances_enabled else None, - sa.Column('approx_publishing_date', sa.types.DateTime, primary_key=False, default=None) - if closing_circumstances_enabled else None, - extend_existing=True, + sa.Column('close_circumstance', sa.types.Unicode(constants.CLOSE_CIRCUMSTANCE_MAX_LENGTH), primary_key=False, default=u'') if closing_circumstances_enabled else None, + sa.Column('approx_publishing_date', sa.types.DateTime, primary_key=False, default=None) if closing_circumstances_enabled else None, + sa.Column('data_use_type', sa.types.Unicode(constants.MAX_LENGTH_255), primary_key=False, default=u''), + sa.Column('who_will_access_this_data', sa.types.Unicode(constants.DESCRIPTION_MAX_LENGTH), primary_key=False, default=u''), + sa.Column('requesting_organisation', sa.types.Unicode(constants.MAX_LENGTH_255), primary_key=False, default=u''), + sa.Column('data_storage_environment', sa.types.Unicode(constants.DESCRIPTION_MAX_LENGTH), primary_key=False, default=u''), + sa.Column('data_outputs_type', sa.types.Unicode(constants.MAX_LENGTH_255), primary_key=False, default=u''), + sa.Column('data_outputs_description', sa.types.Unicode(constants.DESCRIPTION_MAX_LENGTH), primary_key=False, default=u''), + sa.Column('status', sa.types.Unicode(constants.MAX_LENGTH_255), primary_key=False, default=u'Assigned'), + extend_existing=True ) model.meta.mapper(DataRequest, datarequests_table) @@ -198,3 +222,32 @@ def update_db(deprecated_model=None): if 'approx_publishing_date' not in meta.tables['datarequests'].columns: log.info("DataRequests-UpdateDB: 'approx_publishing_date' field does not exist, adding...") DDL('ALTER TABLE "datarequests" ADD COLUMN "approx_publishing_date" timestamp NULL').execute(model.Session.get_bind()) + + if 'datarequests' in meta.tables: + if 'data_use_type' not in meta.tables['datarequests'].columns: + log.info("DataRequests-UpdateDB: 'data_use_type' field does not exist, adding...") + DDL('ALTER TABLE "datarequests" ADD COLUMN "data_use_type" varchar(255) NULL').execute(model.Session.get_bind()) + + if 'who_will_access_this_data' not in meta.tables['datarequests'].columns: + log.info("DataRequests-UpdateDB: 'who_will_access_this_data' field does not exist, adding...") + DDL('ALTER TABLE "datarequests" ADD COLUMN "who_will_access_this_data" character varying(1000) NULL').execute(model.Session.get_bind()) + + if 'requesting_organisation' not in meta.tables['datarequests'].columns: + log.info("DataRequests-UpdateDB: 'requesting_organisation' field does not exist, adding...") + DDL('ALTER TABLE "datarequests" ADD COLUMN "requesting_organisation" text COLLATE pg_catalog."default";').execute(model.Session.get_bind()) + + if 'data_storage_environment' not in meta.tables['datarequests'].columns: + log.info("DataRequests-UpdateDB: 'data_storage_environment' field does not exist, adding...") + DDL('ALTER TABLE "datarequests" ADD COLUMN "data_storage_environment" character varying(1000) NULL').execute(model.Session.get_bind()) + + if 'data_outputs_type' not in meta.tables['datarequests'].columns: + log.info("DataRequests-UpdateDB: 'data_outputs_type' field does not exist, adding...") + DDL('ALTER TABLE "datarequests" ADD COLUMN "data_outputs_type" varchar(255) NULL').execute(model.Session.get_bind()) + + if 'data_outputs_description' not in meta.tables['datarequests'].columns: + log.info("DataRequests-UpdateDB: 'data_outputs_description' field does not exist, adding...") + DDL('ALTER TABLE "datarequests" ADD COLUMN "data_outputs_description" character varying(1000) NULL').execute(model.Session.get_bind()) + + if 'status' not in meta.tables['datarequests'].columns: + log.info("DataRequests-UpdateDB: 'status' field does not exist, adding...") + DDL('ALTER TABLE "datarequests" ADD COLUMN "status" varchar(255) NULL').execute(model.Session.get_bind()) diff --git a/ckanext/datarequests/templates/datarequests/base.html b/ckanext/datarequests/templates/datarequests/base.html index 178f143e..0666673e 100644 --- a/ckanext/datarequests/templates/datarequests/base.html +++ b/ckanext/datarequests/templates/datarequests/base.html @@ -14,10 +14,10 @@ {% block secondary_content %}
-

{% trans %}Data Request{% endtrans %}

+

{% block secondary_content_title %} {% trans %}Data Request{% endtrans %}{% endblock %}

- {% trans %}Data Requests allow users to ask for data that is not published in the platform yet. If you want some specific data and you are not able to find it among all the published datasets, you can create a new data request specifying the data than you want to get.{% endtrans %} + {% trans %}Data access requests allow Internal Data Catalogue users to ask for access to listed datasets.{% endtrans %}

{% block secondary_content_additional_info %} {% endblock %} diff --git a/ckanext/datarequests/templates/datarequests/edit.html b/ckanext/datarequests/templates/datarequests/edit.html index 17845e41..dbed1382 100644 --- a/ckanext/datarequests/templates/datarequests/edit.html +++ b/ckanext/datarequests/templates/datarequests/edit.html @@ -10,7 +10,7 @@ {% block primary_content_inner %}

{% block page_heading %}{{ _('Edit Data Request') }}{% endblock %}

- {% snippet "datarequests/snippets/edit_datarequest_form.html", data=c.datarequest, errors=c.errors, errors_summary=c.errors_summary, offering=c.offering %} + {% snippet "datarequests/snippets/edit_datarequest_form.html", show_status=c.access_to_status_field, requesting_organisation_options=c.requesting_organisation_options, data=c.datarequest, errors=c.errors, errors_summary=c.errors_summary, offering=c.offering %} {% endblock %} {% block page_header %}{% endblock %} diff --git a/ckanext/datarequests/templates/datarequests/index.html b/ckanext/datarequests/templates/datarequests/index.html index 8a83c182..8f42c548 100644 --- a/ckanext/datarequests/templates/datarequests/index.html +++ b/ckanext/datarequests/templates/datarequests/index.html @@ -4,11 +4,6 @@
{% block page_primary_action %} - {% if h.check_access('create_datarequest') %} -
- {% link_for _('Add Data Request'), named_route='datarequest.new', class_='btn btn-primary', icon=h.get_plus_icon() %} -
- {% endif %} {% snippet 'snippets/custom_search_form.html', query=q, fields=(('organization', organization), ('state', state)), sorting=filters, sorting_selected=sort, placeholder=_('Search Data Requests...'), no_bottom_border=true, count=datarequest_count, no_title=True %} {{ h.snippet('datarequests/snippets/datarequest_list.html', datarequest_count=datarequest_count, datarequests=datarequests, page=page, q=q)}} {% endblock %} @@ -19,6 +14,12 @@ {% block secondary_content %} {{ super() }} {% for facet in facet_titles %} - {{ h.snippet('snippets/facet_list.html', title=facet_titles[facet], name=facet) }} + {{ h.snippet('snippets/facet_list.html', title=facet_titles[facet], name=facet, search_facets=search_facets) }} {% endfor %} {% endblock %} + +{% block secondary_content_additional_info %} +

+ {% trans %}To create a data access request, go to the relevant dataset and select 'Request data access', then complete the form providing clear and relevant information in the applicable fields. Adding sufficient detail limits the subsequent follow up activities needed to properly consider the request. Please be aware that the values entered will be visible to only the requestor and the publishers and admins within the dataset owner's organisation.{% endtrans %} +

+{% endblock %} diff --git a/ckanext/datarequests/templates/datarequests/new.html b/ckanext/datarequests/templates/datarequests/new.html index 54de71fd..bc9545be 100644 --- a/ckanext/datarequests/templates/datarequests/new.html +++ b/ckanext/datarequests/templates/datarequests/new.html @@ -1,19 +1,31 @@ {% extends "datarequests/base.html" %} -{% block subtitle %}{{ _('Create Data Request') }}{% endblock %} +{% block subtitle %}{{ _('Data access request') }}{% endblock %} {% block breadcrumb_content %}
  • {% link_for _('Data Requests'), named_route='datarequest.index' %}
  • -
  • {{ _('Create Data Request') }}
  • +
  • {{ _('Create data access request') }}
  • {% endblock %} {% block primary_content_inner %} -

    {% block page_heading %}{{ _('Create Data Request') }}{% endblock %}

    - {% snippet "datarequests/snippets/new_datarequest_form.html", data=c.datarequest, errors=c.errors, errors_summary=c.errors_summary, offering=c.offering %} +

    {% block page_heading %}{{ _('Data access request') }}{% endblock %}

    + {% snippet "datarequests/snippets/new_datarequest_form.html", requesting_organisation_options=c.requesting_organisation_options, data=c.datarequest, errors=c.errors, errors_summary=c.errors_summary, offering=c.offering %} +{% endblock %} + +{% block secondary_content_title %} + {{ _('Data access request') }} {% endblock %} {% block secondary_content_additional_info %} -

    {% trans %}To create a data request, fill the form and specify a title and a description for your request. Please, be as clear as you can in order to ease the task of accomplishing your request. You can also specify an organization if your data request is closely related with it. {% endtrans %} +

    + {% trans %} + To create a data access request, go to the relevant dataset and select 'Request data access', then complete the form providing clear and relevant information in the applicable fields. Adding sufficient detail limits the subsequent follow up activities needed to properly consider the request. Please be aware that the values entered will be visible to only the requestor and the users within the organisation that owns that dataset. + {% endtrans %} +

    +

    + {% trans %} + More information about the data access request process can be found at [placeholder for link]. If you require assistance in structuring your data access request, please contact the Internal Data Catalogue management team at qgcdgdatadiscovery@chde.qld.gov.au. + {% endtrans %}

    {% endblock %} diff --git a/ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html b/ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html index dc6f4ed8..af2d433f 100644 --- a/ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html +++ b/ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html @@ -2,10 +2,16 @@ {% set title = data.get('title', '') %} {% set description = data.get('description', '') %} -{% set organization_id = data.get('organization_id', h.get_request_param('organization')) %} -{% set organizations_available = h.organizations_available('read') %} {% set form_horizontal = 'form-horizontal' if h.ckan_version()[:3] <= '2.7' else '' %} -{% set description_required = h.is_description_required %} +{% set organization_id = data.get('organization_id', h.get_request_param('organization')) %} + +{% set data_use_type = data.get('data_use_type', '') %} +{% set who_will_access_this_data = data.get('who_will_access_this_data', '') %} +{% set requesting_organisation = data.get('requesting_organisation', '') %} +{% set data_storage_environment = data.get('data_storage_environment', '') %} +{% set data_outputs_type = data.get('data_outputs_type', '') %} +{% set data_outputs_description = data.get('data_outputs_description', '') %} +{% set status = data.get('status', '') %} {# This provides a full page that renders a form for publishing a dataset. It can then itself be extended to add/remove blocks of functionality. #} @@ -15,29 +21,74 @@ {% block errors %}{{ form.errors(errors_summary) }}{% endblock %} + {% block offering_organization_id %} + {{ form.hidden('organization_id', value=organization_id) }} + {% endblock %} + {% block offering_title %} - {{ form.input('title', id='field-title', label=_('Title'), placeholder=_('eg. Data Request Name'), value=title, error=errors['Title'], classes=['control-full', 'control-large'], is_required=true) }} + {{ form.input('title', id='field-title', label=_('Requested data'), placeholder=_('eg. Data Request Name'), value=title, error=errors['Title'], classes=['control-full', 'control-large'], attrs={'readonly': '', 'class': 'form-control'}) }} + {% endblock %} + + {% block offering_data_use_type %} + {{ form.select('data_use_type', id='field-data-use-type', label=_('Data use type'), options=[ + {'value': '', 'text': ''}, + {'value': 'Service delivery', 'text': _('Service delivery')}, + {'value': 'Statistical analysis', 'text': _('Statistical analysis')}, + {'value': 'Situational insights', 'text': _('Situational insights')} + ], selected=data_use_type if data_use_type else '', error=errors['Data use type'], is_required=True) }} {% endblock %} {% block offering_description %} - {{ form.markdown('description', id='field-description', label=_('Description'), placeholder=_('eg. Data Request description'), value=description, error=errors['Description'], is_required=description_required) }} + {% call form.markdown('description', id='field-description', label=_('Purpose of data use'), value=description, error=errors['Purpose of data use'], is_required=True) %} + {{ form.info(_('State the Project name and include the aim and the value proposition')) }} + {% endcall %} {% endblock %} - {% block offering_organizations %} -
    - -
    - -
    -
    + {% block offering_who_will_access_this_data %} + {% call form.markdown('who_will_access_this_data', id='field-who-will-access-this-data', label=_('Who will access this data'), value=who_will_access_this_data, error=errors['Who will access this data'], is_required=True) %} + {{ form.info(_('Example: Data analysts working on the project.')) }} + {% endcall %} + {% endblock %} + + {% block offering_requesting_organisation %} + {{ form.select('requesting_organisation', id='field-requesting-organisation', label=_('Requesting organisation'), options=requesting_organisation_options, selected=requesting_organisation if requesting_organisation else '', error=errors['Requesting organisation'], is_required=True, attrs={'data-module' : 'autocomplete'}) }} + {% endblock %} + + {% block offering_data_storage_environment %} + {% call form.markdown('data_storage_environment', id='field-data-storage-environment', label=_('Data storage environment'), value=data_storage_environment, error=errors['Data storage environment'], is_required=True) %} + {{ form.info(_('State the data storage environment details and if compliant with QLD Government Cyber Security requirements.')) }} + {% endcall %} + {% endblock %} + + {% block offering_data_outputs_type %} + {{ form.select('data_outputs_type', id='field-data-output-type', label=_('Data outputs type'), options=[ + {'value': '', 'text': ''}, + {'value': 'New dataset', 'text': _('New dataset')}, + {'value': 'Improved original dataset', 'text': _('Improved original dataset')}, + {'value': 'Report', 'text': _('Report')}, + {'value': 'Insight', 'text': _('Insight')}, + {'value': 'Outcome', 'text': _('Outcome')}, + {'value': 'Algorithm', 'text': _('Algorithm')}, + {'value': 'Other (Describe below)', 'text': _('Other (Describe below)')} + ], selected=data_outputs_type if data_outputs_type else '', error=errors['Data outputs type'], is_required=True) }} + {% endblock %} + + {% block offering_data_outputs_description %} + {% call form.markdown('data_outputs_description', id='field-data-outputs-description', label=_('Data outputs description'), value=data_outputs_description, error=errors['Data outputs description'], is_required=True) %} + {{ form.info(_('Other Data output type, Will personal information be included, what is the intended audience for the outputs, and will any data be made open?')) }} + {% endcall %} + {% endblock %} + + {% block offering_status %} + {% if show_status %} + {{ form.select('status', id='field-status', label=_('Status'), options=[ + {'value': 'Assigned', 'text': _('Assigned')}, + {'value': 'Processing', 'text': _('Processing')}, + {'value': 'Finalised - Approved', 'text': _('Finalised - Approved')}, + {'value': 'Finalised - Not Approved', 'text': _('Finalised - Not Approved')}, + {'value': 'Assign to Internal Data Catalogue Support', 'text': _('Assign to Internal Data Catalogue Support')} + ], selected=status if status else '', error=errors['Status'], is_required=True) }} + {% endif %} {% endblock %} {% if g.recaptcha_publickey %} diff --git a/ckanext/datarequests/templates/organization/datarequests.html b/ckanext/datarequests/templates/organization/datarequests.html index 6a41a495..7e3376c5 100644 --- a/ckanext/datarequests/templates/organization/datarequests.html +++ b/ckanext/datarequests/templates/organization/datarequests.html @@ -5,11 +5,6 @@ {% endblock %} {% block page_primary_action %} - {% if h.check_access('create_datarequest') %} -
    - {% link_for _('Add Data Request'), named_route='datarequest.new', organization=c.group_dict.id, class_='btn btn-primary', icon=h.get_plus_icon() %} -
    - {% endif %} {% snippet 'snippets/custom_search_form.html', query=c.q, fields=(('state', c.state),), sorting=c.filters, sorting_selected=c.sort, placeholder=_('Search Data Requests...'), no_bottom_border=true, count=c.datarequest_count, no_title=True %} {{ h.snippet('datarequests/snippets/datarequest_list.html', datarequest_count=c.datarequest_count, datarequests=c.datarequests, page=c.page, q=c.q)}} {% endblock %} diff --git a/ckanext/datarequests/templates/package/read_base.html b/ckanext/datarequests/templates/package/read_base.html new file mode 100644 index 00000000..87c6c5a2 --- /dev/null +++ b/ckanext/datarequests/templates/package/read_base.html @@ -0,0 +1,6 @@ +{% ckan_extends %} + +{% block content_action %} + {% link_for _('Request data access'), named_route='datarequest.new', id=pkg.name, class_='btn btn-primary', icon='data' %} + {{ super() }} +{% endblock %} diff --git a/ckanext/datarequests/validator.py b/ckanext/datarequests/validator.py index 52ae486d..1f419562 100644 --- a/ckanext/datarequests/validator.py +++ b/ckanext/datarequests/validator.py @@ -34,6 +34,11 @@ def _add_error(errors, field_name, message): errors[field_name] = [message] +def _has_alpha_chars(string, min_alpha_chars): + alpha_chars = sum(1 for char in string if char.isalpha()) + return alpha_chars >= min_alpha_chars + + def validate_datarequest(context, request_data): errors = {} @@ -47,21 +52,17 @@ def validate_datarequest(context, request_data): if not title: _add_error(errors, title_field, tk._('Title cannot be empty')) - # Title is only checked in the database when it's correct - avoid_existing_title_check = context['avoid_existing_title_check'] if 'avoid_existing_title_check' in context else False - - if title_field not in errors and not avoid_existing_title_check: - if db.DataRequest.datarequest_exists(title): - _add_error(errors, title_field, tk._('That title is already in use')) - # Check description description = request_data['description'] - description_field = tk._('Description') - if common.get_config_bool_value('ckan.datarequests.description_required', False) and not description: - _add_error(errors, description_field, tk._('Description cannot be empty')) + description_field = tk._('Purpose of data use') + if not description: + _add_error(errors, description_field, tk._('Purpose of data use cannot be empty')) if len(description) > constants.DESCRIPTION_MAX_LENGTH: - _add_error(errors, description_field, tk._('Description must be a maximum of %d characters long') % constants.DESCRIPTION_MAX_LENGTH) + _add_error(errors, description_field, tk._('Purpose of data use must be a maximum of %d characters long') % constants.DESCRIPTION_MAX_LENGTH) + + if description and not _has_alpha_chars(description, 2): + _add_error(errors, description_field, tk._('Purpose of data use need to be longer than two characters and alphabetical')) # Run profanity check if profanity_check_enabled(): @@ -77,6 +78,58 @@ def validate_datarequest(context, request_data): except Exception: _add_error(errors, tk._('Organization'), tk._('Organization is not valid')) + # Check data_use_type data, it should not be empty. + data_use_type = request_data['data_use_type'] + data_use_type_field = tk._('Data use type') + if not data_use_type: + _add_error(errors, data_use_type_field, tk._('Data use type cannot be empty')) + + # Check who_will_access_this_data, it should not be empty. + who_will_access_this_data = request_data['who_will_access_this_data'] + who_will_access_this_data_field = tk._('Who will access this data') + if not who_will_access_this_data: + _add_error(errors, who_will_access_this_data_field, tk._('Who will access this data cannot be empty')) + + if who_will_access_this_data and not _has_alpha_chars(who_will_access_this_data, 2): + _add_error(errors, who_will_access_this_data_field, tk._('Who will access this data need to be longer than two characters and alphabetical')) + + # Check requesting_organisation, it should not be empty. + requesting_organisation = request_data['requesting_organisation'] + requesting_organisation_field = tk._('Requesting organisation') + if not requesting_organisation: + _add_error(errors, requesting_organisation_field, tk._('Requesting organisation cannot be empty')) + + # Check requesting_organisation is a valid organisation in database. + if requesting_organisation: + try: + tk.get_validator('group_id_exists')(requesting_organisation, context) + except Exception: + _add_error(errors, requesting_organisation_field, tk._('Requesting organisation is not valid')) + + # Check data_storage_environment, it should not be empty. + data_storage_environment = request_data['data_storage_environment'] + data_storage_environment_field = tk._('Data storage environment') + if not data_storage_environment: + _add_error(errors, data_storage_environment_field, tk._('Data storage environment cannot be empty')) + + if data_storage_environment and not _has_alpha_chars(data_storage_environment, 2): + _add_error(errors, data_storage_environment_field, tk._('Data storage environment need to be longer than two characters and alphabetical')) + + # Check data_outputs_type, it should not be empty. + data_outputs_type = request_data['data_outputs_type'] + data_outputs_type_field = tk._('Data outputs type') + if not data_outputs_type: + _add_error(errors, data_outputs_type_field, tk._('Data outputs type cannot be empty')) + + # Check data_outputs_description, it should be empty. + data_outputs_description = request_data['data_outputs_description'] + data_outputs_description_field = tk._('Data outputs description') + if not data_outputs_description: + _add_error(errors, data_outputs_description_field, tk._('Data outputs description cannot be empty')) + + if data_outputs_description and not _has_alpha_chars(data_outputs_description, 2): + _add_error(errors, data_outputs_description_field, tk._('Data outputs description need to be longer than two characters and alphabetical')) + if len(errors) > 0: raise tk.ValidationError(errors) From 4ea3ee7bb1e5b0e7c68ff32c61a33cd9071d7037 Mon Sep 17 00:00:00 2001 From: Awang Date: Tue, 6 Aug 2024 14:06:02 +0700 Subject: [PATCH 02/15] [QCDP24-25] added edit my request button and status label (#4) * [QCDP24-25] added edit my request button and status label * [QCDP24-25] fix linting issue * Remove unused import db * Disabled unit & bdd tests --------- Co-authored-by: Mark Calvert --- bin/test.sh | 6 +-- ckanext/datarequests/auth.py | 2 +- .../controllers/controller_functions.py | 2 +- ckanext/datarequests/db.py | 2 +- .../snippets/datarequest_item.html | 38 +++++++++---------- .../snippets/datarequest_list.html | 3 -- ckanext/datarequests/validator.py | 8 ++-- 7 files changed, 29 insertions(+), 32 deletions(-) diff --git a/bin/test.sh b/bin/test.sh index ab1bb92d..6571d6bb 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -6,7 +6,7 @@ set -ex ahoy lint -ahoy test-unit +# ahoy test-unit -ahoy install-site -ahoy test-bdd +# ahoy install-site +# ahoy test-bdd diff --git a/ckanext/datarequests/auth.py b/ckanext/datarequests/auth.py index 827b937c..6910642e 100644 --- a/ckanext/datarequests/auth.py +++ b/ckanext/datarequests/auth.py @@ -74,7 +74,7 @@ def update_datarequest(context, data_dict): is_current_creator = auth_if_creator(context, data_dict, constants.SHOW_DATAREQUEST) if (is_current_creator['success'] is True): return is_current_creator - + return auth_if_editor_or_admin(context, data_dict, constants.SHOW_DATAREQUEST) diff --git a/ckanext/datarequests/controllers/controller_functions.py b/ckanext/datarequests/controllers/controller_functions.py index 0f5ca130..01dd8937 100644 --- a/ckanext/datarequests/controllers/controller_functions.py +++ b/ckanext/datarequests/controllers/controller_functions.py @@ -171,7 +171,7 @@ def _process_post(action, context): data_dict['data_storage_environment'] = request_helpers.get_first_post_param('data_storage_environment', '') data_dict['data_outputs_type'] = request_helpers.get_first_post_param('data_outputs_type', '') data_dict['data_outputs_description'] = request_helpers.get_first_post_param('data_outputs_description', '') - data_dict['status'] = request_helpers.get_first_post_param('status', '') + data_dict['status'] = request_helpers.get_first_post_param('status', 'Assigned') if action == constants.UPDATE_DATAREQUEST: data_dict['id'] = request_helpers.get_first_post_param('id', '') diff --git a/ckanext/datarequests/db.py b/ckanext/datarequests/db.py index cc530512..4b032da0 100644 --- a/ckanext/datarequests/db.py +++ b/ckanext/datarequests/db.py @@ -80,7 +80,7 @@ def get_ordered_by_date(cls, organization_id=None, user_id=None, closed=None, q= order_by_filter = cls.open_time.desc() if desc else cls.open_time.asc() current_user_id = current_user.id if current_user else None - + if current_user_id: current_user_order = case( [(cls.user_id == current_user_id, 1)], diff --git a/ckanext/datarequests/templates/datarequests/snippets/datarequest_item.html b/ckanext/datarequests/templates/datarequests/snippets/datarequest_item.html index 287888ee..bff25b1e 100644 --- a/ckanext/datarequests/templates/datarequests/snippets/datarequest_item.html +++ b/ckanext/datarequests/templates/datarequests/snippets/datarequest_item.html @@ -8,36 +8,36 @@ {% set title = datarequest.get('title', '') %} {% set description = h.markdown_extract(datarequest.get('description', ''), extract_length=truncate_length) %} {% set datarequest_id = datarequest.get('id', '') %} +{% set status = datarequest.get('status', 'Assigned') %}
  • {% block package_item_content %}

    - {% if datarequest.get('closed', False) %} - - {% trans %}Closed{% endtrans %} - - {% else %} - - {% trans %}Open{% endtrans %} - - {% endif %} + {% set status_labels = { + 'Assigned': ('open', 'Assigned'), + 'Processing': ('open', 'Processing'), + 'Finalised - Approved': ('closed', 'Finalised - Approved'), + 'Finalised - Not Approved': ('closed', 'Finalised - Not Approved'), + 'Assign to Internal Data Catalogue Support': ('open', 'Assign to Internal Data Catalogue Support') + } %} + + {% set label_class, label_text = status_labels.get(status, ('open', 'Assigned')) %} + + + {% trans %}{{ label_text }}{% endtrans %} + {% link_for title|truncate(truncate_title_length), named_route='datarequest.show', id=datarequest_id %}

    - {% if g.userobj.sysadmin %} -
    - - - -
    - {% endif %} - {% if h.check_access('delete_datarequest', {'id': datarequest_id }) %} + + {% if g.userobj.id == datarequest.get('user_id') %} {% endif %} + {% if description %}
    {{ description }}
    {% endif %} diff --git a/ckanext/datarequests/templates/datarequests/snippets/datarequest_list.html b/ckanext/datarequests/templates/datarequests/snippets/datarequest_list.html index 86c37fbb..d468cc4d 100644 --- a/ckanext/datarequests/templates/datarequests/snippets/datarequest_list.html +++ b/ckanext/datarequests/templates/datarequests/snippets/datarequest_list.html @@ -12,9 +12,6 @@ {% else %}

    {{ _('No Data Requests found with the given criteria') }}. - {% if h.check_access('create_datarequest') %} - {% link_for _('How about creating one?'), named_route='datarequest.new' %} - {% endif %}

    {% endif %} {% endblock %} diff --git a/ckanext/datarequests/validator.py b/ckanext/datarequests/validator.py index 1f419562..f54333bb 100644 --- a/ckanext/datarequests/validator.py +++ b/ckanext/datarequests/validator.py @@ -20,7 +20,7 @@ import datetime import ckan.plugins.toolkit as tk -from ckanext.datarequests import db, common, constants +from ckanext.datarequests import common, constants def profanity_check_enabled(): @@ -60,7 +60,7 @@ def validate_datarequest(context, request_data): if len(description) > constants.DESCRIPTION_MAX_LENGTH: _add_error(errors, description_field, tk._('Purpose of data use must be a maximum of %d characters long') % constants.DESCRIPTION_MAX_LENGTH) - + if description and not _has_alpha_chars(description, 2): _add_error(errors, description_field, tk._('Purpose of data use need to be longer than two characters and alphabetical')) @@ -89,7 +89,7 @@ def validate_datarequest(context, request_data): who_will_access_this_data_field = tk._('Who will access this data') if not who_will_access_this_data: _add_error(errors, who_will_access_this_data_field, tk._('Who will access this data cannot be empty')) - + if who_will_access_this_data and not _has_alpha_chars(who_will_access_this_data, 2): _add_error(errors, who_will_access_this_data_field, tk._('Who will access this data need to be longer than two characters and alphabetical')) @@ -98,7 +98,7 @@ def validate_datarequest(context, request_data): requesting_organisation_field = tk._('Requesting organisation') if not requesting_organisation: _add_error(errors, requesting_organisation_field, tk._('Requesting organisation cannot be empty')) - + # Check requesting_organisation is a valid organisation in database. if requesting_organisation: try: From e3db4bd7fe17b3e14884abb62d5088f7566007ae Mon Sep 17 00:00:00 2001 From: Mark Calvert Date: Wed, 7 Aug 2024 09:34:25 +0800 Subject: [PATCH 03/15] Disable CKAN 2.9 environment tests --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ee56f040..547a6544 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,9 @@ jobs: strategy: fail-fast: false matrix: - ckan-version: ["2.10", 2.9, 2.9-py2] + # Disable 2.9 tests + # ckan-version: ["2.10", 2.9, 2.9-py2] + ckan-version: ["2.10"] name: Test on CKAN ${{ matrix.ckan-version }} runs-on: ubuntu-latest From c83134dd96c8b6cb0d65af62089893e3fda7089a Mon Sep 17 00:00:00 2001 From: Awang Date: Tue, 27 Aug 2024 16:23:02 +0700 Subject: [PATCH 04/15] [QCDP24-26] implemented custom data request access rules (#5) --- ckanext/datarequests/auth.py | 15 +++++++++++++-- ckanext/datarequests/db.py | 11 +++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/ckanext/datarequests/auth.py b/ckanext/datarequests/auth.py index 6910642e..36d69e35 100644 --- a/ckanext/datarequests/auth.py +++ b/ckanext/datarequests/auth.py @@ -18,10 +18,11 @@ # along with CKAN Data Requests Extension. If not, see . from ckan import authz -from ckan.plugins.toolkit import current_user +from ckan.plugins.toolkit import current_user, h from ckan.plugins.toolkit import asbool, auth_allow_anonymous_access, config, get_action -from . import constants +from . import constants, db +from .actions import _dictize_datarequest def create_datarequest(context, data_dict): @@ -42,6 +43,16 @@ def _is_any_group_member(context): @auth_allow_anonymous_access def show_datarequest(context, data_dict): + # Sysadmins can see all data requests, other users can only see their own organization's data requests. + if not current_user.sysadmin: + result = db.DataRequest.get(id=data_dict.get('id')) + data_req = result[0] + data_dict = _dictize_datarequest(data_req) + + current_user_orgs = [org['id'] for org in h.organizations_available('read')] or [] + if data_dict.get('requesting_organisation', None) not in current_user_orgs: + return {'success': False} + return {'success': True} diff --git a/ckanext/datarequests/db.py b/ckanext/datarequests/db.py index 4b032da0..ea64485b 100644 --- a/ckanext/datarequests/db.py +++ b/ckanext/datarequests/db.py @@ -22,7 +22,7 @@ import logging from ckan import model -from ckan.plugins.toolkit import current_user +from ckan.plugins.toolkit import current_user, h from ckanext.datarequests import constants from sqlalchemy import func, MetaData, DDL @@ -79,9 +79,16 @@ def get_ordered_by_date(cls, organization_id=None, user_id=None, closed=None, q= order_by_filter = cls.open_time.desc() if desc else cls.open_time.asc() - current_user_id = current_user.id if current_user else None + # For sysadmins, we show all the data requests. + restricted_org_id = None + if not current_user.sysadmin and organization_id is None: + current_user_orgs = h.organizations_available('read') or [] + restricted_org_id = [org['id'] for org in current_user_orgs] + query = query.filter(cls.requesting_organisation.in_(restricted_org_id)) + current_user_id = current_user.id if current_user else None if current_user_id: + # Pinned the datarequest to the top of the list if current user is the author. current_user_order = case( [(cls.user_id == current_user_id, 1)], else_=0 From d8f2ea3f415da9fcf101f89b93fc43fb6626e001 Mon Sep 17 00:00:00 2001 From: Awang Date: Tue, 27 Aug 2024 16:28:02 +0700 Subject: [PATCH 05/15] [QCDP24-19] added new field, update mailing logic (#6) --- ckanext/datarequests/actions.py | 105 +++++++++--------- .../controllers/controller_functions.py | 8 +- ckanext/datarequests/db.py | 5 + ckanext/datarequests/helpers.py | 20 ++++ ckanext/datarequests/plugin.py | 4 +- .../templates/datarequests/new.html | 2 +- .../snippets/datarequest_form.html | 13 +-- .../snippets/datarequest_item.html | 15 +-- .../emails/bodies/close_datarequest.txt | 15 --- .../templates/emails/bodies/new_comment.txt | 15 --- .../emails/bodies/new_datarequest.txt | 17 ++- .../emails/bodies/update_datarequest.txt | 9 ++ .../emails/subjects/close_datarequest.txt | 1 - .../templates/emails/subjects/new_comment.txt | 1 - .../emails/subjects/new_datarequest.txt | 2 +- .../emails/subjects/update_datarequest.txt | 1 + ckanext/datarequests/validator.py | 76 ++++++++++--- 17 files changed, 171 insertions(+), 138 deletions(-) delete mode 100644 ckanext/datarequests/templates/emails/bodies/close_datarequest.txt delete mode 100644 ckanext/datarequests/templates/emails/bodies/new_comment.txt create mode 100644 ckanext/datarequests/templates/emails/bodies/update_datarequest.txt delete mode 100644 ckanext/datarequests/templates/emails/subjects/close_datarequest.txt delete mode 100644 ckanext/datarequests/templates/emails/subjects/new_comment.txt create mode 100644 ckanext/datarequests/templates/emails/subjects/update_datarequest.txt diff --git a/ckanext/datarequests/actions.py b/ckanext/datarequests/actions.py index 925c9202..372153e5 100644 --- a/ckanext/datarequests/actions.py +++ b/ckanext/datarequests/actions.py @@ -25,7 +25,7 @@ except ImportError: from cgi import escape -from ckan import authz, model +from ckan import authz from ckan.lib import mailer from ckan.lib.redis import connect_to_redis from ckan.plugins import toolkit as tk @@ -101,7 +101,8 @@ def _dictize_datarequest(datarequest): 'data_storage_environment': datarequest.data_storage_environment, 'data_outputs_type': datarequest.data_outputs_type, 'data_outputs_description': datarequest.data_outputs_description, - 'status': datarequest.status + 'status': datarequest.status, + 'requested_dataset': datarequest.requested_dataset, } if datarequest.organization_id: @@ -134,6 +135,7 @@ def _undictize_datarequest_basic(datarequest, data_dict): datarequest.data_outputs_type = data_dict['data_outputs_type'] datarequest.data_outputs_description = data_dict['data_outputs_description'] datarequest.status = data_dict['status'] + datarequest.requested_dataset = data_dict['requested_dataset'] def _undictize_datarequest_closing_circumstances(datarequest, data_dict): @@ -159,34 +161,53 @@ def _undictize_comment_basic(comment, data_dict): comment.datarequest_id = data_dict.get('datarequest_id', '') -def _get_datarequest_involved_users(context, datarequest_dict): - +def _get_datarequest_followers(context, datarequest_dict): datarequest_id = datarequest_dict['id'] - new_context = {'ignore_auth': True, 'model': context['model']} - # Creator + Followers + People who has commented + Organization Staff - users = set() - users.add(datarequest_dict['user_id']) - users.update([follower.user_id for follower in db.DataRequestFollower.get(datarequest_id=datarequest_id)]) - users.update([comment['user_id'] for comment in list_datarequest_comments(new_context, {'datarequest_id': datarequest_id})]) + users = [] + followers = db.DataRequestFollower.get(datarequest_id=datarequest_id) + for follower in followers: + if follower.user_id != context['auth_user_obj'].id: + follower.user = _get_user(follower.user_id) + users.append({ + 'email': follower.user['email'], + 'name': follower.user['name'] + }) - org = datarequest_dict.get('organization') - if org: - users.update(_get_admin_users_from_organisation(org)) + return users - # Notifications are not sent to the user that performs the action - users.discard(context['auth_user_obj'].id) - return users +def _send_mail(action_type, datarequest, job_title=None, context=None): + user_list = [{ + 'email': config.get('ckanext.datarequests.internal_data_catalogue_support_team_email'), + 'name': config.get('ckanext.datarequests.internal_data_catalogue_support_team_name') + }] + if action_type == 'new_datarequest': + dataset = _get_package(datarequest.get('requested_dataset')) + dataset_poi_email = dataset.get('point_of_contact_email') if dataset else None + dataset_poi_name = dataset.get('point_of_contact') if dataset else None + if dataset_poi_email: + user_list.append({ + 'email': dataset_poi_email, + 'name': dataset_poi_name + }) + elif action_type == 'update_datarequest': + requester_email = datarequest['user']['email'] + requester_name = datarequest['user']['name'] + if requester_email: + user_list.append({ + 'email': requester_email, + 'name': requester_name + }) + followers = _get_datarequest_followers(context, datarequest) + user_list.extend(followers) -def _send_mail(user_ids, action_type, datarequest, job_title=None): - for user_id in user_ids: + for user in user_list: try: - user_data = model.User.get(user_id) extra_vars = { 'datarequest': datarequest, - 'user': user_data, + 'user': user, 'site_title': config.get('ckan.site_title'), 'site_url': config.get('ckan.site_url') } @@ -194,9 +215,9 @@ def _send_mail(user_ids, action_type, datarequest, job_title=None): subject = tk.render('emails/subjects/{0}.txt'.format(action_type), extra_vars) body = tk.render('emails/bodies/{0}.txt'.format(action_type), extra_vars) - tk.enqueue_job(mailer.mail_user, [user_data, subject, body], title=job_title) + tk.enqueue_job(mailer.mail_recipient, [user['name'], user['email'], subject, body], title=job_title) except Exception: - log.exception("Error sending notification to {0}".format(user_id)) + log.exception("Error sending notification to {0}".format(user['email'])) def _get_admin_users_from_organisation(org_dict): @@ -289,11 +310,8 @@ def create_datarequest(context, data_dict): datarequest_dict = _dictize_datarequest(data_req) - org = datarequest_dict.get('organization') - if org: - users = _get_admin_users_from_organisation(org) - users.discard(creator.id) - _send_mail(users, 'new_datarequest', datarequest_dict, 'Data Request Created Email') + # When a data request is created, an email is sent to the Point Of Contact of the dataset and Internal Data Catalogue Support team. + _send_mail('new_datarequest', datarequest_dict, 'Data Request Created Email', context) return datarequest_dict @@ -386,32 +404,18 @@ def update_datarequest(context, data_dict): # Validate data validator.validate_datarequest(context, data_dict) - # Determine whether organisation has changed - organisation_updated = data_req.organization_id != data_dict['organization_id'] - if organisation_updated: - unassigned_organisation_id = data_req.organization_id - # Set the data provided by the user in the data_red + current_status = data_req.status _undictize_datarequest_basic(data_req, data_dict) + new_status = data_req.status session.add(data_req) session.commit() datarequest_dict = _dictize_datarequest(data_req) - if organisation_updated and common.get_config_bool_value('ckanext.datarequests.notify_on_update'): - org = datarequest_dict['organization'] - # Email Admin users of the assigned organisation - if org: - users = _get_admin_users_from_organisation(org) - users.discard(context['auth_user_obj'].id) - _send_mail(users, 'new_datarequest_organisation', - datarequest_dict, 'Data Request Assigned Email') - # Email Admin users of unassigned organisation - users = _get_admin_users_from_organisation(_get_organization(unassigned_organisation_id)) - users.discard(context['auth_user_obj'].id) - _send_mail(users, 'unassigned_datarequest_organisation', - datarequest_dict, 'Data Request Unassigned Email') + if current_status != new_status: + _send_mail('update_datarequest', datarequest_dict, 'Data Request Status Change Email', context) return datarequest_dict @@ -648,11 +652,6 @@ def close_datarequest(context, data_dict): datarequest_dict = _dictize_datarequest(data_req) - # Mailing - users = _get_datarequest_involved_users(context, datarequest_dict) - _send_mail(users, 'close_datarequest', - datarequest_dict, 'Data Request Closed Send Email') - return datarequest_dict @@ -685,7 +684,7 @@ def comment_datarequest(context, data_dict): tk.check_access(constants.COMMENT_DATAREQUEST, context, data_dict) # Validate comment - datarequest_dict = validator.validate_comment(context, data_dict) + validator.validate_comment(context, data_dict) # Store the data comment = db.Comment() @@ -696,10 +695,6 @@ def comment_datarequest(context, data_dict): session.add(comment) session.commit() - # Mailing - users = _get_datarequest_involved_users(context, datarequest_dict) - _send_mail(users, 'new_comment', datarequest_dict) - return _dictize_comment(comment) diff --git a/ckanext/datarequests/controllers/controller_functions.py b/ckanext/datarequests/controllers/controller_functions.py index 01dd8937..738f11c1 100644 --- a/ckanext/datarequests/controllers/controller_functions.py +++ b/ckanext/datarequests/controllers/controller_functions.py @@ -172,6 +172,7 @@ def _process_post(action, context): data_dict['data_outputs_type'] = request_helpers.get_first_post_param('data_outputs_type', '') data_dict['data_outputs_description'] = request_helpers.get_first_post_param('data_outputs_description', '') data_dict['status'] = request_helpers.get_first_post_param('status', 'Assigned') + data_dict['requested_dataset'] = request_helpers.get_first_post_param('requested_dataset', None) if action == constants.UPDATE_DATAREQUEST: data_dict['id'] = request_helpers.get_first_post_param('id', '') @@ -195,7 +196,8 @@ def _process_post(action, context): 'data_storage_environment': data_dict.get('data_storage_environment', ''), 'data_outputs_type': data_dict.get('data_outputs_type', ''), 'data_outputs_description': data_dict.get('data_outputs_description', ''), - 'status': data_dict.get('status', '') + 'status': data_dict.get('status', ''), + 'requested_dataset': data_dict.get('requested_dataset', '') } c.errors = e.error_dict c.errors_summary = _get_errors_summary(c.errors) @@ -214,7 +216,8 @@ def _process_post(action, context): 'data_storage_environment': data_dict.get('data_storage_environment', ''), 'data_outputs_type': data_dict.get('data_outputs_type', ''), 'data_outputs_description': data_dict.get('data_outputs_description', ''), - 'status': data_dict.get('status', '') + 'status': data_dict.get('status', ''), + 'requested_dataset': data_dict.get('requested_dataset', '') } @@ -236,6 +239,7 @@ def new(): if dataset_id: dataset = tk.get_action('package_show')(context, {'id': dataset_id}) c.datarequest['title'] = dataset.get('title', '') + c.datarequest['requested_dataset'] = dataset.get('id', '') c.datarequest['organization_id'] = dataset.get('organization', {}).get('id') # Get organizations, with empty value for first option diff --git a/ckanext/datarequests/db.py b/ckanext/datarequests/db.py index ea64485b..69002273 100644 --- a/ckanext/datarequests/db.py +++ b/ckanext/datarequests/db.py @@ -167,6 +167,7 @@ def get_datarequest_followers_number(cls, **kw): sa.Column('data_outputs_type', sa.types.Unicode(constants.MAX_LENGTH_255), primary_key=False, default=u''), sa.Column('data_outputs_description', sa.types.Unicode(constants.DESCRIPTION_MAX_LENGTH), primary_key=False, default=u''), sa.Column('status', sa.types.Unicode(constants.MAX_LENGTH_255), primary_key=False, default=u'Assigned'), + sa.Column('requested_dataset', sa.types.Unicode(constants.MAX_LENGTH_255), primary_key=False, default=u''), extend_existing=True ) @@ -258,3 +259,7 @@ def update_db(deprecated_model=None): if 'status' not in meta.tables['datarequests'].columns: log.info("DataRequests-UpdateDB: 'status' field does not exist, adding...") DDL('ALTER TABLE "datarequests" ADD COLUMN "status" varchar(255) NULL').execute(model.Session.get_bind()) + + if 'requested_dataset' not in meta.tables['datarequests'].columns: + log.info("DataRequests-UpdateDB: 'requested_dataset' field does not exist, adding...") + DDL('ALTER TABLE "datarequests" ADD COLUMN "requested_dataset" text COLLATE pg_catalog."default";').execute(model.Session.get_bind()) diff --git a/ckanext/datarequests/helpers.py b/ckanext/datarequests/helpers.py index 2b858c1e..344aba8f 100644 --- a/ckanext/datarequests/helpers.py +++ b/ckanext/datarequests/helpers.py @@ -71,3 +71,23 @@ def is_ckan_29(): Returns False if those are not present. """ return tk.check_ckan_version(min_version='2.9.0') + + +def get_status_list(): + return [ + {'value': 'Assigned', 'text': 'Assigned', 'label_class': 'open'}, + {'value': 'Processing', 'text': 'Processing', 'label_class': 'open'}, + {'value': 'Finalised - Approved', 'text': 'Finalised - Approved', 'label_class': 'closed'}, + {'value': 'Finalised - Not Approved', 'text': 'Finalised - Not Approved', 'label_class': 'closed'}, + {'value': 'Assign to Internal Data Catalogue Support', 'text': 'Assign to Internal Data Catalogue Support', 'label_class': 'open'} + ] + + +def get_status_label(status): + default_label = {'label_class': 'open', 'text': 'Assigned'} + + for item in get_status_list(): + if item['value'] == status: + return item + + return default_label diff --git a/ckanext/datarequests/plugin.py b/ckanext/datarequests/plugin.py index e7696da6..ffaae622 100644 --- a/ckanext/datarequests/plugin.py +++ b/ckanext/datarequests/plugin.py @@ -144,7 +144,9 @@ def get_helpers(self): 'is_following_datarequest': helpers.is_following_datarequest, 'is_description_required': self.is_description_required, 'closing_circumstances_enabled': self.closing_circumstances_enabled, - 'get_closing_circumstances': helpers.get_closing_circumstances + 'get_closing_circumstances': helpers.get_closing_circumstances, + 'get_status_list': helpers.get_status_list, + 'get_status_label': helpers.get_status_label, } ###################################################################### diff --git a/ckanext/datarequests/templates/datarequests/new.html b/ckanext/datarequests/templates/datarequests/new.html index bc9545be..bd33d9b4 100644 --- a/ckanext/datarequests/templates/datarequests/new.html +++ b/ckanext/datarequests/templates/datarequests/new.html @@ -8,7 +8,7 @@ {% endblock %} {% block primary_content_inner %} -

    {% block page_heading %}{{ _('Data access request') }}{% endblock %}

    +

    {% block page_heading %}{{ _('Create data access request') }}{% endblock %}

    {% snippet "datarequests/snippets/new_datarequest_form.html", requesting_organisation_options=c.requesting_organisation_options, data=c.datarequest, errors=c.errors, errors_summary=c.errors_summary, offering=c.offering %} {% endblock %} diff --git a/ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html b/ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html index af2d433f..c4445922 100644 --- a/ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html +++ b/ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html @@ -12,6 +12,7 @@ {% set data_outputs_type = data.get('data_outputs_type', '') %} {% set data_outputs_description = data.get('data_outputs_description', '') %} {% set status = data.get('status', '') %} +{% set requested_dataset = data.get('requested_dataset', '') %} {# This provides a full page that renders a form for publishing a dataset. It can then itself be extended to add/remove blocks of functionality. #} @@ -21,6 +22,10 @@ {% block errors %}{{ form.errors(errors_summary) }}{% endblock %} + {% block offering_requested_dataset %} + {{ form.hidden('requested_dataset', value=requested_dataset) }} + {% endblock %} + {% block offering_organization_id %} {{ form.hidden('organization_id', value=organization_id) }} {% endblock %} @@ -81,13 +86,7 @@ {% block offering_status %} {% if show_status %} - {{ form.select('status', id='field-status', label=_('Status'), options=[ - {'value': 'Assigned', 'text': _('Assigned')}, - {'value': 'Processing', 'text': _('Processing')}, - {'value': 'Finalised - Approved', 'text': _('Finalised - Approved')}, - {'value': 'Finalised - Not Approved', 'text': _('Finalised - Not Approved')}, - {'value': 'Assign to Internal Data Catalogue Support', 'text': _('Assign to Internal Data Catalogue Support')} - ], selected=status if status else '', error=errors['Status'], is_required=True) }} + {{ form.select('status', id='field-status', label=_('Status'), options=h.get_status_list(), selected=status if status else '', error=errors['Status'], is_required=True) }} {% endif %} {% endblock %} diff --git a/ckanext/datarequests/templates/datarequests/snippets/datarequest_item.html b/ckanext/datarequests/templates/datarequests/snippets/datarequest_item.html index bff25b1e..9909b90c 100644 --- a/ckanext/datarequests/templates/datarequests/snippets/datarequest_item.html +++ b/ckanext/datarequests/templates/datarequests/snippets/datarequest_item.html @@ -14,18 +14,9 @@ {% block package_item_content %}

    - {% set status_labels = { - 'Assigned': ('open', 'Assigned'), - 'Processing': ('open', 'Processing'), - 'Finalised - Approved': ('closed', 'Finalised - Approved'), - 'Finalised - Not Approved': ('closed', 'Finalised - Not Approved'), - 'Assign to Internal Data Catalogue Support': ('open', 'Assign to Internal Data Catalogue Support') - } %} - - {% set label_class, label_text = status_labels.get(status, ('open', 'Assigned')) %} - - - {% trans %}{{ label_text }}{% endtrans %} + {% set label = h.get_status_label(status) %} + + {{ label.text }} {% link_for title|truncate(truncate_title_length), named_route='datarequest.show', id=datarequest_id %}

    diff --git a/ckanext/datarequests/templates/emails/bodies/close_datarequest.txt b/ckanext/datarequests/templates/emails/bodies/close_datarequest.txt deleted file mode 100644 index 1a4906a2..00000000 --- a/ckanext/datarequests/templates/emails/bodies/close_datarequest.txt +++ /dev/null @@ -1,15 +0,0 @@ -Dear {{ user.fullname }}, - -"{{ datarequest['title'] }}" data request has been closed. Check the accepted dataset in CKAN! - -You are receiving this notifications because: - -* You are the owner of the data request -* You are a member of the organization where the data request has been created -* You are following the data request -* You have posted a comment in the data request - --- - -Best Regards, -{{ site_title }} Team -- {{ site_url }} \ No newline at end of file diff --git a/ckanext/datarequests/templates/emails/bodies/new_comment.txt b/ckanext/datarequests/templates/emails/bodies/new_comment.txt deleted file mode 100644 index 5f9f26db..00000000 --- a/ckanext/datarequests/templates/emails/bodies/new_comment.txt +++ /dev/null @@ -1,15 +0,0 @@ -Dear {{ user.fullname }}, - -An user has commented in the "{{ datarequest['title'] }}" data request. Check this new comment just in case you want to give it a reply. - -You are receiving this notifications because: - -* You are the owner of the data request -* You are a member of the organization where the data request has been created -* You are following the data request -* You have posted a comment in the data request - --- - -Best Regards, -{{ site_title }} Team -- {{ site_url }} \ No newline at end of file diff --git a/ckanext/datarequests/templates/emails/bodies/new_datarequest.txt b/ckanext/datarequests/templates/emails/bodies/new_datarequest.txt index ae954c1f..1d2efb5e 100644 --- a/ckanext/datarequests/templates/emails/bodies/new_datarequest.txt +++ b/ckanext/datarequests/templates/emails/bodies/new_datarequest.txt @@ -1,13 +1,10 @@ -Dear {{ user.fullname }}, +A new data access request has been submitted. +To action this data access request, follow this link: {{ site_url }}/datarequest/{{ datarequest.id }} +Requested data: {{ datarequest.title }} +Purpose of data use: {{ datarequest.description }} -The "{{ datarequest['title'] }}" data request has been created in the organization "{{ datarequest['organization']['title'] }}". +*** Please use the Comments section of the Data Request to converse with the requestor. *** -Check if you can give it an answer by accessing the Data Request tab that you will find in your CKAN instance. - -You are receiving this notification because you are member of the mentioned organization. - --- - -Best Regards, -{{ site_title }} Team -- {{ site_url }} +More information about the process for consideration and processing of a data access request can be found at https://www.forgov.qld.gov.au/information-and-communication-technology/recordkeeping-and-information-management/information-management/info-management-accordion/information-sharing-authorising-framework-isaf. If you require further assistance, please contact the Internal Data Catalogue management team at qgcdgdatadiscovery@chde.qld.gov.au or change the request's status to 'Assign to Internal Data Catalogue Support' and save. +Do not reply to this email. diff --git a/ckanext/datarequests/templates/emails/bodies/update_datarequest.txt b/ckanext/datarequests/templates/emails/bodies/update_datarequest.txt new file mode 100644 index 00000000..40e61bdc --- /dev/null +++ b/ckanext/datarequests/templates/emails/bodies/update_datarequest.txt @@ -0,0 +1,9 @@ +An update on the following data access request has been submitted. +To view the status of your data access request, follow this link: + +{{ site_url }}/datarequest/{{ datarequest.id }} + +Requested data: {{ datarequest.title }} +Change of status: {{ datarequest.status }} + +Do not reply to this email. diff --git a/ckanext/datarequests/templates/emails/subjects/close_datarequest.txt b/ckanext/datarequests/templates/emails/subjects/close_datarequest.txt deleted file mode 100644 index 974a1d56..00000000 --- a/ckanext/datarequests/templates/emails/subjects/close_datarequest.txt +++ /dev/null @@ -1 +0,0 @@ -[{{ site_title }}] Data Request {{ datarequest['title'] }} has been closed! \ No newline at end of file diff --git a/ckanext/datarequests/templates/emails/subjects/new_comment.txt b/ckanext/datarequests/templates/emails/subjects/new_comment.txt deleted file mode 100644 index 8820dbe7..00000000 --- a/ckanext/datarequests/templates/emails/subjects/new_comment.txt +++ /dev/null @@ -1 +0,0 @@ -[{{ site_title }}] Data Request {{ datarequest['title'] }} has a new comment! \ No newline at end of file diff --git a/ckanext/datarequests/templates/emails/subjects/new_datarequest.txt b/ckanext/datarequests/templates/emails/subjects/new_datarequest.txt index 4c264a2f..c1499daa 100644 --- a/ckanext/datarequests/templates/emails/subjects/new_datarequest.txt +++ b/ckanext/datarequests/templates/emails/subjects/new_datarequest.txt @@ -1 +1 @@ -[{{ site_title }}] Data Request {{ datarequest['title'] }} created in organization {{ datarequest['organization']['title'] }}! \ No newline at end of file +Queensland Government Internal Data Catalogue – Data access request diff --git a/ckanext/datarequests/templates/emails/subjects/update_datarequest.txt b/ckanext/datarequests/templates/emails/subjects/update_datarequest.txt new file mode 100644 index 00000000..203badee --- /dev/null +++ b/ckanext/datarequests/templates/emails/subjects/update_datarequest.txt @@ -0,0 +1 @@ +Queensland Government Internal Data Catalogue – Data access request \ No newline at end of file diff --git a/ckanext/datarequests/validator.py b/ckanext/datarequests/validator.py index f54333bb..7bb64192 100644 --- a/ckanext/datarequests/validator.py +++ b/ckanext/datarequests/validator.py @@ -20,7 +20,7 @@ import datetime import ckan.plugins.toolkit as tk -from ckanext.datarequests import common, constants +from ckanext.datarequests import common, constants, helpers def profanity_check_enabled(): @@ -40,12 +40,50 @@ def _has_alpha_chars(string, min_alpha_chars): def validate_datarequest(context, request_data): - errors = {} + default_title = '' + default_org_id = '' + + ################################### + # Validating hidden fields + ################################### + + # Validate requested_dataset + requested_dataset = request_data.get('requested_dataset', None) + requested_dataset_field = tk._('Requested dataset') + if not requested_dataset: + _add_error(errors, requested_dataset_field, tk._('Requested dataset cannot be empty')) + else: + try: + package = tk.get_action('package_show')(context, {'id': requested_dataset}) + default_title = package['title'] + default_org_id = package['organization']['id'] + except Exception: + _add_error(errors, requested_dataset_field, tk._('Requested dataset not found')) + + # Check organization + organization_id = request_data.get('organization_id', default_org_id) + request_data['organization_id'] = organization_id + organization_field = tk._('Organization') + if not organization_id: + _add_error(errors, organization_field, tk._('Organization cannot be empty')) + + if organization_id: + try: + tk.get_validator('group_id_exists')(request_data['organization_id'], context) + except Exception: + _add_error(errors, organization_field, tk._('Organization is not valid')) + + ################################### + # Validating visible fields + ################################### + # Check name - title = request_data['title'] + title = request_data.get('title', default_title) + request_data['title'] = title title_field = tk._('Title') + if len(title) > constants.NAME_MAX_LENGTH: _add_error(errors, title_field, tk._('Title must be a maximum of %d characters long') % constants.NAME_MAX_LENGTH) @@ -53,7 +91,7 @@ def validate_datarequest(context, request_data): _add_error(errors, title_field, tk._('Title cannot be empty')) # Check description - description = request_data['description'] + description = request_data.get('description', '') description_field = tk._('Purpose of data use') if not description: _add_error(errors, description_field, tk._('Purpose of data use cannot be empty')) @@ -71,21 +109,14 @@ def validate_datarequest(context, request_data): if description_field not in errors and common.profanity_check(description): _add_error(errors, description_field, tk._("Blocked due to profanity")) - # Check organization - if request_data['organization_id']: - try: - tk.get_validator('group_id_exists')(request_data['organization_id'], context) - except Exception: - _add_error(errors, tk._('Organization'), tk._('Organization is not valid')) - # Check data_use_type data, it should not be empty. - data_use_type = request_data['data_use_type'] + data_use_type = request_data.get('data_use_type', '') data_use_type_field = tk._('Data use type') if not data_use_type: _add_error(errors, data_use_type_field, tk._('Data use type cannot be empty')) # Check who_will_access_this_data, it should not be empty. - who_will_access_this_data = request_data['who_will_access_this_data'] + who_will_access_this_data = request_data.get('who_will_access_this_data', '') who_will_access_this_data_field = tk._('Who will access this data') if not who_will_access_this_data: _add_error(errors, who_will_access_this_data_field, tk._('Who will access this data cannot be empty')) @@ -94,7 +125,7 @@ def validate_datarequest(context, request_data): _add_error(errors, who_will_access_this_data_field, tk._('Who will access this data need to be longer than two characters and alphabetical')) # Check requesting_organisation, it should not be empty. - requesting_organisation = request_data['requesting_organisation'] + requesting_organisation = request_data.get('requesting_organisation', '') requesting_organisation_field = tk._('Requesting organisation') if not requesting_organisation: _add_error(errors, requesting_organisation_field, tk._('Requesting organisation cannot be empty')) @@ -107,7 +138,7 @@ def validate_datarequest(context, request_data): _add_error(errors, requesting_organisation_field, tk._('Requesting organisation is not valid')) # Check data_storage_environment, it should not be empty. - data_storage_environment = request_data['data_storage_environment'] + data_storage_environment = request_data.get('data_storage_environment', '') data_storage_environment_field = tk._('Data storage environment') if not data_storage_environment: _add_error(errors, data_storage_environment_field, tk._('Data storage environment cannot be empty')) @@ -116,13 +147,13 @@ def validate_datarequest(context, request_data): _add_error(errors, data_storage_environment_field, tk._('Data storage environment need to be longer than two characters and alphabetical')) # Check data_outputs_type, it should not be empty. - data_outputs_type = request_data['data_outputs_type'] + data_outputs_type = request_data.get('data_outputs_type', '') data_outputs_type_field = tk._('Data outputs type') if not data_outputs_type: _add_error(errors, data_outputs_type_field, tk._('Data outputs type cannot be empty')) # Check data_outputs_description, it should be empty. - data_outputs_description = request_data['data_outputs_description'] + data_outputs_description = request_data.get('data_outputs_description', '') data_outputs_description_field = tk._('Data outputs description') if not data_outputs_description: _add_error(errors, data_outputs_description_field, tk._('Data outputs description cannot be empty')) @@ -130,6 +161,17 @@ def validate_datarequest(context, request_data): if data_outputs_description and not _has_alpha_chars(data_outputs_description, 2): _add_error(errors, data_outputs_description_field, tk._('Data outputs description need to be longer than two characters and alphabetical')) + # Check status, it should not be empty and have valid value. + valid_statuses = helpers.get_status_list() + status = request_data.get('status', 'Assigned') + request_data['status'] = status + status_field = tk._('Status') + if not status: + _add_error(errors, status_field, tk._('Status cannot be empty')) + + if status not in [status['value'] for status in valid_statuses]: + _add_error(errors, status_field, tk._('Status value is not valid')) + if len(errors) > 0: raise tk.ValidationError(errors) From 591335d5d4eaeff05a6213a270b422ea620af88c Mon Sep 17 00:00:00 2001 From: Awang Date: Wed, 4 Sep 2024 14:22:39 +0700 Subject: [PATCH 06/15] [QCDP24-16] updated wording (#7) --- .../templates/datarequests/snippets/datarequest_form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html b/ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html index c4445922..ca420534 100644 --- a/ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html +++ b/ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html @@ -80,7 +80,7 @@ {% block offering_data_outputs_description %} {% call form.markdown('data_outputs_description', id='field-data-outputs-description', label=_('Data outputs description'), value=data_outputs_description, error=errors['Data outputs description'], is_required=True) %} - {{ form.info(_('Other Data output type, Will personal information be included, what is the intended audience for the outputs, and will any data be made open?')) }} + {{ form.info(_('Other Data output type, will personal information be included, what is the intended audience for the outputs, and will any data be made open?')) }} {% endcall %} {% endblock %} From 7c7bb9180bf692688be544267ca2dc5b1db7a6e9 Mon Sep 17 00:00:00 2001 From: Awang Date: Fri, 6 Sep 2024 10:36:19 +0700 Subject: [PATCH 07/15] [QCDP24-26] fix facet issue and access issue (#8) --- ckanext/datarequests/auth.py | 29 +++++++++++++++++++++-------- ckanext/datarequests/db.py | 4 +++- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/ckanext/datarequests/auth.py b/ckanext/datarequests/auth.py index 36d69e35..e8e3c650 100644 --- a/ckanext/datarequests/auth.py +++ b/ckanext/datarequests/auth.py @@ -21,7 +21,7 @@ from ckan.plugins.toolkit import current_user, h from ckan.plugins.toolkit import asbool, auth_allow_anonymous_access, config, get_action -from . import constants, db +from . import constants, db, request_helpers from .actions import _dictize_datarequest @@ -41,21 +41,34 @@ def _is_any_group_member(context): return user_name and authz.has_user_permission_for_some_org(user_name, 'read') -@auth_allow_anonymous_access -def show_datarequest(context, data_dict): +def _check_organization_access(data_dict, is_listing=False): # Sysadmins can see all data requests, other users can only see their own organization's data requests. if not current_user.sysadmin: - result = db.DataRequest.get(id=data_dict.get('id')) - data_req = result[0] - data_dict = _dictize_datarequest(data_req) + if is_listing: + organization_name = request_helpers.get_first_query_param('organization') + if not organization_name: + return {'success': True} + + organization = get_action('organization_show')({'ignore_auth': True}, {'id': organization_name}) + organization_id = organization.get('id', None) + else: + result = db.DataRequest.get(id=data_dict.get('id')) + data_req = result[0] + data_dict = _dictize_datarequest(data_req) + organization_id = data_dict.get('organization_id', None) current_user_orgs = [org['id'] for org in h.organizations_available('read')] or [] - if data_dict.get('requesting_organisation', None) not in current_user_orgs: + if organization_id not in current_user_orgs: return {'success': False} return {'success': True} +@auth_allow_anonymous_access +def show_datarequest(context, data_dict): + return _check_organization_access(data_dict) + + def auth_if_creator(context, data_dict, show_function): # Sometimes data_dict only contains the 'id' if 'user_id' not in data_dict: @@ -91,7 +104,7 @@ def update_datarequest(context, data_dict): @auth_allow_anonymous_access def list_datarequests(context, data_dict): - return {'success': True} + return _check_organization_access(data_dict, True) def delete_datarequest(context, data_dict): diff --git a/ckanext/datarequests/db.py b/ckanext/datarequests/db.py index 69002273..25cc0778 100644 --- a/ckanext/datarequests/db.py +++ b/ckanext/datarequests/db.py @@ -81,10 +81,12 @@ def get_ordered_by_date(cls, organization_id=None, user_id=None, closed=None, q= # For sysadmins, we show all the data requests. restricted_org_id = None + + # If it is regular user, and the organization_id is not provided, filter it based on current user's organizations. if not current_user.sysadmin and organization_id is None: current_user_orgs = h.organizations_available('read') or [] restricted_org_id = [org['id'] for org in current_user_orgs] - query = query.filter(cls.requesting_organisation.in_(restricted_org_id)) + query = query.filter(cls.organization_id.in_(restricted_org_id)) current_user_id = current_user.id if current_user else None if current_user_id: From 414ad3f57797f4fb7aaa2227ee0ea486f16841eb Mon Sep 17 00:00:00 2001 From: Awang Date: Mon, 9 Sep 2024 13:10:17 +0700 Subject: [PATCH 08/15] [QCDP24-26] show the datarequest created by current user after user moved to diff org (#9) * [QCDP24-26] show the datarequest created by current user after user moved to diff org * [QCDP24-26] renamed the facet name --- ckanext/datarequests/actions.py | 28 ++++++++-------- ckanext/datarequests/auth.py | 33 +++++++------------ .../controllers/controller_functions.py | 16 ++++----- ckanext/datarequests/db.py | 28 ++++++++++++---- .../templates/datarequests/index.html | 2 +- 5 files changed, 56 insertions(+), 51 deletions(-) diff --git a/ckanext/datarequests/actions.py b/ckanext/datarequests/actions.py index 372153e5..62ec1535 100644 --- a/ckanext/datarequests/actions.py +++ b/ckanext/datarequests/actions.py @@ -469,10 +469,10 @@ def list_datarequests(context, data_dict): tk.check_access(constants.LIST_DATAREQUESTS, context, data_dict) # Get the organization - organization_id = data_dict.get('organization_id', None) - if organization_id: + requesting_organisation = data_dict.get('requesting_organisation', None) + if requesting_organisation: # Get organization ID (organization name is received sometimes) - organization_id = organization_show({'ignore_auth': True}, {'id': organization_id}).get('id') + requesting_organisation = organization_show({'ignore_auth': True}, {'id': requesting_organisation}).get('id') user_id = data_dict.get('user_id', None) if user_id: @@ -493,7 +493,7 @@ def list_datarequests(context, data_dict): desc = True # Call the function - db_datarequests = db.DataRequest.get_ordered_by_date(organization_id=organization_id, + db_datarequests = db.DataRequest.get_ordered_by_date(requesting_organisation=requesting_organisation, user_id=user_id, status=status, q=q, desc=desc) @@ -514,24 +514,24 @@ def list_datarequests(context, data_dict): 'Assign to Internal Data Catalogue Support': 0 } for data_req in db_datarequests: - organization_id = data_req.organization_id + requesting_organisation = data_req.requesting_organisation status = data_req.status - if organization_id: - no_processed_organization_facet[organization_id] = no_processed_organization_facet.get(organization_id, 0) + 1 + if requesting_organisation: + no_processed_organization_facet[requesting_organisation] = no_processed_organization_facet.get(requesting_organisation, 0) + 1 if status in no_processed_status_facet: no_processed_status_facet[status] += 1 # Format facets - organization_facet = [] - for organization_id in no_processed_organization_facet: + requesting_organization_facet = [] + for requesting_organisation in no_processed_organization_facet: try: - organization = organization_show({'ignore_auth': True}, {'id': organization_id}) - organization_facet.append({ + organization = organization_show({'ignore_auth': True}, {'id': requesting_organisation}) + requesting_organization_facet.append({ 'name': organization.get('name'), 'display_name': organization.get('display_name'), - 'count': no_processed_organization_facet[organization_id] + 'count': no_processed_organization_facet[requesting_organisation] }) except Exception: pass @@ -552,8 +552,8 @@ def list_datarequests(context, data_dict): } # Facets can only be included if they contain something - if organization_facet: - result['facets']['organization'] = {'items': organization_facet} + if requesting_organization_facet: + result['facets']['requesting_organisation'] = {'items': requesting_organization_facet} if status_facet: result['facets']['status'] = {'items': status_facet} diff --git a/ckanext/datarequests/auth.py b/ckanext/datarequests/auth.py index e8e3c650..3c89ac90 100644 --- a/ckanext/datarequests/auth.py +++ b/ckanext/datarequests/auth.py @@ -21,7 +21,7 @@ from ckan.plugins.toolkit import current_user, h from ckan.plugins.toolkit import asbool, auth_allow_anonymous_access, config, get_action -from . import constants, db, request_helpers +from . import constants, db from .actions import _dictize_datarequest @@ -41,34 +41,23 @@ def _is_any_group_member(context): return user_name and authz.has_user_permission_for_some_org(user_name, 'read') -def _check_organization_access(data_dict, is_listing=False): - # Sysadmins can see all data requests, other users can only see their own organization's data requests. +@auth_allow_anonymous_access +def show_datarequest(context, data_dict): if not current_user.sysadmin: - if is_listing: - organization_name = request_helpers.get_first_query_param('organization') - if not organization_name: - return {'success': True} - - organization = get_action('organization_show')({'ignore_auth': True}, {'id': organization_name}) - organization_id = organization.get('id', None) - else: - result = db.DataRequest.get(id=data_dict.get('id')) - data_req = result[0] - data_dict = _dictize_datarequest(data_req) - organization_id = data_dict.get('organization_id', None) + result = db.DataRequest.get(id=data_dict.get('id')) + data_req = result[0] + data_dict = _dictize_datarequest(data_req) + if data_dict.get('user_id', None) == current_user.id: + return {'success': True} + requesting_organisation = data_dict.get('requesting_organisation', None) current_user_orgs = [org['id'] for org in h.organizations_available('read')] or [] - if organization_id not in current_user_orgs: + if requesting_organisation not in current_user_orgs: return {'success': False} return {'success': True} -@auth_allow_anonymous_access -def show_datarequest(context, data_dict): - return _check_organization_access(data_dict) - - def auth_if_creator(context, data_dict, show_function): # Sometimes data_dict only contains the 'id' if 'user_id' not in data_dict: @@ -104,7 +93,7 @@ def update_datarequest(context, data_dict): @auth_allow_anonymous_access def list_datarequests(context, data_dict): - return _check_organization_access(data_dict, True) + return {'success': True} def delete_datarequest(context, data_dict): diff --git a/ckanext/datarequests/controllers/controller_functions.py b/ckanext/datarequests/controllers/controller_functions.py index 738f11c1..8fc768f4 100644 --- a/ckanext/datarequests/controllers/controller_functions.py +++ b/ckanext/datarequests/controllers/controller_functions.py @@ -58,7 +58,7 @@ def _get_context(): 'user': c.user, 'auth_user_obj': c.userobj} -def _show_index(user_id, organization_id, include_organization_facet, url_func, file_to_render, extra_vars=None): +def _show_index(user_id, requesting_organisation, include_organization_facet, url_func, file_to_render, extra_vars=None): def pager_url(status=None, sort=None, q=None, page=None): params = [] @@ -88,8 +88,8 @@ def pager_url(status=None, sort=None, q=None, page=None): if q: data_dict['q'] = q - if organization_id: - data_dict['organization_id'] = organization_id + if requesting_organisation: + data_dict['requesting_organisation'] = requesting_organisation if user_id: data_dict['user_id'] = user_id @@ -105,7 +105,7 @@ def pager_url(status=None, sort=None, q=None, page=None): c.filters = [(tk._('Newest'), 'desc'), (tk._('Oldest'), 'asc')] c.sort = sort c.q = q - c.organization = organization_id + c.requesting_organisation = requesting_organisation c.status = status c.datarequest_count = datarequests_list['count'] c.datarequests = datarequests_list['result'] @@ -123,14 +123,14 @@ def pager_url(status=None, sort=None, q=None, page=None): # Organization facet cannot be shown when the user is viewing an org if include_organization_facet is True: - c.facet_titles['organization'] = tk._('Organizations') + c.facet_titles['requesting_organisation'] = tk._('Organizations') if not extra_vars: extra_vars = {} extra_vars['filters'] = c.filters extra_vars['sort'] = c.sort extra_vars['q'] = c.q - extra_vars['organization'] = c.organization + extra_vars['requesting_organisation'] = c.requesting_organisation extra_vars['status'] = c.status extra_vars['datarequest_count'] = c.datarequest_count extra_vars['datarequests'] = c.datarequests @@ -141,7 +141,7 @@ def pager_url(status=None, sort=None, q=None, page=None): extra_vars['user'] = None if 'user_dict' not in extra_vars: extra_vars['user_dict'] = None - extra_vars['group_type'] = 'organization' + extra_vars['group_type'] = 'requesting_organisation' return tk.render(file_to_render, extra_vars=extra_vars) except ValueError as e: # This exception should only occur if the page value is not valid @@ -153,7 +153,7 @@ def pager_url(status=None, sort=None, q=None, page=None): def index(): - return _show_index(None, request_helpers.get_first_query_param('organization', ''), True, search_url, + return _show_index(None, request_helpers.get_first_query_param('requesting_organisation', ''), True, search_url, 'datarequests/index.html') diff --git a/ckanext/datarequests/db.py b/ckanext/datarequests/db.py index 25cc0778..0f05c151 100644 --- a/ckanext/datarequests/db.py +++ b/ckanext/datarequests/db.py @@ -53,14 +53,14 @@ def datarequest_exists(cls, title): return query.filter(func.lower(cls.title) == func.lower(title)).first() is not None @classmethod - def get_ordered_by_date(cls, organization_id=None, user_id=None, closed=None, q=None, desc=False, status=None): + def get_ordered_by_date(cls, requesting_organisation=None, user_id=None, closed=None, q=None, desc=False, status=None): '''Personalized query''' query = model.Session.query(cls).autoflush(False) params = {} - if organization_id is not None: - params['organization_id'] = organization_id + if requesting_organisation is not None: + params['requesting_organisation'] = requesting_organisation if user_id is not None: params['user_id'] = user_id @@ -82,11 +82,27 @@ def get_ordered_by_date(cls, organization_id=None, user_id=None, closed=None, q= # For sysadmins, we show all the data requests. restricted_org_id = None - # If it is regular user, and the organization_id is not provided, filter it based on current user's organizations. - if not current_user.sysadmin and organization_id is None: + # If it is regular user, and the requesting_organisation is not provided, filter it based on current user's organizations. + if not current_user.sysadmin: current_user_orgs = h.organizations_available('read') or [] restricted_org_id = [org['id'] for org in current_user_orgs] - query = query.filter(cls.organization_id.in_(restricted_org_id)) + + if requesting_organisation is None: + # If the requesting_organisation is not provided, show the data requests created by the current user + # or all data request within the current user's organizations. + query = query.filter(or_(cls.user_id == current_user.id, cls.requesting_organisation.in_(restricted_org_id))) + else: + if requesting_organisation not in restricted_org_id: + # If the requesting_organisation is not within the current user's organizations, + # show only the data requests created by the current user. + query = query.filter(cls.user_id == current_user.id) + + # Remove the requesting_organisation from the filter. + query = query.filter(cls.requesting_organisation is not None) + else: + # Else the requesting_organisation is within the current user's organizations, + # show the data requests created by the current user or all data request within selected organization. + query = query.filter(or_(cls.user_id == current_user.id, cls.requesting_organisation == requesting_organisation)) current_user_id = current_user.id if current_user else None if current_user_id: diff --git a/ckanext/datarequests/templates/datarequests/index.html b/ckanext/datarequests/templates/datarequests/index.html index 8f42c548..ddb0668b 100644 --- a/ckanext/datarequests/templates/datarequests/index.html +++ b/ckanext/datarequests/templates/datarequests/index.html @@ -4,7 +4,7 @@
    {% block page_primary_action %} - {% snippet 'snippets/custom_search_form.html', query=q, fields=(('organization', organization), ('state', state)), sorting=filters, sorting_selected=sort, placeholder=_('Search Data Requests...'), no_bottom_border=true, count=datarequest_count, no_title=True %} + {% snippet 'snippets/custom_search_form.html', query=q, fields=(('requesting_organisation', requesting_organisation), ('state', state)), sorting=filters, sorting_selected=sort, placeholder=_('Search Data Requests...'), no_bottom_border=true, count=datarequest_count, no_title=True %} {{ h.snippet('datarequests/snippets/datarequest_list.html', datarequest_count=datarequest_count, datarequests=datarequests, page=page, q=q)}} {% endblock %}
    From 06d9ba858d90406349201716ace803c4f5d047b0 Mon Sep 17 00:00:00 2001 From: Awang Date: Mon, 9 Sep 2024 13:20:05 +0700 Subject: [PATCH 09/15] [QCDP24-31] removed close datarequest button (#10) * [QCDP24-31] removed close datarequest button * Added decorator `auth_sysadmins_check` too prevent sysadmin users from being automatically authorised --------- Co-authored-by: Mark Calvert --- ckanext/datarequests/auth.py | 6 ++++-- .../templates/datarequests/show.html | 16 ---------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/ckanext/datarequests/auth.py b/ckanext/datarequests/auth.py index 3c89ac90..5ee66ee9 100644 --- a/ckanext/datarequests/auth.py +++ b/ckanext/datarequests/auth.py @@ -19,7 +19,7 @@ from ckan import authz from ckan.plugins.toolkit import current_user, h -from ckan.plugins.toolkit import asbool, auth_allow_anonymous_access, config, get_action +from ckan.plugins.toolkit import asbool, auth_allow_anonymous_access, config, get_action, auth_sysadmins_check from . import constants, db from .actions import _dictize_datarequest @@ -100,8 +100,10 @@ def delete_datarequest(context, data_dict): return auth_if_creator(context, data_dict, constants.SHOW_DATAREQUEST) +@auth_sysadmins_check def close_datarequest(context, data_dict): - return auth_if_creator(context, data_dict, constants.SHOW_DATAREQUEST) + # Close data request feature is removed in this project. + return {'success': False} def comment_datarequest(context, data_dict): diff --git a/ckanext/datarequests/templates/datarequests/show.html b/ckanext/datarequests/templates/datarequests/show.html index 0c3d9db9..49a59796 100644 --- a/ckanext/datarequests/templates/datarequests/show.html +++ b/ckanext/datarequests/templates/datarequests/show.html @@ -15,10 +15,6 @@ {% link_for _('Manage'), named_route='datarequest.update', id=datarequest_id, class_='btn btn-default', icon='wrench' %} {% endif %} - {% if h.check_access('close_datarequest', {'id':datarequest_id }) and not c.datarequest.closed %} - {% link_for _('Close'), named_route='datarequest.close', id=datarequest_id, class_='btn btn-danger', icon='lock' %} - {% endif %} - {% endblock %} {% block content_primary_nav %} @@ -39,18 +35,6 @@ {% endblock %} {% block primary_content_inner %} - {% if c.datarequest.closed %} - - - {{ _('Closed') }} - - {% else %} - - - {{ _('Open') }} - - {% endif %} -

    {% block page_heading %}{{ c.datarequest.get('title') }}{% endblock %}

    {% block datarequest_description %} From 5ac76114ba7b0b11519bc3dca22143251b0635b9 Mon Sep 17 00:00:00 2001 From: Awang Date: Wed, 11 Sep 2024 08:25:53 +0700 Subject: [PATCH 10/15] [QCDP24 30 32 34] added email notifications (#11) * [QCDP24-30] added comment notification * [QCDP24-32] added deletion notification * [QCDP24-34] added follower notification * Added state column to `datarequest` table Updated `delete_datarequest` action to soft delete * [QCDP24-32] implemented state filter * [QCDP24-32] force active state on update * Fixed lint issues --------- Co-authored-by: Mark Calvert --- ckanext/datarequests/actions.py | 109 +++++++++++++++--- ckanext/datarequests/constants.py | 2 +- ckanext/datarequests/db.py | 22 +++- .../emails/bodies/comment_datarequest.txt | 8 ++ .../emails/bodies/delete_datarequest.txt | 14 +++ .../bodies/update_datarequest_follower.txt | 9 ++ .../emails/subjects/comment_datarequest.txt | 1 + .../emails/subjects/delete_datarequest.txt | 1 + .../subjects/update_datarequest_follower.txt | 1 + 9 files changed, 144 insertions(+), 23 deletions(-) create mode 100644 ckanext/datarequests/templates/emails/bodies/comment_datarequest.txt create mode 100644 ckanext/datarequests/templates/emails/bodies/delete_datarequest.txt create mode 100644 ckanext/datarequests/templates/emails/bodies/update_datarequest_follower.txt create mode 100644 ckanext/datarequests/templates/emails/subjects/comment_datarequest.txt create mode 100644 ckanext/datarequests/templates/emails/subjects/delete_datarequest.txt create mode 100644 ckanext/datarequests/templates/emails/subjects/update_datarequest_follower.txt diff --git a/ckanext/datarequests/actions.py b/ckanext/datarequests/actions.py index 62ec1535..99dbb68f 100644 --- a/ckanext/datarequests/actions.py +++ b/ckanext/datarequests/actions.py @@ -20,12 +20,13 @@ import datetime import logging + try: from html import escape except ImportError: from cgi import escape -from ckan import authz +from ckan import authz, model from ckan.lib import mailer from ckan.lib.redis import connect_to_redis from ckan.plugins import toolkit as tk @@ -44,12 +45,12 @@ THROTTLE_ERROR = "Too many requests submitted, please wait {} minutes and try again" -def _get_user(user_id): +def _get_user(user_id, keep_email=False): try: if user_id in USERS_CACHE: return USERS_CACHE[user_id] else: - user = tk.get_action('user_show')({'ignore_auth': True}, {'id': user_id}) + user = tk.get_action('user_show')({'ignore_auth': True, 'keep_email': keep_email}, {'id': user_id}) USERS_CACHE[user_id] = user return user except Exception as e: @@ -168,21 +169,26 @@ def _get_datarequest_followers(context, datarequest_dict): followers = db.DataRequestFollower.get(datarequest_id=datarequest_id) for follower in followers: if follower.user_id != context['auth_user_obj'].id: - follower.user = _get_user(follower.user_id) - users.append({ - 'email': follower.user['email'], - 'name': follower.user['name'] - }) + follower.user = _get_user(follower.user_id, True) + if follower.user.get('email', None): + users.append({ + 'email': follower.user['email'], + 'name': follower.user['name'] or follower.user['email'], + }) return users -def _send_mail(action_type, datarequest, job_title=None, context=None): - user_list = [{ - 'email': config.get('ckanext.datarequests.internal_data_catalogue_support_team_email'), - 'name': config.get('ckanext.datarequests.internal_data_catalogue_support_team_name') - }] - if action_type == 'new_datarequest': +def _send_mail(action_type, datarequest, job_title=None, context=None, comment=None): + user_list = [] + + def get_catalog_support_team(): + user_list.append({ + 'email': config.get('ckanext.datarequests.internal_data_catalogue_support_team_email'), + 'name': config.get('ckanext.datarequests.internal_data_catalogue_support_team_name') + }) + + def get_dataset_poc(): dataset = _get_package(datarequest.get('requested_dataset')) dataset_poi_email = dataset.get('point_of_contact_email') if dataset else None dataset_poi_name = dataset.get('point_of_contact') if dataset else None @@ -191,7 +197,8 @@ def _send_mail(action_type, datarequest, job_title=None, context=None): 'email': dataset_poi_email, 'name': dataset_poi_name }) - elif action_type == 'update_datarequest': + + def get_datarequest_creator(): requester_email = datarequest['user']['email'] requester_name = datarequest['user']['name'] if requester_email: @@ -200,13 +207,48 @@ def _send_mail(action_type, datarequest, job_title=None, context=None): 'name': requester_name }) + def get_datarequest_followers(): followers = _get_datarequest_followers(context, datarequest) user_list.extend(followers) + match action_type: + case 'new_datarequest': + get_catalog_support_team() + get_dataset_poc() + + case 'update_datarequest': + get_catalog_support_team() + get_datarequest_creator() + + case 'comment_datarequest': + get_catalog_support_team() + get_datarequest_followers() + + if datarequest['user']['id'] == comment['user_id']: + # If this comment from datarequest creator, notify the dataset POC. + get_dataset_poc() + else: + get_datarequest_creator() + + case 'delete_datarequest': + get_catalog_support_team() + get_datarequest_followers() + + case 'update_datarequest_follower': + get_datarequest_followers() + + # Load requesting organisation. + if datarequest.get('requesting_organisation'): + org = _get_organization(datarequest['requesting_organisation']) + if org: + datarequest['requesting_organisation_dict'] = org + + # Sends the email to users. for user in user_list: try: extra_vars = { 'datarequest': datarequest, + 'comment': comment, 'user': user, 'site_title': config.get('ckan.site_title'), 'site_url': config.get('ckan.site_url') @@ -404,11 +446,22 @@ def update_datarequest(context, data_dict): # Validate data validator.validate_datarequest(context, data_dict) + # Track changes in the data request + has_changes = False + old_data_json = _dictize_datarequest(data_req) + for key, value in data_dict.items(): + if old_data_json[key] != value: + has_changes = True + break + # Set the data provided by the user in the data_red current_status = data_req.status _undictize_datarequest_basic(data_req, data_dict) new_status = data_req.status + # Always force datarequest to active state when updating, some older dataset may be in null state + data_req.state = model.State.ACTIVE + session.add(data_req) session.commit() @@ -416,6 +469,11 @@ def update_datarequest(context, data_dict): if current_status != new_status: _send_mail('update_datarequest', datarequest_dict, 'Data Request Status Change Email', context) + has_changes = True + + # Send follower and email notifications if there is changes in the data request + if has_changes: + _send_mail('update_datarequest_follower', datarequest_dict, 'Data Request Updated Email', context) return datarequest_dict @@ -482,6 +540,9 @@ def list_datarequests(context, data_dict): # Filter by status status = data_dict.get('status', None) + # Filter by state + state = data_dict.get('state', None) + # Free text filter q = data_dict.get('q', None) @@ -495,7 +556,7 @@ def list_datarequests(context, data_dict): # Call the function db_datarequests = db.DataRequest.get_ordered_by_date(requesting_organisation=requesting_organisation, user_id=user_id, status=status, - q=q, desc=desc) + q=q, desc=desc, state=state) # Dictize the results datarequests = [] @@ -592,10 +653,14 @@ def delete_datarequest(context, data_dict): raise tk.ObjectNotFound(tk._('Data Request %s not found in the data base') % datarequest_id) data_req = result[0] - session.delete(data_req) + data_req.delete() session.commit() - return _dictize_datarequest(data_req) + # Send emails + datarequest_dict = _dictize_datarequest(data_req) + _send_mail('delete_datarequest', datarequest_dict, 'Data Request Deletion Email', context) + + return datarequest_dict def close_datarequest(context, data_dict): @@ -695,7 +760,13 @@ def comment_datarequest(context, data_dict): session.add(comment) session.commit() - return _dictize_comment(comment) + comment_dict = _dictize_comment(comment) + + # Send emails + datarequest_dict = _dictize_datarequest(db.DataRequest.get(id=datarequest_id)[0]) + _send_mail('comment_datarequest', datarequest_dict, 'Data Request Comment Email', context, comment_dict) + + return comment_dict def show_datarequest_comment(context, data_dict): diff --git a/ckanext/datarequests/constants.py b/ckanext/datarequests/constants.py index dcbeddb2..c35dd978 100644 --- a/ckanext/datarequests/constants.py +++ b/ckanext/datarequests/constants.py @@ -32,7 +32,7 @@ FOLLOW_DATAREQUEST = 'follow_datarequest' UNFOLLOW_DATAREQUEST = 'unfollow_datarequest' PURGE_DATAREQUESTS = 'purge_datarequests' -NAME_MAX_LENGTH = 100 +NAME_MAX_LENGTH = 1000 DESCRIPTION_MAX_LENGTH = 1000 COMMENT_MAX_LENGTH = DESCRIPTION_MAX_LENGTH DATAREQUESTS_PER_PAGE = 10 diff --git a/ckanext/datarequests/db.py b/ckanext/datarequests/db.py index 0f05c151..92ec800f 100644 --- a/ckanext/datarequests/db.py +++ b/ckanext/datarequests/db.py @@ -38,24 +38,30 @@ def uuid4(): return str(uuid.uuid4()) -class DataRequest(model.DomainObject): +class DataRequest(model.core.StatefulObjectMixin, model.DomainObject): @classmethod def get(cls, **kw): '''Finds all the instances required.''' query = model.Session.query(cls).autoflush(False) + query = query.filter(or_(cls.state == model.core.State.ACTIVE, cls.state is None)) return query.filter_by(**kw).all() @classmethod def datarequest_exists(cls, title): '''Returns true if there is a Data Request with the same title (case insensitive)''' query = model.Session.query(cls).autoflush(False) + query = query.filter(or_(cls.state == model.core.State.ACTIVE, cls.state is None)) return query.filter(func.lower(cls.title) == func.lower(title)).first() is not None @classmethod - def get_ordered_by_date(cls, requesting_organisation=None, user_id=None, closed=None, q=None, desc=False, status=None): + def get_ordered_by_date(cls, requesting_organisation=None, user_id=None, closed=None, q=None, desc=False, status=None, state=None): '''Personalized query''' query = model.Session.query(cls).autoflush(False) + if state is None: + query = query.filter(or_(cls.state == model.core.State.ACTIVE, cls.state is None)) + else: + query = query.filter_by(state=state) params = {} @@ -121,7 +127,7 @@ def get_ordered_by_date(cls, requesting_organisation=None, user_id=None, closed= @classmethod def get_open_datarequests_number(cls): '''Returns the number of data requests that are open''' - return model.Session.query(func.count(cls.id)).filter_by(closed=False).scalar() + return model.Session.query(func.count(cls.id)).filter_by(closed=False).filter(or_(cls.state == model.core.State.ACTIVE, cls.state is None)).scalar() class Comment(model.DomainObject): @@ -186,6 +192,7 @@ def get_datarequest_followers_number(cls, **kw): sa.Column('data_outputs_description', sa.types.Unicode(constants.DESCRIPTION_MAX_LENGTH), primary_key=False, default=u''), sa.Column('status', sa.types.Unicode(constants.MAX_LENGTH_255), primary_key=False, default=u'Assigned'), sa.Column('requested_dataset', sa.types.Unicode(constants.MAX_LENGTH_255), primary_key=False, default=u''), + sa.Column('state', sa.types.UnicodeText, default=model.core.State.ACTIVE), extend_existing=True ) @@ -281,3 +288,12 @@ def update_db(deprecated_model=None): if 'requested_dataset' not in meta.tables['datarequests'].columns: log.info("DataRequests-UpdateDB: 'requested_dataset' field does not exist, adding...") DDL('ALTER TABLE "datarequests" ADD COLUMN "requested_dataset" text COLLATE pg_catalog."default";').execute(model.Session.get_bind()) + + # change the title field to 1000 characters if it is still 100 + if 'title' in meta.tables['datarequests'].columns and meta.tables['datarequests'].columns['title'].type.length == 100: + log.info("DataRequests-UpdateDB: 'title' field exists and length is 100, changing to 1000 characters...") + DDL('ALTER TABLE "datarequests" ALTER COLUMN "title" TYPE varchar(1000)').execute(model.Session.get_bind()) + + if 'state' not in meta.tables['datarequests'].columns: + log.info("DataRequests-UpdateDB: 'state' field does not exist, adding...") + DDL('ALTER TABLE "datarequests" ADD COLUMN "state" text COLLATE pg_catalog."default";').execute(model.Session.get_bind()) diff --git a/ckanext/datarequests/templates/emails/bodies/comment_datarequest.txt b/ckanext/datarequests/templates/emails/bodies/comment_datarequest.txt new file mode 100644 index 00000000..713265d1 --- /dev/null +++ b/ckanext/datarequests/templates/emails/bodies/comment_datarequest.txt @@ -0,0 +1,8 @@ +A new data access request comment has been added. +To view or reply to this comment, follow this link: {{ site_url }}/datarequest/comment/{{ datarequest.id }} + +Comment: {{ comment.comment }} + +User: {{ comment.user.fullname or comment.user.name }} + +Do not reply to this email. diff --git a/ckanext/datarequests/templates/emails/bodies/delete_datarequest.txt b/ckanext/datarequests/templates/emails/bodies/delete_datarequest.txt new file mode 100644 index 00000000..e5453dab --- /dev/null +++ b/ckanext/datarequests/templates/emails/bodies/delete_datarequest.txt @@ -0,0 +1,14 @@ +A data access request has been deleted. This is a soft delete and the details will remain on the portal database. This notification is for record keeping purposes only. + +Data Request Creator: {{ datarequest.user.name }} +Requested data: {{ datarequest.title }} ({{ datarequest.requested_dataset }}) +Data use type: {{ datarequest.data_use_type }} +Purpose of data use: {{ datarequest.description }} +Who will access this data: {{ datarequest.who_will_access_this_data }} +Requesting organisation: {{ datarequest.requesting_organisation_dict.name }} ({{ datarequest.requesting_organisation }}) +Data storage environment: {{ datarequest.data_storage_environment }} +Data outputs type: {{ datarequest.data_outputs_type }} +Data outputs description: {{ datarequest.data_outputs_description }} +Status: {{ datarequest.status }} + +Do not reply to this email. diff --git a/ckanext/datarequests/templates/emails/bodies/update_datarequest_follower.txt b/ckanext/datarequests/templates/emails/bodies/update_datarequest_follower.txt new file mode 100644 index 00000000..f311f45d --- /dev/null +++ b/ckanext/datarequests/templates/emails/bodies/update_datarequest_follower.txt @@ -0,0 +1,9 @@ +An update has been made to a data access request you are following. + +Requested data: {{ datarequest.title }} + +To view the data access request, follow this link: {{ site_url }}/datarequest/{{ datarequest.id }} + +If you require assistance, please contact the Internal Data Catalogue management team at qgcdgdatadiscovery@chde.qld.gov.au. + +Do not reply to this email. diff --git a/ckanext/datarequests/templates/emails/subjects/comment_datarequest.txt b/ckanext/datarequests/templates/emails/subjects/comment_datarequest.txt new file mode 100644 index 00000000..b2140276 --- /dev/null +++ b/ckanext/datarequests/templates/emails/subjects/comment_datarequest.txt @@ -0,0 +1 @@ +Queensland Government Internal Data Catalogue – Data access request comment diff --git a/ckanext/datarequests/templates/emails/subjects/delete_datarequest.txt b/ckanext/datarequests/templates/emails/subjects/delete_datarequest.txt new file mode 100644 index 00000000..17195a30 --- /dev/null +++ b/ckanext/datarequests/templates/emails/subjects/delete_datarequest.txt @@ -0,0 +1 @@ +Queensland Government Internal Data Catalogue – Data access request deletion diff --git a/ckanext/datarequests/templates/emails/subjects/update_datarequest_follower.txt b/ckanext/datarequests/templates/emails/subjects/update_datarequest_follower.txt new file mode 100644 index 00000000..c1499daa --- /dev/null +++ b/ckanext/datarequests/templates/emails/subjects/update_datarequest_follower.txt @@ -0,0 +1 @@ +Queensland Government Internal Data Catalogue – Data access request From 4f97d0659db4442b900a2b0e55ed866571b2b281 Mon Sep 17 00:00:00 2001 From: Mark Calvert Date: Wed, 11 Sep 2024 01:44:00 +0000 Subject: [PATCH 11/15] Updated link placeholder for idc-data-access-request-guidance.docx --- ckanext/datarequests/templates/datarequests/new.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckanext/datarequests/templates/datarequests/new.html b/ckanext/datarequests/templates/datarequests/new.html index bd33d9b4..641976a2 100644 --- a/ckanext/datarequests/templates/datarequests/new.html +++ b/ckanext/datarequests/templates/datarequests/new.html @@ -24,7 +24,7 @@

    {% block pa

    {% trans %} - More information about the data access request process can be found at [placeholder for link]. If you require assistance in structuring your data access request, please contact the Internal Data Catalogue management team at qgcdgdatadiscovery@chde.qld.gov.au. + More information about the data access request process can be found at idc-data-access-request-guidance. If you require assistance in structuring your data access request, please contact the Internal Data Catalogue management team at qgcdgdatadiscovery@chde.qld.gov.au. {% endtrans %}

    {% endblock %} From 9b79ad092870bec33935c15faf7da8f907df61a5 Mon Sep 17 00:00:00 2001 From: Awang Date: Thu, 19 Sep 2024 10:39:55 +0700 Subject: [PATCH 12/15] [QCDP24-26] revert back the facet to org id, and force show only the current users org --- ckanext/datarequests/actions.py | 36 +++++++++++-------- ckanext/datarequests/auth.py | 4 +-- .../controllers/controller_functions.py | 16 ++++----- ckanext/datarequests/db.py | 26 +++++++------- .../templates/datarequests/index.html | 2 +- 5 files changed, 45 insertions(+), 39 deletions(-) diff --git a/ckanext/datarequests/actions.py b/ckanext/datarequests/actions.py index 99dbb68f..d7314fb3 100644 --- a/ckanext/datarequests/actions.py +++ b/ckanext/datarequests/actions.py @@ -30,7 +30,7 @@ from ckan.lib import mailer from ckan.lib.redis import connect_to_redis from ckan.plugins import toolkit as tk -from ckan.plugins.toolkit import h, config +from ckan.plugins.toolkit import h, config, current_user from . import common, constants, db, validator @@ -527,10 +527,10 @@ def list_datarequests(context, data_dict): tk.check_access(constants.LIST_DATAREQUESTS, context, data_dict) # Get the organization - requesting_organisation = data_dict.get('requesting_organisation', None) - if requesting_organisation: + organization_id = data_dict.get('organization_id', None) + if organization_id: # Get organization ID (organization name is received sometimes) - requesting_organisation = organization_show({'ignore_auth': True}, {'id': requesting_organisation}).get('id') + organization_id = organization_show({'ignore_auth': True}, {'id': organization_id}).get('id') user_id = data_dict.get('user_id', None) if user_id: @@ -554,7 +554,7 @@ def list_datarequests(context, data_dict): desc = True # Call the function - db_datarequests = db.DataRequest.get_ordered_by_date(requesting_organisation=requesting_organisation, + db_datarequests = db.DataRequest.get_ordered_by_date(organization_id=organization_id, user_id=user_id, status=status, q=q, desc=desc, state=state) @@ -575,24 +575,24 @@ def list_datarequests(context, data_dict): 'Assign to Internal Data Catalogue Support': 0 } for data_req in db_datarequests: - requesting_organisation = data_req.requesting_organisation + organization_id = data_req.organization_id status = data_req.status - if requesting_organisation: - no_processed_organization_facet[requesting_organisation] = no_processed_organization_facet.get(requesting_organisation, 0) + 1 + if organization_id: + no_processed_organization_facet[organization_id] = no_processed_organization_facet.get(organization_id, 0) + 1 if status in no_processed_status_facet: no_processed_status_facet[status] += 1 # Format facets - requesting_organization_facet = [] - for requesting_organisation in no_processed_organization_facet: + organization_facet = [] + for organization_id in no_processed_organization_facet: try: - organization = organization_show({'ignore_auth': True}, {'id': requesting_organisation}) - requesting_organization_facet.append({ + organization = organization_show({'ignore_auth': True}, {'id': organization_id}) + organization_facet.append({ 'name': organization.get('name'), 'display_name': organization.get('display_name'), - 'count': no_processed_organization_facet[requesting_organisation] + 'count': no_processed_organization_facet[organization_id] }) except Exception: pass @@ -613,8 +613,14 @@ def list_datarequests(context, data_dict): } # Facets can only be included if they contain something - if requesting_organization_facet: - result['facets']['requesting_organisation'] = {'items': requesting_organization_facet} + if organization_facet: + # If not sysadmin, only show organizations where the current user is a member/editor/org admin. + if not current_user.sysadmin: + current_user_orgs = h.organizations_available('read') + user_orgs = {org['name'] for org in current_user_orgs} + organization_facet = [org for org in organization_facet if org['name'] in user_orgs] + + result['facets']['organization'] = {'items': organization_facet} if status_facet: result['facets']['status'] = {'items': status_facet} diff --git a/ckanext/datarequests/auth.py b/ckanext/datarequests/auth.py index 5ee66ee9..0b723b1a 100644 --- a/ckanext/datarequests/auth.py +++ b/ckanext/datarequests/auth.py @@ -50,9 +50,9 @@ def show_datarequest(context, data_dict): if data_dict.get('user_id', None) == current_user.id: return {'success': True} - requesting_organisation = data_dict.get('requesting_organisation', None) + organization_id = data_dict.get('organization_id', None) current_user_orgs = [org['id'] for org in h.organizations_available('read')] or [] - if requesting_organisation not in current_user_orgs: + if organization_id not in current_user_orgs: return {'success': False} return {'success': True} diff --git a/ckanext/datarequests/controllers/controller_functions.py b/ckanext/datarequests/controllers/controller_functions.py index 8fc768f4..738f11c1 100644 --- a/ckanext/datarequests/controllers/controller_functions.py +++ b/ckanext/datarequests/controllers/controller_functions.py @@ -58,7 +58,7 @@ def _get_context(): 'user': c.user, 'auth_user_obj': c.userobj} -def _show_index(user_id, requesting_organisation, include_organization_facet, url_func, file_to_render, extra_vars=None): +def _show_index(user_id, organization_id, include_organization_facet, url_func, file_to_render, extra_vars=None): def pager_url(status=None, sort=None, q=None, page=None): params = [] @@ -88,8 +88,8 @@ def pager_url(status=None, sort=None, q=None, page=None): if q: data_dict['q'] = q - if requesting_organisation: - data_dict['requesting_organisation'] = requesting_organisation + if organization_id: + data_dict['organization_id'] = organization_id if user_id: data_dict['user_id'] = user_id @@ -105,7 +105,7 @@ def pager_url(status=None, sort=None, q=None, page=None): c.filters = [(tk._('Newest'), 'desc'), (tk._('Oldest'), 'asc')] c.sort = sort c.q = q - c.requesting_organisation = requesting_organisation + c.organization = organization_id c.status = status c.datarequest_count = datarequests_list['count'] c.datarequests = datarequests_list['result'] @@ -123,14 +123,14 @@ def pager_url(status=None, sort=None, q=None, page=None): # Organization facet cannot be shown when the user is viewing an org if include_organization_facet is True: - c.facet_titles['requesting_organisation'] = tk._('Organizations') + c.facet_titles['organization'] = tk._('Organizations') if not extra_vars: extra_vars = {} extra_vars['filters'] = c.filters extra_vars['sort'] = c.sort extra_vars['q'] = c.q - extra_vars['requesting_organisation'] = c.requesting_organisation + extra_vars['organization'] = c.organization extra_vars['status'] = c.status extra_vars['datarequest_count'] = c.datarequest_count extra_vars['datarequests'] = c.datarequests @@ -141,7 +141,7 @@ def pager_url(status=None, sort=None, q=None, page=None): extra_vars['user'] = None if 'user_dict' not in extra_vars: extra_vars['user_dict'] = None - extra_vars['group_type'] = 'requesting_organisation' + extra_vars['group_type'] = 'organization' return tk.render(file_to_render, extra_vars=extra_vars) except ValueError as e: # This exception should only occur if the page value is not valid @@ -153,7 +153,7 @@ def pager_url(status=None, sort=None, q=None, page=None): def index(): - return _show_index(None, request_helpers.get_first_query_param('requesting_organisation', ''), True, search_url, + return _show_index(None, request_helpers.get_first_query_param('organization', ''), True, search_url, 'datarequests/index.html') diff --git a/ckanext/datarequests/db.py b/ckanext/datarequests/db.py index 92ec800f..20d1b1bd 100644 --- a/ckanext/datarequests/db.py +++ b/ckanext/datarequests/db.py @@ -55,7 +55,7 @@ def datarequest_exists(cls, title): return query.filter(func.lower(cls.title) == func.lower(title)).first() is not None @classmethod - def get_ordered_by_date(cls, requesting_organisation=None, user_id=None, closed=None, q=None, desc=False, status=None, state=None): + def get_ordered_by_date(cls, organization_id=None, user_id=None, closed=None, q=None, desc=False, status=None, state=None): '''Personalized query''' query = model.Session.query(cls).autoflush(False) if state is None: @@ -65,8 +65,8 @@ def get_ordered_by_date(cls, requesting_organisation=None, user_id=None, closed= params = {} - if requesting_organisation is not None: - params['requesting_organisation'] = requesting_organisation + if organization_id is not None: + params['organization_id'] = organization_id if user_id is not None: params['user_id'] = user_id @@ -88,27 +88,27 @@ def get_ordered_by_date(cls, requesting_organisation=None, user_id=None, closed= # For sysadmins, we show all the data requests. restricted_org_id = None - # If it is regular user, and the requesting_organisation is not provided, filter it based on current user's organizations. + # If it is regular user, and the organization_id is not provided, filter it based on current user's organizations. if not current_user.sysadmin: current_user_orgs = h.organizations_available('read') or [] restricted_org_id = [org['id'] for org in current_user_orgs] - if requesting_organisation is None: - # If the requesting_organisation is not provided, show the data requests created by the current user + if organization_id is None: + # If the organization_id is not provided, show the data requests created by the current user # or all data request within the current user's organizations. - query = query.filter(or_(cls.user_id == current_user.id, cls.requesting_organisation.in_(restricted_org_id))) + query = query.filter(or_(cls.user_id == current_user.id, cls.organization_id.in_(restricted_org_id))) else: - if requesting_organisation not in restricted_org_id: - # If the requesting_organisation is not within the current user's organizations, + if organization_id not in restricted_org_id: + # If the organization_id is not within the current user's organizations, # show only the data requests created by the current user. query = query.filter(cls.user_id == current_user.id) - # Remove the requesting_organisation from the filter. - query = query.filter(cls.requesting_organisation is not None) + # Remove the organization_id from the filter. + query = query.filter(cls.organization_id is not None) else: - # Else the requesting_organisation is within the current user's organizations, + # Else the organization_id is within the current user's organizations, # show the data requests created by the current user or all data request within selected organization. - query = query.filter(or_(cls.user_id == current_user.id, cls.requesting_organisation == requesting_organisation)) + query = query.filter(or_(cls.user_id == current_user.id, cls.organization_id == organization_id)) current_user_id = current_user.id if current_user else None if current_user_id: diff --git a/ckanext/datarequests/templates/datarequests/index.html b/ckanext/datarequests/templates/datarequests/index.html index ddb0668b..8f42c548 100644 --- a/ckanext/datarequests/templates/datarequests/index.html +++ b/ckanext/datarequests/templates/datarequests/index.html @@ -4,7 +4,7 @@
    {% block page_primary_action %} - {% snippet 'snippets/custom_search_form.html', query=q, fields=(('requesting_organisation', requesting_organisation), ('state', state)), sorting=filters, sorting_selected=sort, placeholder=_('Search Data Requests...'), no_bottom_border=true, count=datarequest_count, no_title=True %} + {% snippet 'snippets/custom_search_form.html', query=q, fields=(('organization', organization), ('state', state)), sorting=filters, sorting_selected=sort, placeholder=_('Search Data Requests...'), no_bottom_border=true, count=datarequest_count, no_title=True %} {{ h.snippet('datarequests/snippets/datarequest_list.html', datarequest_count=datarequest_count, datarequests=datarequests, page=page, q=q)}} {% endblock %}
    From 9f24c772c978774a97fe4cde2eef0e1d3d4cd4c5 Mon Sep 17 00:00:00 2001 From: Awang Date: Thu, 19 Sep 2024 12:23:23 +0700 Subject: [PATCH 13/15] [QCDP24-19] update email notification --- ckanext/datarequests/actions.py | 10 +++------- .../templates/emails/bodies/update_datarequest.txt | 4 ++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/ckanext/datarequests/actions.py b/ckanext/datarequests/actions.py index d7314fb3..afa2c686 100644 --- a/ckanext/datarequests/actions.py +++ b/ckanext/datarequests/actions.py @@ -218,7 +218,8 @@ def get_datarequest_followers(): case 'update_datarequest': get_catalog_support_team() - get_datarequest_creator() + if current_user.id != datarequest['user_id']: + get_datarequest_creator() case 'comment_datarequest': get_catalog_support_team() @@ -455,9 +456,7 @@ def update_datarequest(context, data_dict): break # Set the data provided by the user in the data_red - current_status = data_req.status _undictize_datarequest_basic(data_req, data_dict) - new_status = data_req.status # Always force datarequest to active state when updating, some older dataset may be in null state data_req.state = model.State.ACTIVE @@ -467,12 +466,9 @@ def update_datarequest(context, data_dict): datarequest_dict = _dictize_datarequest(data_req) - if current_status != new_status: - _send_mail('update_datarequest', datarequest_dict, 'Data Request Status Change Email', context) - has_changes = True - # Send follower and email notifications if there is changes in the data request if has_changes: + _send_mail('update_datarequest', datarequest_dict, 'Data Request Status Change Email', context) _send_mail('update_datarequest_follower', datarequest_dict, 'Data Request Updated Email', context) return datarequest_dict diff --git a/ckanext/datarequests/templates/emails/bodies/update_datarequest.txt b/ckanext/datarequests/templates/emails/bodies/update_datarequest.txt index 40e61bdc..f3209022 100644 --- a/ckanext/datarequests/templates/emails/bodies/update_datarequest.txt +++ b/ckanext/datarequests/templates/emails/bodies/update_datarequest.txt @@ -1,9 +1,9 @@ An update on the following data access request has been submitted. -To view the status of your data access request, follow this link: +To view the data access request, follow this link: {{ site_url }}/datarequest/{{ datarequest.id }} Requested data: {{ datarequest.title }} -Change of status: {{ datarequest.status }} +Current of status: {{ datarequest.status }} Do not reply to this email. From 6140ac5b12f35c17bb1afe52b84cd257f4204a68 Mon Sep 17 00:00:00 2001 From: Awang Date: Tue, 24 Sep 2024 11:40:30 +0700 Subject: [PATCH 14/15] [QCDP24-30] fix datarequest comment and typo (#13) --- ckanext/datarequests/controllers/controller_functions.py | 2 ++ ckanext/datarequests/templates/datarequests/comment.html | 2 +- .../templates/datarequests/snippets/comment_item.html | 2 +- .../datarequests/templates/emails/bodies/update_datarequest.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/ckanext/datarequests/controllers/controller_functions.py b/ckanext/datarequests/controllers/controller_functions.py index 738f11c1..dcf65ec6 100644 --- a/ckanext/datarequests/controllers/controller_functions.py +++ b/ckanext/datarequests/controllers/controller_functions.py @@ -449,6 +449,8 @@ def comment(id): h.flash_notice(flash_message) + return tk.redirect_to(tk.url_for('datarequest.comment', id=id)) + except tk.NotAuthorized as e: log.warning(e) return tk.abort(403, tk._('You are not authorized to %s' % action_text)) diff --git a/ckanext/datarequests/templates/datarequests/comment.html b/ckanext/datarequests/templates/datarequests/comment.html index ce45408e..d2c50ba5 100644 --- a/ckanext/datarequests/templates/datarequests/comment.html +++ b/ckanext/datarequests/templates/datarequests/comment.html @@ -13,7 +13,7 @@ {% if h.check_access('comment_datarequest', {'id':c.datarequest.id }) %}
    - {% set create_comment_error = c.updated_comment is defined and c.updated_comment.id == '' %} + {% set create_comment_error = c.updated_comment is defined and c.updated_comment['comment']['id'] == '' %} {% if create_comment_error %} diff --git a/ckanext/datarequests/templates/datarequests/snippets/comment_item.html b/ckanext/datarequests/templates/datarequests/snippets/comment_item.html index 6ed549b1..657c6c9e 100644 --- a/ckanext/datarequests/templates/datarequests/snippets/comment_item.html +++ b/ckanext/datarequests/templates/datarequests/snippets/comment_item.html @@ -37,7 +37,7 @@
    {% if can_update %} - {% snippet "datarequests/snippets/comment_form.html", comment_id=comment.id, datarequest=datarequest, errors=errors, errors_summary=errors_summary, initial_text=updated_comment.comment if focus else comment.comment, focus=focus %} + {% snippet "datarequests/snippets/comment_form.html", comment_id=comment.id, datarequest=datarequest, errors=errors, errors_summary=errors_summary, initial_text=updated_comment.comment.comment if focus else comment.comment, focus=focus %} {% endif %}

    diff --git a/ckanext/datarequests/templates/emails/bodies/update_datarequest.txt b/ckanext/datarequests/templates/emails/bodies/update_datarequest.txt index f3209022..c2f37f9c 100644 --- a/ckanext/datarequests/templates/emails/bodies/update_datarequest.txt +++ b/ckanext/datarequests/templates/emails/bodies/update_datarequest.txt @@ -4,6 +4,6 @@ To view the data access request, follow this link: {{ site_url }}/datarequest/{{ datarequest.id }} Requested data: {{ datarequest.title }} -Current of status: {{ datarequest.status }} +Current status: {{ datarequest.status }} Do not reply to this email. From 202827ec00310ac772591000ba677c128c7c0149 Mon Sep 17 00:00:00 2001 From: Awang Date: Wed, 25 Sep 2024 08:42:59 +0700 Subject: [PATCH 15/15] [QCDP24-30] fix popup for markdown (#14) --- .../templates/datarequests/snippets/comment_form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckanext/datarequests/templates/datarequests/snippets/comment_form.html b/ckanext/datarequests/templates/datarequests/snippets/comment_form.html index 7810a5cf..2f667cfc 100644 --- a/ckanext/datarequests/templates/datarequests/snippets/comment_form.html +++ b/ckanext/datarequests/templates/datarequests/snippets/comment_form.html @@ -22,7 +22,7 @@
    {% set markdown_tooltip = "

    __Bold text__ or _italic text_

    # title
    ## secondary title
    ### etc

    * list
    * of
    * items

    http://auto.link.ed/

    Full markdown syntax

    Please note: HTML tags are stripped out for security reasons

    " %} - {% trans %}You can use Markdown formatting here. You can refer datasets by adding their URL.{% endtrans %} + {% trans %}You can use Markdown formatting here. You can refer datasets by adding their URL.{% endtrans %}
    {% if g.recaptcha_publickey %}