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: