diff --git a/hepdata/modules/records/utils/submission.py b/hepdata/modules/records/utils/submission.py
index 37bf5fde2..e065260bc 100644
--- a/hepdata/modules/records/utils/submission.py
+++ b/hepdata/modules/records/utils/submission.py
@@ -43,9 +43,10 @@
from hepdata.modules.email.api import send_finalised_email
from hepdata.modules.permissions.models import SubmissionParticipant
from hepdata.modules.records.utils.workflow import create_record
-from hepdata.modules.submission.api import get_latest_hepsubmission
+from hepdata.modules.submission.api import get_latest_hepsubmission, get_or_create_submission_observer, \
+ delete_submission_observer
from hepdata.modules.submission.models import DataSubmission, DataReview, \
- DataResource, Keyword, RelatedTable, RelatedRecid, HEPSubmission, RecordVersionCommitMessage
+ DataResource, Keyword, RelatedTable, RelatedRecid, HEPSubmission, RecordVersionCommitMessage, SubmissionObserver
from hepdata.modules.records.utils.common import \
get_license, infer_file_type, get_record_by_id, contains_accepted_url
from hepdata.modules.records.utils.common import get_or_create
@@ -61,7 +62,6 @@
import os
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.exc import SQLAlchemyError
-import yaml
from yaml import CSafeLoader as Loader
def construct_yaml_str(self, node):
@@ -625,13 +625,18 @@ def get_or_create_hepsubmission(recid, coordinator=1, status="todo"):
hepsubmission = HEPSubmission(publication_recid=recid,
coordinator=coordinator,
overall_status=status)
-
+ # Commit so that the submission exists in the database.
db.session.add(hepsubmission)
db.session.commit()
+ # Create a new observer key here
+ if status == "todo":
+ get_or_create_submission_observer(hepsubmission.publication_recid, regenerate=True)
+
return hepsubmission
+
def create_data_review(data_recid, publication_recid, version=1):
"""
Creates a new data review given a data record id and a publication record id.
@@ -683,6 +688,8 @@ def unload_submission(record_id, version=1):
except NotFoundError as nfe:
print(nfe)
+ delete_submission_observer(record_id)
+
print('Finished unloading record {0} version {1}.'.format(record_id, version))
@@ -781,6 +788,9 @@ def do_finalise(recid, publication_record=None, force_finalise=False,
hep_submission.overall_status = "finished"
db.session.add(hep_submission)
+ # Clear any existing SubmissionObserver entry from the database
+ delete_submission_observer(hep_submission.publication_recid)
+
db.session.commit()
create_celery_app(current_app)
diff --git a/hepdata/modules/records/views.py b/hepdata/modules/records/views.py
index 77f8ec2d3..b6aeed625 100644
--- a/hepdata/modules/records/views.py
+++ b/hepdata/modules/records/views.py
@@ -33,7 +33,7 @@
from dateutil import parser
from invenio_accounts.models import User
from flask_login import login_required, login_user
-from flask import Blueprint, send_file, abort, redirect, url_for
+from flask import Blueprint, send_file, abort, redirect, current_app, url_for
from flask_security.utils import verify_password
from sqlalchemy import or_, func
from sqlalchemy.orm import joinedload
@@ -43,17 +43,19 @@
from hepdata.config import CFG_DATA_TYPE, CFG_PUB_TYPE, SITE_URL, ADDITIONAL_SIZE_LOAD_CHECK_THRESHOLD
from hepdata.ext.opensearch.api import get_records_matching_field, get_count_for_collection, get_n_latest_records, \
index_record_ids
+from hepdata.modules.converter.views import get_version_count
from hepdata.modules.email.api import send_notification_email, send_new_review_message_email, NoParticipantsException, \
send_question_email, send_coordinator_notification_email
from hepdata.modules.inspire_api.views import get_inspire_record_information
+from hepdata.modules.permissions.api import verify_observer_key
from hepdata.modules.records.api import request, determine_user_privileges, render_template, format_submission, \
render_record, current_user, db, jsonify, get_user_from_id, get_record_contents, extract_journal_info, \
user_allowed_to_perform_action, NoResultFound, OrderedDict, query_messages_for_data_review, returns_json, \
process_payload, has_upload_permissions, has_coordinator_permissions, create_new_version, format_resource, \
should_send_json_ld, JSON_LD_MIMETYPES, get_resource_mimetype, get_table_data_list
-from hepdata.modules.submission.api import get_submission_participants_for_record
+from hepdata.modules.submission.api import get_submission_participants_for_record, get_or_create_submission_observer
from hepdata.modules.submission.models import HEPSubmission, DataSubmission, \
- DataResource, DataReview, Message, Question
+ DataResource, DataReview, Message, Question, SubmissionObserver
from hepdata.modules.records.utils.common import get_record_by_id, \
default_time, IMAGE_TYPES, decode_string, file_size_check, generate_license_data_by_id, load_table_data
from hepdata.modules.records.utils.data_processing_utils import \
@@ -141,13 +143,14 @@ def get_metadata_by_alternative_id(recid):
output_format = request.args.get('format', 'html')
light_mode = bool(request.args.get('light', False))
+ observer_key = request.args.get('observer_key', None)
# Check the Accept header to determine whether to send JSON-LD
if output_format == 'html' and should_send_json_ld(request):
output_format = 'json_ld'
return render_record(recid=record['recid'], record=record, version=version, output_format=output_format,
- light_mode=light_mode)
+ light_mode=light_mode, observer_key=observer_key)
except Exception as e:
log.warning("Unable to find %s.", recid)
@@ -225,6 +228,8 @@ def metadata(recid):
:return: renders the record template
"""
+ observer_key = request.args.get('observer_key')
+
try:
version = int(request.args.get('version', -1))
except ValueError:
@@ -242,7 +247,7 @@ def metadata(recid):
output_format = 'json_ld'
return render_record(recid=recid, record=record, version=version, output_format=output_format,
- light_mode=light_mode)
+ light_mode=light_mode, observer_key=observer_key)
@blueprint.route('/count')
@@ -309,6 +314,20 @@ def get_table_details(recid, data_recid, version, load_all=1):
:param load_all: Whether to perform the filesize check or not when loading (1 will always load the file)
:return:
"""
+
+ observer_key = request.args.get('observer_key')
+ key_verified = verify_observer_key(recid, observer_key)
+
+ version_count, version_count_all = get_version_count(recid)
+
+ if not version:
+ # If version not given explicitly, take to be latest allowed version (or 1 if there are no allowed versions).
+ version = version_count if version_count else 1
+
+ # Check for a user trying to access a version of a publication record where they don't have permissions.
+ if version_count < version_count_all and version == version_count_all and not key_verified:
+ abort(403)
+
# joinedload allows query of data in another table without a second database access.
datasub_query = DataSubmission.query.options(joinedload('related_tables')).filter_by(id=data_recid, version=version)
table_contents = {}
@@ -441,6 +460,44 @@ def get_coordinator_view(recid):
"reserve-uploaders": participants["uploader"]["reserve"]})
+@blueprint.route('/coordinator/observer_key//', methods=['GET', ])
+@blueprint.route('/coordinator/observer_key//', methods=['GET', ])
+@login_required
+def get_observer_data(recid, as_url=None):
+ """
+ Returns the observer url for a record, if it exists, and the user
+ has permission.
+
+ :param recid: The publication recid for requested observer key
+ :param as_url: Default: None - Whether to return as url (when set to 1), or just key
+ :return: JSON object with observer url and recid/status, or failure message.
+ """
+ response = { "recid": recid, "observer_exists": False }
+
+ if user_allowed_to_perform_action(recid) and has_coordinator_permissions(recid, current_user):
+ # Query for the observer key object
+ observer = get_or_create_submission_observer(recid)
+
+ if observer:
+ # If exists, set response value and key
+ response['observer_exists'] = True
+
+ if as_url == 1:
+ site_url = current_app.config.get('SITE_URL', 'https://www.hepdata.net')
+ observer_url = f"{site_url}/record/{recid}?observer_key={observer.observer_key}"
+ response['observer_key'] = observer_url
+ else:
+ response['observer_key'] = observer.observer_key
+ else:
+ # Set response object value for status
+ response['observer_exists'] = False
+ else:
+ # Return error message if user does not have relevant permissions
+ response['message'] = "You do not have permission to perform this action."
+
+ return json.dumps(response)
+
+
@blueprint.route('/data/review/status/', methods=['POST', ])
@login_required
def set_data_review_status():
@@ -698,10 +755,33 @@ def get_resource(resource_id):
Attempts to find any HTML resources to be displayed for a record in the event that it
does not have proper data records included.
- :param recid: publication record id
+ :param resource_id: Resource id
:return: json dictionary containing any HTML files to show.
"""
resource_obj = DataResource.query.filter_by(id=resource_id).first()
+
+ # Perform a join to determine parent ID
+ parent_submission = db.session.query(HEPSubmission).join(
+ HEPSubmission.resources # Uses the relationship, not the table
+ ).filter(
+ DataResource.id == resource_id
+ ).first()
+
+ # Get parent submission recid, or default 0
+ recid = parent_submission.publication_recid if parent_submission else 0
+
+ # Retrieve and verify observer key value
+ observer_key = request.args.get('observer_key')
+ key_verified = verify_observer_key(recid, observer_key)
+
+ version_count, version_count_all = get_version_count(recid)
+ # If version not given explicitly, take to be latest allowed version (or 1 if there are no allowed versions).
+ version = version_count if version_count else 1
+
+ # Check for a user trying to access a version of a publication record where they don't have permissions.
+ if version_count < version_count_all and version == version_count_all and not key_verified:
+ abort(403)
+
view_mode = bool(request.args.get('view', False))
landing_page = bool(request.args.get('landing_page', False))
output_format = 'html'
diff --git a/hepdata/modules/records/webpack.py b/hepdata/modules/records/webpack.py
index bcd8a996a..f22cae664 100644
--- a/hepdata/modules/records/webpack.py
+++ b/hepdata/modules/records/webpack.py
@@ -43,7 +43,7 @@
'hepdata-vis-status-js': './js/hepdata_vis_status.js',
},
dependencies={
- "clipboard": "~1.5.5",
+ "clipboard": "~2.0.11",
"d3": "~3.5.12",
"d3-tip": "~0.6.7"
}
diff --git a/hepdata/modules/submission/api.py b/hepdata/modules/submission/api.py
index 08f12b963..9370f9e78 100644
--- a/hepdata/modules/submission/api.py
+++ b/hepdata/modules/submission/api.py
@@ -16,13 +16,18 @@
# along with HEPData; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
#
+import logging
-from hepdata.modules.submission.models import DataResource
+from invenio_db import db
+from hepdata.modules.submission.models import DataResource, SubmissionObserver
from hepdata.modules.permissions.models import SubmissionParticipant
from hepdata.modules.submission.models import HEPSubmission
"""Common utilities used across the code base."""
+logging.basicConfig()
+log = logging.getLogger(__name__)
+
def is_resource_added_to_submission(recid, version, resource_url):
"""
@@ -80,3 +85,67 @@ def get_submission_participants_for_record(publication_recid, roles=None, **kwar
def get_primary_submission_participants_for_record(publication_recid):
submission_participants = get_submission_participants_for_record(publication_recid, status="primary")
return submission_participants
+
+
+def get_or_create_submission_observer(publication_recid, regenerate=False):
+ """
+ Gets or re/generates a SubmissionObserver key for a given recid.
+ Where an observer does not exist for a recid (with existing sub),
+ it is created and returned instead.
+
+ :param publication_recid: The publication record id
+ :param regenerate: Whether to regenerate/force generate the key
+ :return: SubmissionObserver key, created, or None
+ """
+ submission_observer = SubmissionObserver.query.filter_by(publication_recid=publication_recid).first()
+ created = False
+
+ if submission_observer is None:
+ submission = get_latest_hepsubmission(publication_recid=publication_recid)
+ if submission:
+ if submission.overall_status == "todo" or regenerate:
+ submission_observer = SubmissionObserver(publication_recid=publication_recid)
+ created = True
+ else:
+ # No submission, no observer, return None
+ return None
+
+
+ # If we are to regenerate, and SubmissionObserver was queried and not generated.
+ # If just created, we don't need to generate anything.
+ if not created and regenerate:
+ submission_observer.generate_observer_key()
+
+ # Only commit if we have created or regenerated
+ if created or regenerate:
+ db.session.add(submission_observer)
+ db.session.commit()
+
+ return submission_observer
+
+def delete_submission_observer(recid):
+ """
+ Deletes a SubmissionObserver object from the database
+ based on a given recid value.
+
+ :param: recid: int - The recid to delete on
+ """
+
+ # Validate recid is an integer
+ try:
+ recid = int(recid)
+ except (ValueError, TypeError) as e:
+ log.error(f"Invalid recid provided for observer deletion: {recid}")
+ raise ValueError(f"Supplied recid value ({recid}) for deletion is not an Integer.") from e
+
+ try:
+ submission_observer = SubmissionObserver.query.filter_by(publication_recid=recid).first()
+
+ if submission_observer:
+ db.session.delete(submission_observer)
+ db.session.commit()
+ log.info(f"Deleted observer for submission {recid}")
+ except Exception as e:
+ log.error(f"Error deleting observer for submission {recid}: {e}")
+ db.session.rollback()
+ raise
diff --git a/hepdata/modules/submission/assets/js/hepdata_submission.js b/hepdata/modules/submission/assets/js/hepdata_submission.js
index 22a96c6db..9bde79289 100644
--- a/hepdata/modules/submission/assets/js/hepdata_submission.js
+++ b/hepdata/modules/submission/assets/js/hepdata_submission.js
@@ -215,12 +215,24 @@ $(document).ready(function () {
url: '/submit',
data: payload,
method: 'POST',
- success: function () {
- var finished_html = ' ' +
+ success: function (response) {
+ let finished_html = ' ' +
'
Submission Complete!
';
+
+ // Generate full URL for observer access
+ let observer_url = '/record/' + response.submission_id + '?observer_key=' + response.observer_key;
+ let full_url = response.site_url + observer_url;
+
$("#submission_state").html(finished_html);
+ // Set observer key value on the copy button and show widget
+ $("#direct_data_link").val(full_url);
+ $("#so_button").attr('data-clipboard-text', full_url);
+ $("#submission_observer_container").removeAttr("hidden");
$("#another_submission").removeClass("hidden");
+ // Ensure Clipboard is properly initialised on modal widget
+ HEPDATA.setup_clipboard("#submit_clipboard");
+
}
})
})
diff --git a/hepdata/modules/submission/models.py b/hepdata/modules/submission/models.py
index ca5addb8e..0dcea1e58 100644
--- a/hepdata/modules/submission/models.py
+++ b/hepdata/modules/submission/models.py
@@ -31,6 +31,9 @@
from sqlalchemy import TypeDecorator, types, event
from invenio_db import db
from datetime import datetime
+from uuid import uuid4
+
+from hepdata.config import OBSERVER_KEY_LENGTH
logging.basicConfig()
log = logging.getLogger(__name__)
@@ -88,7 +91,6 @@ class HEPSubmission(db.Model):
# and is used as the document id in opensearch and as the record id
publication_recid = db.Column(db.Integer)
inspire_id = db.Column(db.String(128))
-
data_abstract = db.Column(LargeBinaryString)
resources = db.relationship("DataResource",
@@ -121,6 +123,37 @@ class HEPSubmission(db.Model):
cascade="all,delete")
+class SubmissionObserver(db.Model):
+ """
+ Contains observer key entry for access per publication
+ on the publication_recid field.
+ """
+ __tablename__ = "submissionobserver"
+ publication_recid = db.Column(db.Integer, primary_key=True)
+ observer_key = db.Column(db.String(OBSERVER_KEY_LENGTH), nullable=False)
+
+ def __init__(self, publication_recid):
+ # Validate publication_recid
+ try:
+ recid = int(publication_recid)
+ if recid <= 0:
+ raise ValueError(f"publication_recid must be positive, got: {recid}")
+ self.publication_recid = recid
+ except (ValueError, TypeError) as e:
+ raise ValueError(f"publication_recid must be a positive integer, got: {publication_recid}") from e
+ self.generate_observer_key()
+
+ def generate_observer_key(self):
+ """
+ Generates a new observer key (UUID4/random)
+ and sets the observer key.
+ """
+ # Generate UUID4 and cast to string
+ generated_key = str(uuid4())
+ # Split to desired key length
+ truncated_key = generated_key[:OBSERVER_KEY_LENGTH]
+ self.observer_key = truncated_key
+
# Declarations of the helper tables used to manage many-to-many relationships.
datafile_identifier = db.Table(
'datafile_identifier',
diff --git a/hepdata/modules/submission/templates/hepdata_submission/submit.html b/hepdata/modules/submission/templates/hepdata_submission/submit.html
index 5e7359b27..c65be52bd 100644
--- a/hepdata/modules/submission/templates/hepdata_submission/submit.html
+++ b/hepdata/modules/submission/templates/hepdata_submission/submit.html
@@ -26,6 +26,8 @@
{%- block additional_assets %}
+ {{ webpack['toastr.css'] }}
+