diff --git a/dev/images/icon_cross.svg b/dev/images/icon_cross.svg old mode 100755 new mode 100644 index ba2b3cc1f..a037959bd --- a/dev/images/icon_cross.svg +++ b/dev/images/icon_cross.svg @@ -1,14 +1,3 @@ - - - - - - - - + + + diff --git a/dev/js/main2.js b/dev/js/main2.js index 37b971b7e..f653d2d67 100755 --- a/dev/js/main2.js +++ b/dev/js/main2.js @@ -6,18 +6,20 @@ $(document).ready(function(){ // Before toggling menu, change default header menu class to non-selected state and change default aria attribute: + // According to ChatGPT using aria-hidden for visual appearance isn't recommended and items should not be hidden from + // screen readers. Instead, use aria-expanded to indicate the state of the menu. $('#js-header__nav').attr('class', 'header__nav'); - $('#js-header__nav').attr('aria-hidden', 'true'); + $('#js-header__nav').attr('aria-expanded', 'false'); // Toggle classes and attributes: $('#js-header__nav-button').click(function(){ $('#js-header__nav').toggleClass('header__nav header__nav--selected', 300, 'easeInOutCubic'); - if($('#js-header__nav').attr('aria-hidden') == 'true') { - $('#js-header__nav').attr('aria-hidden', 'false'); + if($('#js-header__nav').attr('aria-expanded') == 'false') { + $('#js-header__nav').attr('aria-expanded', 'true'); } else { - $('#js-header__nav').attr('aria-hidden', 'true'); + $('#js-header__nav').attr('aria-expanded', 'false'); } }); @@ -62,18 +64,25 @@ $(document).ready(function(){ // Toggle open and closed from login button $('#js-header__loginout-button').click(function(){ + if ($('#js-login-modal').attr('aria-hidden') == 'true') { + $('#js-login-modal').attr('aria-hidden', 'false'); + }else { + $('#js-login-modal').attr('aria-hidden', 'true'); + } $('#js-login-modal').fadeToggle(200); }); // Close when close icon is clicked $('#js-login-modal__close').click(function(){ + $('#js-login-modal').attr('aria-hidden', 'true'); $('#js-login-modal').fadeToggle(200); }); // Close when form is submitted $('#js-login-modal__form').submit(function(){ + $('#js-login-modal').attr('aria-hidden', 'true'); $('#js-login-modal').fadeToggle(200); }); diff --git a/dev/legacy-scss/variables.scss b/dev/legacy-scss/variables.scss index 3462cb0c7..48879dc2f 100644 --- a/dev/legacy-scss/variables.scss +++ b/dev/legacy-scss/variables.scss @@ -16,30 +16,30 @@ $breadcrumb-separator: "/" !default; // now missing from bootstrap $text-link-color: rgb(176, 189, 202); // muted blue #B0BDCA -$text-link-over-beige: rgb(0, 86, 149); // #005695 blue link over beige backgrounds +$text-link-over-beige: rgb(0, 57, 97); // #003961 blue link over beige backgrounds -$design-primary-color: rgb(0, 119, 138); // Teal Blue #00778b -$design-primary-gradient-top: rgb(6, 80, 89); // Lighter Teal Blue #168b99 was #17A2AC +$design-primary-color: rgb(0, 95, 112); // Teal Blue #005F70 +$design-primary-gradient-top: rgb(6, 80, 89); // Lighter #11616A $design-primary-gradient-bottom: rgb(1, 64, 71); // Slightly darker Teal Blue #056a7b // Brown colors -$design-secondary-gradient-top: rgb(198, 176, 130); // #C6B082 Used for subnav -$design-secondary-color: rgb(180, 151, 90); // Metallic Gold #B4975A -$design-secondary-lighter: rgb(199, 177, 131); // #C7B183 Used for table headers +$design-secondary-gradient-top: rgb(213, 197, 165); // #D5C5A5 +$design-secondary-color: rgb(208, 190, 149); // #D0BE95 +$design-secondary-lighter: rgb(214, 200, 164); // #D6C8A4 $design-secondary-footer-newsfeed: rgb(221, 208, 181); // #DDD0B5 $design-secondary-footer-icon-bg: rgb(229, 219, 197); // #E5DBC5 $design-secondary-lightest: rgb(236, 229, 214); // Beige #ECE5D6 // Orange colors -$design-pop-color: rgb(255, 127, 34); // Bright Orange #FF7F22 -$design-pop-gradient-top: rgb(255,142,40); // Slightly Lighter Orange #FF8E28 -$design-pop-gradient-bottom: rgb(255,112,28); // Slightly Darker Orange #FF701C -$design-pop-dark: rgb(216,64,47); // Reddish Orange #D8402F +$design-pop-color: rgb(124, 24, 44); // # 7C182C +$design-pop-gradient-top: rgb(138, 25, 48); // Slightly Lighter #8A1930 +$design-pop-gradient-bottom: rgb(116, 22, 41); // Slightly Darker #741629 +$design-pop-dark: rgb(106, 20, 37); // Deep Burgundy #6A1425 // These were missing so I'm guessing at the values and required to compile scss -$gray-light: rgb(190, 182, 175); // #BEB6AF -$gray-lighter: rgb(237, 234, 229); // #EDEAE5 +$gray-light: rgb(230, 230, 230); // #E6E6E6 +$gray-lighter: rgb(240, 240, 240); // #F0F0F0 $design-light-gray-border-color: rgb(190, 182, 175); $design-light-gray-bg-color: rgb(249, 248, 247); // #F9F8F7 diff --git a/dev/scss/_customize-table.scss b/dev/scss/_customize-table.scss index 4c4839f89..cb74256d9 100755 --- a/dev/scss/_customize-table.scss +++ b/dev/scss/_customize-table.scss @@ -9,6 +9,7 @@ margin: 0 auto 10px; padding: 10px; background: linear-gradient(to bottom, $design-pop-gradient-top, $design-pop-gradient-bottom); + color: $design-white-color; @include bp(screen1) { flex-flow: row nowrap; diff --git a/dev/scss/_header.scss b/dev/scss/_header.scss index d15e4c059..00b7ce85d 100755 --- a/dev/scss/_header.scss +++ b/dev/scss/_header.scss @@ -8,7 +8,7 @@ justify-content: flex-end; max-width: 1000px; margin: 10px auto; - color: $design-pop-color; + color: $design-primary-color; } } diff --git a/dev/scss/_login-menu.scss b/dev/scss/_login-menu.scss index 74acf88d0..ea5c07147 100755 --- a/dev/scss/_login-menu.scss +++ b/dev/scss/_login-menu.scss @@ -51,7 +51,7 @@ &:hover, &:focus { - color: $design-white-color; + color: $text-link-over-beige; } } @@ -62,7 +62,7 @@ .login-menu__link--selected { @extend %login-menu__link; - color: $design-white-color; + color: $text-link-over-beige; &::before { @include bp(screen1) { diff --git a/dev/scss/_login-modal.scss b/dev/scss/_login-modal.scss index 245e7808e..7c204d6b3 100644 --- a/dev/scss/_login-modal.scss +++ b/dev/scss/_login-modal.scss @@ -33,6 +33,7 @@ padding: 2px; background-color: $design-white-color; cursor: pointer; + object-fit: contain; // Or "cover" depending on the fit you need } .login-modal__form { diff --git a/ezidapp/management/commands/opensearch-update.py b/ezidapp/management/commands/opensearch-update.py index d6f957c88..f192e8e49 100644 --- a/ezidapp/management/commands/opensearch-update.py +++ b/ezidapp/management/commands/opensearch-update.py @@ -17,7 +17,8 @@ urllib3.disable_warnings(InsecureRequestWarning) # end suppression of urllib3 InsecureRequestWarning -SPLIT_SIZE = 100 +SPLIT_SIZE = 5 +DB_PAGE_SIZE = 100 # run: python manage.py opensearch-update # optional parameters: --starting_id 1234 --updated_since 2023-10-10T00:00:00Z @@ -37,7 +38,7 @@ class Command(BaseCommand): def handle(self, *args, **options): - # Get all items from Identifier table 100 at a time manually since + # Get all items from Identifier table DB_PAGE_SIZE at a time manually since # I had lockup issues with the ORM, even with constructs that were # supposed to be lazy and handle large datasets. :shrug: # @@ -68,7 +69,7 @@ def handle(self, *args, **options): while True: iden_arr = (SearchIdentifier.objects.filter(id__gt=start_after_id) - .filter(additional_filter).order_by('id')[:100]) + .filter(additional_filter).order_by('id')[:DB_PAGE_SIZE]) # break when we run out of items if not iden_arr: diff --git a/ezidapp/management/commands/proc-cleanup-async-queues.py b/ezidapp/management/commands/proc-cleanup-async-queues.py index d9f578ff1..211e0087f 100644 --- a/ezidapp/management/commands/proc-cleanup-async-queues.py +++ b/ezidapp/management/commands/proc-cleanup-async-queues.py @@ -15,19 +15,22 @@ import logging import time +from datetime import datetime +from dateutil.parser import parse -import django.conf import django.conf import django.db -import django.db.transaction +from django.db import transaction +from django.db.models import Q import ezidapp.management.commands.proc_base import ezidapp.models.identifier import ezidapp.models.shoulder -from django.db.models import Q + log = logging.getLogger(__name__) + class Command(ezidapp.management.commands.proc_base.AsyncProcessingCommand): help = __doc__ name = __name__ @@ -45,6 +48,29 @@ class Command(ezidapp.management.commands.proc_base.AsyncProcessingCommand): def __init__(self): super().__init__() + def add_arguments(self, parser): + super().add_arguments(parser) + parser.add_argument( + '--pagesize', help='Rows in each batch select.', type=int) + + parser.add_argument( + '--updated_range_from', type=str, + help = ( + 'Updated date range from - local date/time in ISO 8601 format without timezone \n' + 'YYYYMMDD, YYYYMMDDTHHMMSS, YYYY-MM-DD, YYYY-MM-DDTHH:MM:SS. \n' + 'Examples: 20241001, 20241001T131001, 2024-10-01, 2024-10-01T13:10:01 or 2024-10-01' + ) + ) + + parser.add_argument( + '--updated_range_to', type=str, + help = ( + 'Updated date range to - local date/time in ISO 8601 format without timezone \n' + 'YYYYMMDD, YYYYMMDDTHHMMSS, YYYY-MM-DD, YYYY-MM-DDTHH:MM:SS. \n' + 'Examples: 20241001, 20241001T131001, 2024-10-01, 2024-10-01T13:10:01 or 2024-10-01' + ) + ) + def run(self): """ @@ -53,21 +79,54 @@ def run(self): Args: None """ + ASYNC_CLEANUP_SLEEP = 60 * 60 + + BATCH_SIZE = self.opt.pagesize + if BATCH_SIZE is None: + BATCH_SIZE = 10000 + + updated_from = None + updated_to = None + updated_from_str = self.opt.updated_range_from + updated_to_str = self.opt.updated_range_to + if updated_from_str is not None: + try: + updated_from = self.date_to_seconds(updated_from_str) + except Exception as ex: + log.error(f"Input date/time error: {ex}") + exit() + if updated_to_str is not None: + try: + updated_to = self.date_to_seconds(updated_to_str) + except Exception as ex: + log.error(f"Input date/time error: {ex}") + exit() + + if updated_from is not None and updated_to is not None: + time_range = Q(updateTime__gte=updated_from) & Q(updateTime__lte=updated_to) + time_range_str = f"updated between: {updated_from_str} and {updated_to_str}" + elif updated_to is not None: + time_range = Q(updateTime__lte=updated_to) + time_range_str = f"updated before: {updated_to_str}" + else: + max_age_ts = int(time.time()) - django.conf.settings.DAEMONS_EXPUNGE_MAX_AGE_SEC + min_age_ts = max_age_ts - django.conf.settings.DAEMONS_EXPUNGE_MAX_AGE_SEC + time_range = Q(updateTime__gte=min_age_ts) & Q(updateTime__lte=max_age_ts) + time_range_str = f"updated between: {self.seconds_to_date(min_age_ts)} and {self.seconds_to_date(max_age_ts)}" + + last_id = 0 # keep running until terminated while not self.terminated(): - currentTime=int(time.time()) - timeDelta=django.conf.settings.DAEMONS_CHECK_IDENTIFIER_ASYNC_STATUS_TIMESTAMP + # retrieve identifiers with update timestamp within a date range + filter = time_range & Q(id__gt=last_id) + refIdsQS = self.refIdentifier.objects.filter(filter).order_by("pk")[: BATCH_SIZE] - # retrieve identifiers with update timestamp within a set range - refIdsQS = self.refIdentifier.objects.filter( - updateTime__lte=currentTime, - updateTime__gte=currentTime - timeDelta - ).order_by("-pk")[: django.conf.settings.DAEMONS_MAX_BATCH_SIZE] - - log.info("Checking ref Ids in the range: " + str(currentTime) + " - " + str(currentTime - timeDelta)) + log.info(f"Checking ref Ids: {time_range_str}, filter: {filter}") + log.info(f"Checking ref Ids returned: {len(refIdsQS)} records") # iterate over query set to check each identifier status for refId in refIdsQS: + last_id = refId.pk # set status for each handle system identifierStatus = { @@ -109,7 +168,20 @@ def run(self): "Delete identifier: " + refId.identifier + " from refIdentifier table.") self.deleteRecord(self.refIdentifier, refId.pk, record_type='refId', identifier=refId.identifier) - self.sleep(django.conf.settings.DAEMONS_BATCH_SLEEP) + if len(refIdsQS) < BATCH_SIZE: + if updated_from is not None or updated_to is not None: + log.info(f"Finished - Checking ref Ids: {time_range_str}, filter: {filter}") + exit() + else: + log.info(f"Sleep {ASYNC_CLEANUP_SLEEP} seconds before processing next time range.") + self.sleep(ASYNC_CLEANUP_SLEEP) + last_id = 0 + min_age_ts = max_age_ts + max_age_ts = int(time.time()) - django.conf.settings.DAEMONS_EXPUNGE_MAX_AGE_SEC + time_range = Q(updateTime__gte=min_age_ts) & Q(updateTime__lte=max_age_ts) + time_range_str = f"updated between: {self.seconds_to_date(min_age_ts)} and {self.seconds_to_date(max_age_ts)}" + else: + self.sleep(django.conf.settings.DAEMONS_BATCH_SLEEP) def deleteRecord(self, queue, primary_key, record_type=None, identifier=None): """ @@ -125,13 +197,49 @@ def deleteRecord(self, queue, primary_key, record_type=None, identifier=None): try: # check if the record to be deleted is a refIdentifier record if (record_type is not None and record_type == 'refId'): - log.info(type(queue)) - log.info("Delete refId: " + str(primary_key)) - queue.objects.filter(id=primary_key).delete() + log.info(f"Delete from {queue.__name__} refId: " + str(primary_key)) + with transaction.atomic(): + obj = queue.objects.select_for_update().get(id=primary_key) + obj.delete() else: - log.info("Delete async entry: " + str(primary_key)) - queue.objects.filter(seq=primary_key).delete() + log.info(f"Delete async queue {queue.__name__} entry: " + str(primary_key)) + with transaction.atomic(): + obj = queue.objects.select_for_update().get(seq=primary_key) + obj.delete() except Exception as e: log.error("Exception occured while processing identifier '" + identifier + "' for '" + record_type + "' table") log.error(e) + + + def date_to_seconds(self, date_time_str: str) -> int: + """ + Convert date/time string to seconds since the Epotch. + For example: + 2024-01-01 00:00:00 => 1704096000 + 2024-10-10 00:00:00 => 1728543600 + + Parameter: + date_time_str: A date/time string in in ISO 8601 format without timezone. + For example: 'YYYYMMDD, YYYYMMDDTHHMMSS, YYYY-MM-DD, YYYY-MM-DDTHH:MM:SS. + + Returns: + int: seconds since the Epotch + + """ + + # Parse the date and time string to a datetime object + dt_object = parse(date_time_str) + + # Convert the datetime object to seconds since the Epoch + seconds_since_epoch = int(dt_object.timestamp()) + + return seconds_since_epoch + + + def seconds_to_date(self, seconds_since_epoch: int) -> str: + dt_object = datetime.fromtimestamp(seconds_since_epoch) + + # Format the datetime object to a string in the desired format + formatted_time = dt_object.strftime("%Y-%m-%dT%H:%M:%S") + return formatted_time \ No newline at end of file diff --git a/ezidapp/management/commands/proc-cleanup-async-queues_v1.py b/ezidapp/management/commands/proc-cleanup-async-queues_v1.py new file mode 100644 index 000000000..d9f578ff1 --- /dev/null +++ b/ezidapp/management/commands/proc-cleanup-async-queues_v1.py @@ -0,0 +1,137 @@ +#! /usr/bin/env python + +# Copyright©2021, Regents of the University of California +# http://creativecommons.org/licenses/BSD + +""" + +Clean up entries that are successfully completed or are a 'no-op' + +Identifier operation entries are retrieved by querying the database; +operations that successfully completed or are a no-op are deleted based on +pre-set interval. + +""" + +import logging +import time + +import django.conf +import django.conf +import django.db +import django.db.transaction + +import ezidapp.management.commands.proc_base +import ezidapp.models.identifier +import ezidapp.models.shoulder +from django.db.models import Q + +log = logging.getLogger(__name__) + +class Command(ezidapp.management.commands.proc_base.AsyncProcessingCommand): + help = __doc__ + name = __name__ + + setting = 'DAEMONS_QUEUE_CLEANUP_ENABLED' + + queueType = { + 'crossref': ezidapp.models.async_queue.CrossrefQueue, + 'datacite': ezidapp.models.async_queue.DataciteQueue, + 'search': ezidapp.models.async_queue.SearchIndexerQueue + } + + refIdentifier = ezidapp.models.identifier.RefIdentifier + + def __init__(self): + super().__init__() + + + def run(self): + """ + Checks for the successfully processed identifier + + Args: + None + """ + # keep running until terminated + while not self.terminated(): + currentTime=int(time.time()) + timeDelta=django.conf.settings.DAEMONS_CHECK_IDENTIFIER_ASYNC_STATUS_TIMESTAMP + + # retrieve identifiers with update timestamp within a set range + refIdsQS = self.refIdentifier.objects.filter( + updateTime__lte=currentTime, + updateTime__gte=currentTime - timeDelta + ).order_by("-pk")[: django.conf.settings.DAEMONS_MAX_BATCH_SIZE] + + log.info("Checking ref Ids in the range: " + str(currentTime) + " - " + str(currentTime - timeDelta)) + + # iterate over query set to check each identifier status + for refId in refIdsQS: + + # set status for each handle system + identifierStatus = { + 'crossref' : False, + 'datacite' : False, + 'search' : False + } + + # check if the identifier is processed for each background job + for key, value in self.queueType.items(): + queue = value + + qs = queue.objects.filter( + Q(refIdentifier_id=refId.pk) + ) + + # if the identifier does not exist in the table + # mark as 'OK' to delete from the refIdentifier + if not qs: + identifierStatus[key] = True + continue + + for task_model in qs: + log.info('-' * 10) + log.info("Running job for identifier: " + refId.identifier + " in " + key + " queue") + + # delete identifier if the status is successfully synced or + # not applicable for this handle system + if (task_model.status==queue.SUCCESS or task_model.status==queue.IGNORED): + log.info( + "Delete identifier: " + refId.identifier + " in " + key + " queue") + identifierStatus[key] = True + self.deleteRecord(queue, task_model.pk, record_type=key, identifier=refId.identifier) + + # if the identifier is successfully processed for all the handle system + # delete it from the refIdentifier table + if all(i for i in identifierStatus.values()): + log.info( + "Delete identifier: " + refId.identifier + " from refIdentifier table.") + self.deleteRecord(self.refIdentifier, refId.pk, record_type='refId', identifier=refId.identifier) + + self.sleep(django.conf.settings.DAEMONS_BATCH_SLEEP) + + def deleteRecord(self, queue, primary_key, record_type=None, identifier=None): + """ + Deletes the identifier record that has been successfully completed + based on the record's primary key provided + + Args: + queue : async handle queue + primary_key (str): primary key of the record to be deleted. + record_type (str): . Defaults to None. + identifier (str): . Defaults to None. + """ + try: + # check if the record to be deleted is a refIdentifier record + if (record_type is not None and record_type == 'refId'): + log.info(type(queue)) + log.info("Delete refId: " + str(primary_key)) + queue.objects.filter(id=primary_key).delete() + else: + log.info("Delete async entry: " + str(primary_key)) + queue.objects.filter(seq=primary_key).delete() + except Exception as e: + log.error("Exception occured while processing identifier '" + identifier + "' for '" + + record_type + "' table") + log.error(e) diff --git a/ezidapp/management/commands/update-async-queue-for-delete.py b/ezidapp/management/commands/update-async-queue-for-delete.py new file mode 100644 index 000000000..2458c2359 --- /dev/null +++ b/ezidapp/management/commands/update-async-queue-for-delete.py @@ -0,0 +1,128 @@ +#! /usr/bin/env python + +# Copyright©2024, Regents of the University of California +# http://creativecommons.org/licenses/BSD + +""" + +Delete async queue entries identified by the refIdentifiers listed in the input file. + +""" + +import logging +import time +import csv +from typing import List + +import django.conf +import django.conf +import django.db +import django.db.transaction + +import ezidapp.management.commands.proc_base +import ezidapp.models.identifier +import ezidapp.models.shoulder +from django.db.models import Q +import impl +import impl.nog_sql.util + +log = logging.getLogger(__name__) + +class Command(django.core.management.BaseCommand): + help = __doc__ + name = __name__ + + queueType = { + 'crossref': ezidapp.models.async_queue.CrossrefQueue, + 'datacite': ezidapp.models.async_queue.DataciteQueue, + 'search': ezidapp.models.async_queue.SearchIndexerQueue + } + + refIdentifier = ezidapp.models.identifier.RefIdentifier + + def __init__(self): + super().__init__() + + def add_arguments(self, parser): + super().add_arguments(parser) + parser.add_argument('-i', '--id_file', type=str, help='Identifier file', required=True) + parser.add_argument('--debug', action='store_true', help='Debug level logging') + + + def handle(self, *args, **opt): + impl.nog_sql.util.log_setup(__name__, opt.get('debug')) + + log.info(f"Update identifier status in the async queues so they can be cleaned up by the proc-cleanup-async-queues job.") + + identifier_file = opt.get('id_file') + identifier_list = self.loadIdFile(identifier_file) + + log.info(f"Identifiers are provided by input file {identifier_file}") + + for identifier in identifier_list: + try: + queue_entry = self.refIdentifier.objects.get(identifier=identifier) + refId = queue_entry.id + log.info(f"Update refIdentifier status: refID={refId}, identifier={identifier}") + + updated = False + # check if the identifier is in each queue + for key, value in self.queueType.items(): + queue_name = key + queue_model = value + + qs = queue_model.objects.filter(Q(refIdentifier_id=refId)) + + if not qs: + log.info(f"refID={refId}, identifier={identifier} is not in {queue_name} queue, skip") + continue + + for queue_entry in qs: + log.info(f"Update identifier: {refId} in {queue_name} queue") + self.update_status(queue_model, queue_entry.pk, queue_model.IGNORED, refId=refId, identifier=identifier) + updated = True + + if updated: + current_time=int(time.time()) + try: + self.refIdentifier.objects.filter(id=refId).update(updateTime=current_time) + except Exception as e: + log.error(f"error:{e}") + + except self.refIdentifier.DoesNotExist: + log.error(f"Identifier {identifier} does not exist in RefIdentifier table.") + except Exception as ex: + log.error(f"Retrieve identifier {identifier} had error: {ex}") + + + def update_status(self, queue, primary_key, status, refId=None, identifier=None): + try: + queue.objects.filter(seq=primary_key).update(status=status) + log.info(f"Updated {queue.__name__} entry status to {status}: seq={primary_key}, refID={refId}, identifier={identifier}") + except Exception as e: + log.error(f"Exception occured while updating {queue.__name__} entry status to {status}: seq={primary_key}, refID={refId}, identifier={identifier}") + log.error(f"Error: {e}") + + + def loadIdFile(self, filename: str)-> List[str]: + """ + Read identifiers from a CSV file. + The identifiers are listed in the 'identifer' column. + Args: + filename (str): input filename + + Returns: + List of identifiers from the input file + """ + if filename is None: + return None + + id_list = [] + with open(filename) as file: + csvreader = csv.DictReader(file, delimiter='\t') + for line in csvreader: + identifier = line.get('identifier') + if identifier: + id_list.append(identifier) + + return id_list diff --git a/impl/ui_home.py b/impl/ui_home.py index aa31a6672..12d08f505 100644 --- a/impl/ui_home.py +++ b/impl/ui_home.py @@ -35,6 +35,24 @@ def index(request): "/id/" + urllib.parse.quote(result.split()[1], ":/") ) # ID Details page +def ajax_index_form(request): + if request.method not in ["GET"]: + return impl.ui_common.methodNotAllowed(request) + d = {'menu_item': 'ui_home.index'} + d['prefixes'] = sorted( + django.conf.settings.TEST_SHOULDER_DICT, key=lambda p: p['namespace'].lower() + ) + d['form_placeholder'] = True # is this necessary? + d = impl.ui_create.simple_form(request, d) + result = d['id_gen_result'] + if result == 'edit_page': + # noinspection PyUnresolvedReferences + # return impl.ui_common.render(request, 'index', d) # ID Creation page + return impl.ui_common.render(request, 'create/_home_demo_form', d) + # return render(request, 'create/home_demo_form.html', d) + elif result == 'bad_request': + return impl.ui_common.badRequest(request) + def learn(request): if request.method != "GET": diff --git a/settings/urls.py b/settings/urls.py index 5d7c2917d..f3763377e 100644 --- a/settings/urls.py +++ b/settings/urls.py @@ -33,6 +33,7 @@ urlpatterns = [ # UI - RENDERED FROM TEMPLATES IN INFO REPOSITORY django.urls.re_path("^$", impl.ui_home.index, name="ui_home.index"), + django.urls.re_path("^home/ajax_index_form$", impl.ui_home.ajax_index_form, name="ui_home.ajax_index_form"), django.urls.re_path("^learn/$", impl.ui_home.learn, name="ui_home.learn"), django.urls.re_path("^learn/ark_open_faq$", impl.ui_home.ark_open_faq, name="ui_home.ark_open_faq"), django.urls.re_path("^learn/crossref_faq$", impl.ui_home.crossref_faq, name="ui_home.crossref_faq"), diff --git a/static_src/images/icon_cross.svg b/static_src/images/icon_cross.svg index 6de30c8bb..91fabb948 100644 --- a/static_src/images/icon_cross.svg +++ b/static_src/images/icon_cross.svg @@ -1,19 +1,3 @@ - - - - - - - - - - + + + diff --git a/static_src/images/icon_minus-sign.svg b/static_src/images/icon_minus-sign.svg index 3ac443deb..4ef4fff4e 100644 --- a/static_src/images/icon_minus-sign.svg +++ b/static_src/images/icon_minus-sign.svg @@ -6,7 +6,7 @@ - diff --git a/static_src/images/icon_plus-sign.svg b/static_src/images/icon_plus-sign.svg index 049934091..2c3c19a58 100644 --- a/static_src/images/icon_plus-sign.svg +++ b/static_src/images/icon_plus-sign.svg @@ -6,7 +6,7 @@ - diff --git a/static_src/javascripts/simple_create_ajax.js b/static_src/javascripts/simple_create_ajax.js new file mode 100644 index 000000000..f371dc0b9 --- /dev/null +++ b/static_src/javascripts/simple_create_ajax.js @@ -0,0 +1,29 @@ +document.getElementById('form-shoulder').value = document.querySelectorAll('input[name=selshoulder]')[0].value; + +document.querySelectorAll('input[name="selshoulder"]').forEach(radio => { + radio.addEventListener('change', function () { + var profile; + if(this.value.startsWith('ark')) { + profile = 'erc'; + } else { + profile = 'datacite'; + } + + const form = document.querySelector('#create_form'); + const formData = new FormData(form); + formData.set('current_profile', profile); + + // Convert FormData to a query string + const queryString = new URLSearchParams(formData).toString(); + fetch(`/home/ajax_index_form?${queryString}`, { + headers: { + 'X-CSRFToken': document.querySelector('input[name="csrfmiddlewaretoken"]').value, + }, + }) + .then(response => response.text()) + .then(data => { + document.getElementById('form-container').innerHTML = data; // Replace form container HTML + document.getElementById('form-shoulder').value = this.value; // needs to happen after the form is replaced + }); + }); +}); \ No newline at end of file diff --git a/static_src/stylesheets/main.css b/static_src/stylesheets/main.css index 15769cd93..03d898e6c 100644 --- a/static_src/stylesheets/main.css +++ b/static_src/stylesheets/main.css @@ -299,10 +299,21 @@ textarea { line-height: inherit; } a { - color: #00778a; + color: #006170; text-decoration: none; } a:hover, a:focus { - color: #00353e; + color: #005160; + text-decoration: underline; } + a:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; } + +a.home-banner__lead-button{ + color: #FFFFFF; + text-decoration: none; } + a:hover, a:focus { + /* color: #FFFFFF; */ text-decoration: underline; } a:focus { outline: thin dotted; @@ -364,6 +375,40 @@ hr { overflow: visible; clip: auto; } +.skip-link { + position: absolute; + top: -40px; /* Hides it above the viewport */ + left: 0; + background-color: #FFF; /* High-contrast background */ + color: #0000ff; /* High-contrast text */ + padding: 8px 16px; + z-index: 1000; + text-decoration: none; +} + +/* Style the radio buttons */ +.fcontrol__radio-button-stacked:focus + span { + outline: 3px solid #007BFF; /* Blue focus outline */ + outline-offset: 3px; /* Space between outline and text */ + border-radius: 4px; /* Smooth edges */ + background-color: #e9f5ff; /* Light blue background */ +} + +/* Style the labels to highlight the entire clickable area */ +.fcontrol__radio-label-inline:focus-within { + border: 2px solid #007BFF; + border-radius: 4px; + padding: 2px; + background-color: #e9f5ff; +} + +.skip-link:focus { + top: 0; /* Brings it into view when focused */ + left: 0; + outline: 3px solid #fff; /* Optional focus outline */ +} + + .container { margin-right: auto; margin-left: auto; @@ -1371,7 +1416,7 @@ tbody.collapse.in { padding: 0 5px; color: #ccc; } .breadcrumb > .active { - color: #777777; } + color: 555555; } .alert { padding: 15px; @@ -2467,7 +2512,7 @@ h2.id-creation { .api table.leftheaders { margin: 5 0 15 5; } .api table.leftheaders tr > td:first-child { - background-color: #c6b082; + background-color: #f0f0f0; font-size: 0.9em; font-weight: bold; } @@ -2633,7 +2678,7 @@ blockquote p { .panel-body .heading_3 { margin-top: 18px; - color: #d8402f; + color: #A32C1F; font-weight: bold; font-size: 1.1em; } diff --git a/static_src/stylesheets/main2.min.css b/static_src/stylesheets/main2.min.css index 11c7a4c53..c48eadda7 100644 --- a/static_src/stylesheets/main2.min.css +++ b/static_src/stylesheets/main2.min.css @@ -71,8 +71,8 @@ Selector pattern using above mixin: margin-top: 15px; } .button__pop, .header__loginout-link, .home-banner__lead-button { - background: -webkit-gradient(linear, left top, left bottom, from(#ff8e28), to(#ff701c)); - background: linear-gradient(to bottom, #ff8e28, #ff701c); } + background: -webkit-gradient(linear, left top, left bottom, from(#8a1930), to(#741629)); + background: linear-gradient(to bottom, #8a1930, #741629); } .button__plus, .button__minus { position: relative; @@ -162,7 +162,7 @@ Selector pattern using above mixin: -ms-flex-align: center; align-items: center; margin: 0 0.5em 0 0; - color: #00778a; + color: #005f70; font-size: 1.2em; font-weight: bold; } @media (min-width: 768px) { @@ -180,7 +180,7 @@ Selector pattern using above mixin: display: -ms-flexbox; display: flex; margin: 0 0 0.5em; - color: #00778a; + color: #005f70; font-size: 1.05em; font-weight: bold; } @@ -224,7 +224,7 @@ Selector pattern using above mixin: width: 1.8em; height: 1.8em; margin: 0 0.8em 0 0; - background: #ff7f22; + background: #7c182c; color: white; font-size: 0.8em; font-weight: normal; } @@ -322,10 +322,10 @@ Selector pattern using above mixin: transition: border-color 0.15s ease-in-out 0s, -webkit-box-shadow 0.15s ease-in-out 0s; transition: border-color 0.15s ease-in-out 0s, box-shadow 0.15s ease-in-out 0s; transition: border-color 0.15s ease-in-out 0s, box-shadow 0.15s ease-in-out 0s, -webkit-box-shadow 0.15s ease-in-out 0s; - border-color: #00778a; + border-color: #005f70; outline: 0; - -webkit-box-shadow: 0 0 8px rgba(0, 119, 138, 0.6); - box-shadow: 0 0 8px rgba(0, 119, 138, 0.6); } + -webkit-box-shadow: 0 0 8px rgba(0, 95, 112, 0.6); + box-shadow: 0 0 8px rgba(0, 95, 112, 0.6); } .fcontrol__group-stacked, .fcontrol__group-stacked--invalid { display: -webkit-box; @@ -353,7 +353,7 @@ Selector pattern using above mixin: .fcontrol__text-label-stacked { margin: 0 2px; - color: #00778a; + color: #005f70; font-size: 1em; } @media (min-width: 768px) { .fcontrol__text-label-stacked { @@ -386,7 +386,7 @@ Selector pattern using above mixin: .fcontrol__text-label-inline { margin: 0 5px 0 0; - color: #00778a; + color: #005f70; font-size: 1em; } @media (min-width: 768px) { .fcontrol__text-label-inline { @@ -442,7 +442,7 @@ Selector pattern using above mixin: .fcontrol__textarea-label-stacked { margin: 0 0 2px; - color: #00778a; + color: #005f70; font-size: 1em; } @media (min-width: 768px) { .fcontrol__textarea-label-stacked { @@ -457,7 +457,7 @@ Selector pattern using above mixin: .fcontrol__textarea-label-inline { margin: 0 5px 0 0; - color: #00778a; + color: #005f70; font-size: 1em; } @media (min-width: 768px) { .fcontrol__textarea-label-inline { @@ -525,7 +525,7 @@ Selector pattern using above mixin: align-items: center; } } .fcontrol__select-label-stacked, .fcontrol__select-label-inline, .fcontrol__checkbox-label-inline { - color: #00778a; + color: #005f70; font-size: 1em; } @media (min-width: 768px) { .fcontrol__select-label-stacked, .fcontrol__select-label-inline, .fcontrol__checkbox-label-inline { @@ -573,7 +573,7 @@ Selector pattern using above mixin: align-self: stretch; } .fcontrol__radio-label-stacked, .fcontrol__radio-label-inline { - color: #00778a; + color: #005f70; font-size: 1em; font-weight: normal; cursor: pointer; } @@ -605,7 +605,7 @@ Selector pattern using above mixin: background: #7c7e7f; } .fcontrol__radio-button-stacked:focus + span::before { - border-color: #00778a; } + border-color: #005f70; } .fcontrol__radio-button-inline { font-size: 1em; } @@ -756,15 +756,15 @@ Selector pattern using above mixin: .heading__pop { margin: 0; padding: 0.8em 10px; - background: -webkit-gradient(linear, left top, left bottom, from(#ff8e28), to(#ff701c)); - background: linear-gradient(to bottom, #ff8e28, #ff701c); + background: -webkit-gradient(linear, left top, left bottom, from(#8a1930), to(#741629)); + background: linear-gradient(to bottom, #8a1930, #741629); color: black; font-size: 1.6em; font-weight: bold; } .heading__noicon { margin: 1em 0; - color: #00778a; + color: #005f70; font-size: 1.2em; font-weight: bold; text-transform: uppercase; } @@ -774,7 +774,7 @@ Selector pattern using above mixin: background-repeat: no-repeat; background-position: left center; background-size: 30px; - color: #00778a; + color: #005f70; text-transform: uppercase; } .heading__icon-1, .heading__icon-2, .heading__icon-3, .heading__icon-4, .heading__icon-5, .heading__icon-6, .heading__icon-7, .heading__icon-8, .heading__icon-9, .heading__icon-10 { @@ -847,24 +847,24 @@ Selector pattern using above mixin: .link__primary, .download__link, .footer__news-feed-link, .footer__news-link, .login-modal__forgot, .dashboard__csv-link { border-bottom: 1px solid #beb6af; - color: #00778a; } + color: #005f70; } .link__primary:hover, .download__link:hover, .footer__news-feed-link:hover, .footer__news-link:hover, .login-modal__forgot:hover, .dashboard__csv-link:hover { - border-bottom: 1px solid #00778a; - color: #00778a; } + border-bottom: 1px solid #005f70; + color: #005f70; } .link__over-beige, .footer__link, .footer__copyright-link { border-bottom: 1px solid #beb6af; background: #ece5d6; - color: #005695; } + color: #003961; } .link__over-beige:hover, .footer__link:hover, .footer__copyright-link:hover { - border-bottom: 1px solid #005695; - color: #005695; } + border-bottom: 1px solid #003961; + color: #003961; } .link__pop, .login-modal__sign-up-link { - color: #ff7f22; } + color: #7c182c; } .link__pop:hover, .login-modal__sign-up-link:hover { - border-bottom: 1px solid #ff7f22; - color: #ff7f22; } + border-bottom: 1px solid #7c182c; + color: #7c182c; } .download__link { margin-left: 5px; @@ -886,8 +886,8 @@ Selector pattern using above mixin: .literal-block, .pre-block { margin: 1em 0; padding: 0.5em; - border: 1px dotted #beb6af; - background-color: #edeae5; + border: 1px dotted #e6e6e6; + background-color: #f0f0f0; font-size: 0.85em; } .literal-block { @@ -965,8 +965,9 @@ Selector pattern using above mixin: max-width: 1000px; margin: 0 auto 10px; padding: 10px; - background: -webkit-gradient(linear, left top, left bottom, from(#ff8e28), to(#ff701c)); - background: linear-gradient(to bottom, #ff8e28, #ff701c); } + background: -webkit-gradient(linear, left top, left bottom, from(#8a1930), to(#741629)); + background: linear-gradient(to bottom, #8a1930, #741629); + color: white; } @media (min-width: 480px) { .customize-table { -webkit-box-orient: horizontal; @@ -1144,7 +1145,7 @@ Selector pattern using above mixin: justify-content: flex-end; max-width: 1000px; margin: 10px auto; - color: #ff7f22; } } + color: #005f70; } } .header { display: -webkit-box; @@ -1257,7 +1258,7 @@ Selector pattern using above mixin: background-repeat: no-repeat; background-position: 10px center; background-size: 23px 23px; - color: #00778a; + color: #005f70; font-size: 0.9em; font-weight: bold; text-decoration: none; @@ -1307,7 +1308,7 @@ Selector pattern using above mixin: border: 1px solid #beb6af; border-top: none; background-color: white; - color: #00778a; + color: #005f70; font-size: 0.9em; font-weight: bold; text-decoration: none; @@ -1325,17 +1326,17 @@ Selector pattern using above mixin: .header__nav-subitem:nth-of-type(6) { border-bottom: 1px dotted black; } .header__nav-subitem:nth-of-type(8) { - background: -webkit-gradient(linear, left top, left bottom, from(#ff8e28), to(#ff701c)); - background: linear-gradient(to bottom, #ff8e28, #ff701c); + background: -webkit-gradient(linear, left top, left bottom, from(#8a1930), to(#741629)); + background: linear-gradient(to bottom, #8a1930, #741629); color: white; font-weight: bold; } .header__nav-subitem:hover:nth-of-type(8), .header__nav-subitem:focus:nth-of-type(8) { - background: -webkit-gradient(linear, left top, left bottom, from(#ff8e28), to(#ff701c)); - background: linear-gradient(to bottom, #ff8e28, #ff701c); + background: -webkit-gradient(linear, left top, left bottom, from(#8a1930), to(#741629)); + background: linear-gradient(to bottom, #8a1930, #741629); text-decoration: underline; } .header__nav-subitem:hover { - background: -webkit-gradient(linear, left top, left bottom, from(#c6b082), to(#b4975a)); - background: linear-gradient(to bottom, #c6b082, #b4975a); + background: -webkit-gradient(linear, left top, left bottom, from(#d5c5a5), to(#d0be95)); + background: linear-gradient(to bottom, #d5c5a5, #d0be95); color: white; font-weight: bold; text-decoration: none; } } @@ -1364,7 +1365,7 @@ Selector pattern using above mixin: .home-banner { height: 64vw; margin: 0 -10px; - background-color: #00778a; + background-color: #005f70; background-repeat: no-repeat; background-position: bottom center; background-size: 100%; } @@ -1404,8 +1405,8 @@ Selector pattern using above mixin: height: auto; margin: 2vw 0 0; padding: 1.5vw 5vw 1.5vw 2vw; - background: url("../images/icon_double-arrow.svg") no-repeat, -webkit-gradient(linear, left top, left bottom, from(#ff8e28), to(#ff701c)); - background: url("../images/icon_double-arrow.svg") no-repeat, linear-gradient(to bottom, #ff8e28, #ff701c); + background: url("../images/icon_double-arrow.svg") no-repeat, -webkit-gradient(linear, left top, left bottom, from(#8a1930), to(#741629)); + background: url("../images/icon_double-arrow.svg") no-repeat, linear-gradient(to bottom, #8a1930, #741629); background-position: right 10px center; background-size: 0.8em; font-size: 2vw; } @@ -1419,8 +1420,8 @@ Selector pattern using above mixin: .login-menu__container { margin: 0 -10px; - background: -webkit-gradient(linear, left top, left bottom, from(#c6b082), to(#b4975a)); - background: linear-gradient(to bottom, #c6b082, #b4975a); } + background: -webkit-gradient(linear, left top, left bottom, from(#d5c5a5), to(#d0be95)); + background: linear-gradient(to bottom, #d5c5a5, #d0be95); } .login-menu { display: -webkit-box; @@ -1471,10 +1472,10 @@ Selector pattern using above mixin: .login-menu__link:not(:first-child), .login-menu__link--selected:not(:first-child) { border-top: none; } } .login-menu__link:hover, .login-menu__link--selected:hover, .login-menu__link:focus, .login-menu__link--selected:focus { - color: white; } + color: #003961; } .login-menu__link--selected { - color: white; } + color: #003961; } @media (min-width: 480px) { .login-menu__link--selected::before { position: absolute; @@ -1513,7 +1514,7 @@ Selector pattern using above mixin: .login-modal__title { margin: 0 0 10px; - color: #00778a; + color: #005f70; font-size: 1.3em; } .login-modal__close { @@ -1521,7 +1522,9 @@ Selector pattern using above mixin: height: 15px; padding: 2px; background-color: white; - cursor: pointer; } + cursor: pointer; + -o-object-fit: contain; + object-fit: contain; } .login-modal__form { display: -webkit-box; @@ -1574,7 +1577,7 @@ Selector pattern using above mixin: .proxy-modal__title { margin: 20px 0 10px; - color: #00778a; + color: #005f70; font-size: 1.3em; } .pagination { @@ -1614,7 +1617,7 @@ Selector pattern using above mixin: .pagination__next { border: none; background-color: transparent; - color: #00778a; } + color: #005f70; } .pagination__label { font-weight: normal; } @@ -1672,7 +1675,7 @@ Selector pattern using above mixin: position: relative; margin: 0 0 30px; padding: 7px 0 7px 40px; - color: #00778a; + color: #005f70; font-size: 1.1em; font-weight: bold; text-transform: uppercase; } @@ -1777,7 +1780,7 @@ Selector pattern using above mixin: padding: 0.5em 10px; border: 1px solid #beb6af; background: white; - color: #00778a; + color: #005f70; font-size: 1.2em; font-weight: normal; cursor: pointer; @@ -1813,7 +1816,7 @@ Selector pattern using above mixin: top: -9999px; left: -9999px; padding: 0.5em; - background: #b4975a; + background: #d0be95; text-align: left; } @media (min-width: 480px) { .table1 th { @@ -1859,14 +1862,14 @@ Selector pattern using above mixin: .table2 thead { display: block; float: left; - border-top: 1px solid #b4975a; - border-left: 1px solid #b4975a; } + border-top: 1px solid #d0be95; + border-left: 1px solid #d0be95; } .table2 thead tr { display: block; } .table2 th { display: block; position: relative; - background: #c7b183; + background: #d6c8a4; text-align: left; } .table2 .sorting { position: absolute; @@ -1900,7 +1903,7 @@ Selector pattern using above mixin: -webkit-box-flex: 1; -ms-flex: 1 0 auto; flex: 1 0 auto; - border-top: 1px solid #b4975a; } + border-top: 1px solid #d0be95; } .table2 td { display: block; } .table2 th, @@ -1908,8 +1911,8 @@ Selector pattern using above mixin: width: 10em; margin: 0; padding: 0.5em; - border-right: 1px solid #b4975a; - border-bottom: 1px solid #b4975a; + border-right: 1px solid #d0be95; + border-bottom: 1px solid #d0be95; text-overflow: ellipsis; overflow: hidden; vertical-align: top; } @@ -1936,7 +1939,7 @@ Selector pattern using above mixin: width: auto; overflow: visible; } .table2 td:first-child { - border-left: 1px solid #b4975a; } } + border-left: 1px solid #d0be95; } } /*! * Copyright©2021, Regents of the University of California @@ -1949,12 +1952,12 @@ Selector pattern using above mixin: margin: 10px auto; border-collapse: collapse; } .table3 thead tr { - background: #c7b183; } + background: #d6c8a4; } .table3 th, .table3 td { min-width: 80px; padding: 0.5em; - border: 1px solid #b4975a; + border: 1px solid #d0be95; vertical-align: top; /* https://kenneth.io/blog/2012/03/04/word-wrapping-hypernation-using-css/ */ /* Warning: Needed for oldIE support, but words are broken up letter-by-letter */ @@ -2006,16 +2009,16 @@ Selector pattern using above mixin: left: -9999px; } .table3 tbody { margin: 0 0 15px; - border: 1px solid #b4975a; } + border: 1px solid #d0be95; } .table3 tbody tr:nth-child(even) { - border-top: 1px solid #b4975a; - border-bottom: 1px solid #b4975a; - background: #c7b183; } + border-top: 1px solid #d0be95; + border-bottom: 1px solid #d0be95; + background: #d6c8a4; } .table3 td { position: relative; margin: 0 0 0 150px; padding: 6px; - border-left: 1px solid #b4975a; } + border-left: 1px solid #d0be95; } .table3 td::before { position: absolute; top: 6px; @@ -2171,7 +2174,7 @@ Selector pattern using above mixin: .general__note { max-width: 1000px; margin: 30px auto; - color: #00778a; + color: #005f70; font-size: 1.4em; font-weight: bold; } @media (min-width: 768px) { @@ -2235,7 +2238,7 @@ Selector pattern using above mixin: .home__identifier-lead-heading { margin: 0; - color: #00778a; + color: #005f70; font-size: 2em; line-height: 1; text-transform: uppercase; } @@ -2303,7 +2306,7 @@ Selector pattern using above mixin: .loading__text { position: relative; - color: #00778a; + color: #005f70; font-size: 1.2em; font-weight: bold; text-align: center; } diff --git a/templates/create/_datacite_publisher.html b/templates/create/_datacite_publisher.html index 15ec88d56..8aa6d50b7 100644 --- a/templates/create/_datacite_publisher.html +++ b/templates/create/_datacite_publisher.html @@ -1,8 +1,8 @@ {% load i18n %} -
+
-
{% trans "Publisher" %}
+
{% trans "Publisher" %}
{% for p_field in form.publisher_form %} diff --git a/templates/create/_datacite_version.html b/templates/create/_datacite_version.html index 540de2890..b6f02f7ad 100644 --- a/templates/create/_datacite_version.html +++ b/templates/create/_datacite_version.html @@ -1,8 +1,8 @@ {% load i18n %} -
+
-
{% trans "Version" %}
+
{% trans "Version" %}
{% include "create/_datacite_inlinetext.html" with field=form.nonrepeating_form.version %}
diff --git a/templates/create/_home_demo_form.html b/templates/create/_home_demo_form.html new file mode 100644 index 000000000..575c87cb6 --- /dev/null +++ b/templates/create/_home_demo_form.html @@ -0,0 +1,40 @@ +{% load i18n %} +{% load layout_extras %} +
+ + +{% csrf_token %} + +
+ +
+ {% trans "Describe the identified object" %} +
+ +
+ {{ form.non_field_errors }} + {% for field in form %} +
+
+ {% if field|fieldtype == "TextInput" %} + + {{ field|add_attributes:"fcontrol__text-field-stacked" }} + {% else %} + + {{ field|add_attributes:"fcontrol__select" }} + {% endif %} + {% if field.errors %} + {% for error in field.errors %}{{ error|escape }}{% endfor %} + {% endif %} +
+
+ {% endfor %} + +
+ +
+ +
+
+
\ No newline at end of file diff --git a/templates/includes/login-modal.html b/templates/includes/login-modal.html index a3fa07491..451860850 100644 --- a/templates/includes/login-modal.html +++ b/templates/includes/login-modal.html @@ -2,7 +2,9 @@