From 88ee36a863691410f5f43b5e0994fbe7431d3a8b Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Thu, 29 Aug 2024 16:08:26 +0200 Subject: [PATCH] Initial rough outline of the components and parts needed to support the requested feature of letting users assign a sort of updates to their already finalized/frozen published archives. The fundamental issue is that the original MUST remain in place due to any DOI and code-of-conduct policies. So any updates e.g. to replace or supplement existing archives must leave the original intact and only e.g. add informational pointers between involved archives. In that way people finding the original e.g. through a DOI get informed about later errata and people landing at the errata archive can trace the history. --- mig/assets/css/V3/style.css | 10 ++- mig/shared/defaults.py | 1 + mig/shared/freezefunctions.py | 122 +++++++++++++++++++++++++++++++--- mig/shared/output.py | 32 ++++++--- 4 files changed, 145 insertions(+), 20 deletions(-) diff --git a/mig/assets/css/V3/style.css b/mig/assets/css/V3/style.css index 09e1b6890..df3cda7f3 100644 --- a/mig/assets/css/V3/style.css +++ b/mig/assets/css/V3/style.css @@ -4,7 +4,7 @@ # --- BEGIN_HEADER --- # # style - Core UI V3 specific but skin-independent styling -# Copyright (C) 2003-2019 The MiG Project lead by Brian Vinter +# Copyright (C) 2003-2024 The MiG Project lead by Brian Vinter # # This file is part of MiG. # @@ -808,3 +808,11 @@ var, sampl, code { .table-responsive .table td, .table-responsive .table th { padding: 0.5rem 1.2rem; } + +/* Highligt replacement and supplement information for published Archives */ +.archive-update-header { + background-color: #ffcc00; /* Yellow for attention */ + padding: 10px; + border: 1px solid #ff9900; + margin-bottom: 20px; +} diff --git a/mig/shared/defaults.py b/mig/shared/defaults.py index 77c92c0b8..115446022 100644 --- a/mig/shared/defaults.py +++ b/mig/shared/defaults.py @@ -306,6 +306,7 @@ public_archive_index = 'published-archive.html' public_archive_files = 'published-files.json' public_archive_doi = 'published-doi.json' +public_archive_updates = 'published-updates.json' public_doi_index = 'archive-doi-index.html' edit_lock_suffix = '.editor_lock__' diff --git a/mig/shared/freezefunctions.py b/mig/shared/freezefunctions.py index 1ea62c4d4..a942db540 100644 --- a/mig/shared/freezefunctions.py +++ b/mig/shared/freezefunctions.py @@ -41,10 +41,11 @@ brief_list, pretty_format_user, get_site_base_url from mig.shared.defaults import freeze_meta_filename, freeze_lock_filename, \ wwwpublic_alias, public_archive_dir, public_archive_index, \ - public_archive_files, public_archive_doi, freeze_flavors, keyword_final, \ - keyword_pending, keyword_updating, keyword_auto, keyword_any, \ - keyword_all, max_freeze_files, archives_cache_filename, \ - freeze_on_tape_filename, archive_marks_dir, csrf_field + public_archive_files, public_archive_doi, public_archive_updates, \ + freeze_flavors, keyword_final, keyword_pending, keyword_updating, \ + keyword_auto, keyword_any, keyword_all, max_freeze_files, \ + archives_cache_filename, freeze_on_tape_filename, archive_marks_dir, \ + csrf_field from mig.shared.fileio import checksum_file, write_file, copy_file, copy_rec, \ move_file, move_rec, remove_rec, delete_file, delete_symlink, \ makedirs_rec, make_symlink, make_temp_dir, acquire_file_lock, \ @@ -67,7 +68,7 @@ ('DESCRIPTION', 'Description')] __meta_archive_internals = [freeze_meta_filename, freeze_lock_filename] __public_archive_internals = [public_archive_index, public_archive_files, - public_archive_doi] + public_archive_doi, public_archive_updates] def brief_freeze(freeze_dict): @@ -158,10 +159,12 @@ def build_freezeitem_object(configuration, freeze_dict, summary=False, if freeze_state not in (keyword_updating, keyword_final): show_finalize = True register_doi = False + assign_updates = False if freeze_state == keyword_final and flavor != 'backup' and \ configuration.site_freeze_doi_url and \ freeze_dict.get('PUBLISH_URL', ''): register_doi = True + assign_updates = True if summary: freeze_files = len(freeze_dict.get('FILES', [])) @@ -291,6 +294,18 @@ def build_freezeitem_object(configuration, freeze_dict, summary=False, 'text': 'Request archive DOI', } freeze_obj['registerdoi_link'] = registerdoi_link + if assign_updates: + assignupdates_link = { + 'object_type': 'link', + 'destination': + "javascript: confirmDialog(%s, '%s');" % + ('assignfreezeupdates', 'Really assign archive updates for %s?' % freeze_id), + 'class': 'assignarchiveupdateslink iconspace genericbutton', + 'title': 'Assign an update for %s archive %s' % (flavor, freeze_id), + 'text': 'Assign archive replacement or supplementary archive material', + } + # TODO: implement backend to handle assigment to published-updates.json + freeze_obj['assignupdates_link'] = assignupdates_link return freeze_obj @@ -311,9 +326,9 @@ def parse_time_delta(str_value): elif unit == 'h': multiplier = 60 elif unit == 'd': - multiplier = 24*60 + multiplier = 24 * 60 elif unit == 'w': - multiplier = 7*24*60 + multiplier = 7 * 24 * 60 minutes = multiplier * count return datetime.timedelta(minutes=minutes) @@ -974,6 +989,8 @@ def write_landing_page(freeze_dict, arch_dir, frozen_files, cached, arch_url = published_url(freeze_dict, configuration) files_url = published_url(freeze_dict, configuration, public_archive_files) doi_url = published_url(freeze_dict, configuration, public_archive_doi) + updates_url = published_url( + freeze_dict, configuration, public_archive_updates) freeze_dict['PUBLISH_URL'] = arch_url _logger.debug("create landing page for %s on %s" % (freeze_id, arch_url)) publish_preamble = "" @@ -1134,11 +1151,93 @@ def write_landing_page(freeze_dict, arch_dir, frozen_files, cached, } }); } - """ % (sorted_hash_algos, files_url, doi_url) + function ajax_showupdates() { + var url = lookup_url('%s'); + console.debug('loading archive updates data from '+url+' ...'); + $('#updatescontents').html('Loading archive updates data ...'); + $('#updatescontents').addClass('spinner iconleftpad'); + var updates_req = $.ajax({ + url: url, + type: 'GET', + dataType: 'json', + success: function(jsonRes, textStatus) { + console.debug('got response from updates lookup: '+textStatus); + console.debug(jsonRes); + var updates_data = ''; + var updates_url = jsonRes.id; + /* Users can assign archive updates to a published archive. + If so the result here is a dict with the following contents: + * replaces_id: ID of another archive that this replaces + * replaces_url: URL of another archive that this replaces + * replaced_by_id: ID of another archive replacing this + * replaced_by_url: URL of another archive replacing this + + Planned but still pending additional fields for references are + * supplements_id: ID of another archive that this supplements + * supplements_url: URL of another archive that this supplements + * supplemented_by_id: ID of another archive supplementing this + * supplemented_by_url: URL of another archive supplementing this + We could make it supplements lists but it quickly gets hairy. + */ + var updates = jsonRes.updates; + if (updates !== undefined) { + updates_data += '
'; + var replaced_by_id = updates.replaced_by_id; + var replaced_by_url = updates.replaced_by_url; + if (replaced_by_id !== undefined && replaced_by_url !== undefined) { + updates_data += '

Replacement Available!

'; + updates_data += '

'; + updates_data += 'The author(s) of this archive has created the new '; + updates_data += 'public '+replaced_by_id+' archive '; + updates_data += 'as a replacement for this archive. Please refer to '; + updates_data += 'that linked new archive for additional information '; + updates_data + ='about the update and changes.


'; + } else { + console.debug('no archive replaced by data'); + } + var replaces_id = updates.replaces_id; + var replaces_url = updates.replaces_url; + if (replaces_id !== undefined && replaces_url !== undefined) { + updates_data += '

Replacement Archive

'; + updates_data += '

'; + updates_data += 'The author(s) of the previously published '; + updates_data += ''+replaces_id+' archive '; + updates_data += 'created this archive to replace it. Please see '; + updates_data += 'that linked old archive for additional details '; + updates_data + ='about the original publication.


'; + } else { + console.debug('no archive replaces data'); + } + + /* TODO: Add handling of supplement archives here */ + + updates_data += '
'; + $('#updatescontents').html(updates_data); + } else { + updates_data = 'No archive updates data found'; + $('#updatescontents').html(updates_data); + } + $('#updatescontents').removeClass('spinner iconleftpad'); + $('#updatestoggle').show(); + }, + error: function(jqXHR, textStatus, errorThrown) { + console.info('No archive updates data found') + console.debug('Archive updates request said: '+ \ + textStatus+' : '+errorThrown); + updates_data = 'No archive updates data found'; + $('#updatescontents').html(updates_data); + $('#updatescontents').removeClass('spinner iconleftpad'); + } + }); + } + """ % (sorted_hash_algos, files_url, doi_url, updates_url) add_ready += """ ajax_showdoi('%s'); + ajax_showupdates('%s'); %s; - """ % (freeze_id, refresh_call) + """ % (freeze_id, freeze_id, refresh_call) # Fake manager themed style setup for tablesorter layout with site style style_entry = themed_styles(configuration, user_settings={}) base_style = style_entry.get("base", "") @@ -1175,6 +1274,9 @@ def write_landing_page(freeze_dict, arch_dir, frozen_files, cached, contents += """ %s
+
+
+

%s

@@ -1638,7 +1740,7 @@ def delete_frozen_archive(freeze_dict, client_id, configuration): _logger.error(web_res) return (False, web_res) - if not delete_file(arch_dir+CACHE_EXT, _logger, allow_missing=True) \ + if not delete_file(arch_dir + CACHE_EXT, _logger, allow_missing=True) \ or not remove_rec(arch_dir, configuration): _logger.error("could not remove archive dir for %s" % brief_freeze(freeze_dict)) diff --git a/mig/shared/output.py b/mig/shared/output.py index cf9bdd7e1..1a4b3faa4 100644 --- a/mig/shared/output.py +++ b/mig/shared/output.py @@ -4,7 +4,7 @@ # --- BEGIN_HEADER --- # # output - general formatting of backend output objects -# Copyright (C) 2003-2023 The MiG Project lead by Brian Vinter +# Copyright (C) 2003-2024 The MiG Project lead by Brian Vinter # # This file is part of MiG. # @@ -28,6 +28,7 @@ """Helper functions to generate output in format specified by the client""" from __future__ import absolute_import +from past.builtins import basestring import os import sys @@ -363,7 +364,7 @@ def txt_format(configuration, ret_val, ret_msg, out_obj): header = [['ID', 'Path']] optional_cols = [('access', 'Access'), ('created', 'Created'), ('active', 'Active'), ('owner', 'Owner'), - ('invites', 'Invites'), ('expire', 'Expire'), + ('invites', 'Invites'), ('expire', 'Expire'), ('single_file', 'Single file'), ] content_keys = ['share_id', 'path'] @@ -1730,8 +1731,9 @@ def html_format(configuration, ret_val, ret_msg, out_obj): lines.append(""" """) - show_updating, show_edit, show_register = 3 * ['hidden'] - edit_link, finalize_link, register_link = 3 * \ + show_updating, show_edit, show_register, show_updates = 4 * \ + ['hidden'] + edit_link, finalize_link, register_link, updates_link = 4 * \ [''] if i.get('state', '') == keyword_updating: show_updating = '' @@ -1745,6 +1747,9 @@ def html_format(configuration, ret_val, ret_msg, out_obj): if i.get('registerdoi_link', ''): register_link = html_link(i['registerdoi_link']) show_register = '' + if i.get('assignupdates_link', ''): + updates_link = html_link(i['assignupdates_link']) + show_updates = '' lines.append("""
@@ -1770,10 +1775,19 @@ def html_format(configuration, ret_val, ret_msg, out_obj): %s

%s

+
+

+You can assign updates to already finalized published archives. This may be +useful in case you fouund errors in the existing archive or want to reference +the already published contents in another archive. +

+

%s

+
""" % (show_updating, show_edit, edit_link, finalize_link, show_register, - configuration.site_freeze_doi_text, register_link)) + configuration.site_freeze_doi_text, register_link, + show_updates, updates_link)) elif i['object_type'] == 'freezestatus': # We only use this element for scripted archive creation @@ -1894,7 +1908,7 @@ def html_format(configuration, ret_val, ret_msg, out_obj): skip_list = i.get('skip_list', []) optional_cols = [('access', 'Access'), ('created', 'Created'), ('active', 'Active'), ('owner', 'Owner'), - ('invites', 'Invites'), ('expire', 'Expire'), + ('invites', 'Invites'), ('expire', 'Expire'), ('single_file', 'Single file'), ] # IMPORTANT: AdBlock Plus hides elements with class sharelink(s) @@ -2849,10 +2863,10 @@ def format_output( def format_timedelta(timedelta): """Formats timedelta as '[Years,] [days,] HH:MM:SS'""" years = timedelta.days // 365 - days = timedelta.days - (years*365) + days = timedelta.days - (years * 365) hours = timedelta.seconds // 3600 - minutes = (timedelta.seconds-(hours*3600)) // 60 - seconds = timedelta.seconds - (hours*3600) - (minutes*60) + minutes = (timedelta.seconds - (hours * 3600)) // 60 + seconds = timedelta.seconds - (hours * 3600) - (minutes * 60) hours_str = "%s" % hours if hours < 10: