From dd4b305f4fe71162e291fcf7dd5c991adf6ae067 Mon Sep 17 00:00:00 2001 From: susanodd Date: Mon, 10 Feb 2025 17:12:29 +0100 Subject: [PATCH 1/3] #1477: Template command to cleanup revision history --- signbank/dictionary/gloss_revision.py | 59 +++++++++++++++++++ .../dictionary/gloss_revision_history.html | 36 ++++++++++- signbank/dictionary/urls.py | 5 ++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 signbank/dictionary/gloss_revision.py diff --git a/signbank/dictionary/gloss_revision.py b/signbank/dictionary/gloss_revision.py new file mode 100644 index 000000000..4f464840b --- /dev/null +++ b/signbank/dictionary/gloss_revision.py @@ -0,0 +1,59 @@ + +from signbank.dictionary.models import Gloss, GlossRevision +import datetime as DT +from django.shortcuts import render, get_object_or_404, redirect +from django.contrib.auth.decorators import permission_required +from django.http import HttpResponseRedirect +from signbank.dictionary.update import okay_to_update_gloss +from django.urls import reverse +from django.conf import settings + + +@permission_required('dictionary.change_gloss') +def cleanup(request, glossid): + + gloss = Gloss.objects.filter(id=glossid, archived=False).first() + + if not okay_to_update_gloss(request, gloss): + return HttpResponseRedirect(request.META.get('HTTP_REFERER')) + + success_url = '/dictionary/gloss/' + glossid + '/history' + + revisions = GlossRevision.objects.filter(gloss=gloss).order_by('time') + + empty_revisions = [] + duplicate_revisions = dict() + tuple_updates = [] + tuple_update_already_seen = [] + for revision in revisions: + if revision.old_value in ['', '-'] and revision.new_value in ['', '-']: + empty_revisions.append(revision) + elif revision.old_value == revision.new_value: + if revision.field_name not in duplicate_revisions.keys(): + # this is the first time the duplicate occurs + duplicate_revisions[revision.field_name] = [] + else: + # we have already seen this duplicate, schedule to delete + duplicate_revisions[revision.field_name].append(revision) + else: + if not tuple_updates: + tuple_updates.append((revision.field_name, revision.old_value, revision.new_value)) + else: + (last_field, last_old_value, last_new_value) = tuple_updates[-1] + if last_field == revision.field_name and last_old_value == revision.old_value and last_new_value == revision.new_value: + tuple_update_already_seen.append(revision) + else: + tuple_updates.append((revision.field_name, revision.old_value, revision.new_value)) + for empty_revision in empty_revisions: + empty_revision.delete() + + for field_name, duplicates in duplicate_revisions.items(): + for duplicate in duplicates: + duplicate.delete() + + for already_seen in tuple_update_already_seen: + already_seen.delete() + + # because this method updates the database, return such that the template will be redrawn + return HttpResponseRedirect(settings.PREFIX_URL + success_url) + diff --git a/signbank/dictionary/templates/dictionary/gloss_revision_history.html b/signbank/dictionary/templates/dictionary/gloss_revision_history.html index 7077da200..891e3df46 100644 --- a/signbank/dictionary/templates/dictionary/gloss_revision_history.html +++ b/signbank/dictionary/templates/dictionary/gloss_revision_history.html @@ -12,16 +12,40 @@ {% block extrajs %} {% endblock %} {% block content %} +{% get_obj_perms request.user for gloss.lemma.dataset as "dataset_perms" %}

+{% if "change_dataset" in dataset_perms %} +
+ + +
+{% endif %} +
diff --git a/signbank/dictionary/urls.py b/signbank/dictionary/urls.py index 015bfc8c8..f89fe1b09 100755 --- a/signbank/dictionary/urls.py +++ b/signbank/dictionary/urls.py @@ -26,6 +26,7 @@ import signbank.gloss_morphology_update import signbank.frequency import signbank.dataset_operations +import signbank.dictionary.gloss_revision app_name = 'dictionary' urlpatterns = [ @@ -145,6 +146,10 @@ signbank.dictionary.update.toggle_language_fields, name='toggle_language_fields'), + re_path(r'^gloss_revision/cleanup/(?P\d+)$', + signbank.dictionary.gloss_revision.cleanup, + name='gloss_revision_cleanup'), + # The next one does not have a permission check because it should be accessible from a cronjob re_path(r'^update_ecv/', GlossListView.as_view(only_export_ecv=True)), re_path(r'^update/variants_of_gloss/$', signbank.dictionary.update.variants_of_gloss, name='variants_of_gloss'), From 28f68aeb4fa6e33174d78ae6fc83110a68d619fc Mon Sep 17 00:00:00 2001 From: susanodd Date: Tue, 11 Feb 2025 16:27:49 +0100 Subject: [PATCH 2/3] #1477, #1499: Multilingual display of revision history; template cleanup command to remove duplicates and identity empty operations revised code by putting functions in new file. --- signbank/dictionary/gloss_revision.py | 190 +++++++++++++++++- .../dictionary/gloss_revision_history.html | 11 +- signbank/dictionary/views.py | 119 +---------- 3 files changed, 191 insertions(+), 129 deletions(-) diff --git a/signbank/dictionary/gloss_revision.py b/signbank/dictionary/gloss_revision.py index 4f464840b..f7a455f0b 100644 --- a/signbank/dictionary/gloss_revision.py +++ b/signbank/dictionary/gloss_revision.py @@ -1,12 +1,190 @@ -from signbank.dictionary.models import Gloss, GlossRevision +from signbank.dictionary.models import Gloss, GlossRevision, Language, FieldChoice, Handshape import datetime as DT from django.shortcuts import render, get_object_or_404, redirect from django.contrib.auth.decorators import permission_required -from django.http import HttpResponseRedirect +from django.http import HttpResponseRedirect, JsonResponse from signbank.dictionary.update import okay_to_update_gloss from django.urls import reverse from django.conf import settings +from signbank.dictionary.translate_choice_list import check_value_to_translated_human_value +from django.utils.translation import gettext_lazy as _, activate, gettext +from signbank.dictionary.context_data import get_selected_datasets +from django.core.exceptions import PermissionDenied, ObjectDoesNotExist, MultipleObjectsReturned +from signbank.csv_interface import normalize_field_choice +from django.db.models import Q, Count, CharField, TextField, Value as V + + +def get_field_choice_from_name(fieldname, value, language_codes): + # try to get a matching field choice in one of the languages + if fieldname not in Gloss.get_field_names(): + return value + field = Gloss.get_field(fieldname) + if not hasattr(field, 'field_choice_category'): + return value + field_choice_category = field.field_choice_category + for language_name_field in language_codes: + # next_language_name = 'name_'+language_code + fieldchoice = FieldChoice.objects.filter(Q(**{'field': field_choice_category, + language_name_field: value})).first() + if not fieldchoice: + normalised_choice = normalize_field_choice(value) + fieldchoice = FieldChoice.objects.filter(Q(**{'field': field_choice_category, + language_name_field: normalised_choice})).first() + if fieldchoice: + break + return fieldchoice.name if fieldchoice else '-' + + +def get_handshape_from_name(fieldname, value, language_codes): + # try to get a matching handshape name in one of the languages + if fieldname not in ['domhndsh', 'subhndsh', 'final_domhndsh', 'final_subhndsh']: + return value + for language_name_field in language_codes: + # next_language_name = 'name_'+language_code + handshape = Handshape.objects.filter(Q(**{language_name_field: value})).first() + if not handshape: + normalised_choice = normalize_field_choice(value) + handshape = Handshape.objects.filter(Q(**{language_name_field: normalised_choice})).first() + if handshape: + break + return handshape.name if handshape else '-' + + +def pretty_print_revisions(gloss): + # set up all the various translation fields for the interface languages + language_codes = ['name_en'] + for interface_language_code in settings.MODELTRANSLATION_LANGUAGES: + name_languagecode = 'name_' + interface_language_code.replace('-', '_') + if name_languagecode not in language_codes: + language_codes.append(name_languagecode) + revisions = [] + for revision in GlossRevision.objects.filter(gloss=gloss): + if revision.field_name.startswith('sense_'): + prefix, order, language_2char = revision.field_name.split('_') + language = Language.objects.get(language_code_2char=language_2char) + revision_verbose_fieldname = gettext('Sense') + ' ' + order + " (%s)" % language.name + elif revision.field_name.startswith('description_'): + prefix, language_2char = revision.field_name.split('_') + language = Language.objects.get(language_code_2char=language_2char) + revision_verbose_fieldname = gettext('NME Video Description') + " (%s)" % language.name + elif revision.field_name.startswith('nmevideo_'): + prefix, operation = revision.field_name.split('_') + if operation == 'create': + revision_verbose_fieldname = gettext("NME Video") + ' ' + gettext("Create") + elif operation == 'delete': + revision_verbose_fieldname = gettext("NME Video") + ' ' + gettext("Delete") + else: + revision_verbose_fieldname = gettext("NME Video") + ' ' + gettext("Update") + elif revision.field_name in Gloss.get_field_names(): + revision_verbose_fieldname = gettext(Gloss.get_field(revision.field_name).verbose_name) + elif revision.field_name == 'sequential_morphology': + revision_verbose_fieldname = gettext("Sequential Morphology") + elif revision.field_name == 'simultaneous_morphology': + revision_verbose_fieldname = gettext("Simultaneous Morphology") + elif revision.field_name == 'blend_morphology': + revision_verbose_fieldname = gettext("Blend Morphology") + elif revision.field_name.startswith('lemma'): + language_2char = revision.field_name[-2:] + language = Language.objects.get(language_code_2char=language_2char) + revision_verbose_fieldname = gettext('Lemma ID Gloss') + " (%s)" % language.name + elif revision.field_name.startswith('annotation'): + language_2char = revision.field_name[-2:] + language = Language.objects.get(language_code_2char=language_2char) + revision_verbose_fieldname = gettext('Annotation ID Gloss') + " (%s)" % language.name + elif revision.field_name == 'archived': + revision_verbose_fieldname = gettext("Deleted") + elif revision.field_name == 'restored': + revision_verbose_fieldname = gettext("Restored") + else: + revision_verbose_fieldname = gettext(revision.field_name) + # print('fall through: ', revision.field_name, revision_verbose_fieldname) + + # field name qualification is stored separately here + # Django was having a bit of trouble translating it when embedded in the field_name string below + if revision.field_name == 'Tags': + if revision.old_value: + # this translation exists in the interface of Gloss Edit View + delete_command = gettext('delete this tag') + field_name_qualification = ' (' + delete_command + ')' + elif revision.new_value: + # this translation exists in the interface of Gloss Edit View + add_command = gettext('Add Tag') + field_name_qualification = ' (' + add_command + ')' + else: + # this shouldn't happen + field_name_qualification = '' + elif revision.field_name in ['Sense', 'Senses', 'senses']: + if revision.old_value and not revision.new_value: + # this translation exists in the interface of Gloss Edit View + delete_command = gettext('Delete') + field_name_qualification = ' (' + delete_command + ')' + elif revision.new_value and not revision.old_value: + # this translation exists in the interface of Gloss Edit View + add_command = gettext('Create') + field_name_qualification = ' (' + add_command + ')' + else: + # this translation exists in the interface of Gloss Edit View + add_command = gettext('Update') + field_name_qualification = ' (' + add_command + ')' + elif revision.field_name == 'Sentence': + if revision.old_value and not revision.new_value: + # this translation exists in the interface of Gloss Edit View + delete_command = gettext('Delete') + field_name_qualification = ' (' + delete_command + ')' + elif revision.new_value and not revision.old_value: + # this translation exists in the interface of Gloss Edit View + add_command = gettext('Create') + field_name_qualification = ' (' + add_command + ')' + else: + # this translation exists in the interface of Gloss Edit View + add_command = gettext('Update') + field_name_qualification = ' (' + add_command + ')' + elif revision.field_name in ['sequential_morphology', 'simultaneous_morphology', 'blend_morphology']: + if revision.old_value and not revision.new_value: + # this translation exists in the interface of Gloss Edit View + delete_command = gettext('Delete') + field_name_qualification = ' (' + delete_command + ')' + elif revision.new_value and not revision.old_value: + # this translation exists in the interface of Gloss Edit View + add_command = gettext('Create') + field_name_qualification = ' (' + add_command + ')' + else: + # this translation exists in the interface of Gloss Edit View + add_command = gettext('Update') + field_name_qualification = ' (' + add_command + ')' + elif revision.field_name.startswith('lemma') or revision.field_name.startswith('annotation'): + field_name_qualification = '' + elif revision.field_name.startswith('sense'): + field_name_qualification = '' + else: + field_name_qualification = '' + if revision.field_name in Gloss.get_field_names(): + field = Gloss.get_field(revision.field_name) + if hasattr(field, 'field_choice_category'): + display_old_value = get_field_choice_from_name(revision.field_name, revision.old_value, language_codes) + display_new_value = get_field_choice_from_name(revision.field_name, revision.new_value, language_codes) + elif field.name in ['domhndsh', 'subhndsh', 'final_domhndsh', 'final_subhndsh']: + display_old_value = get_handshape_from_name(revision.field_name, revision.old_value, language_codes) + display_new_value = get_handshape_from_name(revision.field_name, revision.new_value, language_codes) + else: + display_old_value = check_value_to_translated_human_value(revision.field_name, revision.old_value) + display_new_value = check_value_to_translated_human_value(revision.field_name, revision.new_value) + else: + display_old_value = revision.old_value + display_new_value = revision.new_value + revision_dict = { + 'is_tag': revision.field_name == 'Tags', + 'gloss': revision.gloss, + 'user': revision.user, + 'time': revision.time, + 'field_name': revision_verbose_fieldname, + 'field_name_qualification': field_name_qualification, + 'old_value': display_old_value, + 'new_value': display_new_value } + revisions.append(revision_dict) + + return revisions @permission_required('dictionary.change_gloss') @@ -15,9 +193,7 @@ def cleanup(request, glossid): gloss = Gloss.objects.filter(id=glossid, archived=False).first() if not okay_to_update_gloss(request, gloss): - return HttpResponseRedirect(request.META.get('HTTP_REFERER')) - - success_url = '/dictionary/gloss/' + glossid + '/history' + return JsonResponse({}) revisions = GlossRevision.objects.filter(gloss=gloss).order_by('time') @@ -54,6 +230,4 @@ def cleanup(request, glossid): for already_seen in tuple_update_already_seen: already_seen.delete() - # because this method updates the database, return such that the template will be redrawn - return HttpResponseRedirect(settings.PREFIX_URL + success_url) - + return JsonResponse({}) diff --git a/signbank/dictionary/templates/dictionary/gloss_revision_history.html b/signbank/dictionary/templates/dictionary/gloss_revision_history.html index 891e3df46..bb10da544 100644 --- a/signbank/dictionary/templates/dictionary/gloss_revision_history.html +++ b/signbank/dictionary/templates/dictionary/gloss_revision_history.html @@ -22,13 +22,6 @@ {% include "dictionary/search_result_bar.html" %} - function do_nothing(data) { - if ($.isEmptyObject(data)) { - return; - }; - return; - }; - $('.quick_revision').click(function(e) { e.preventDefault(); @@ -38,7 +31,9 @@ type: 'POST', data: { 'csrfmiddlewaretoken': csrf_token }, datatype: "json", - success : do_nothing + success : function(data) { + window.location.href = url + '/dictionary/gloss/'+glossid+'/history';; + } }); }); diff --git a/signbank/dictionary/views.py b/signbank/dictionary/views.py index ac4f5f63c..acd920107 100644 --- a/signbank/dictionary/views.py +++ b/signbank/dictionary/views.py @@ -41,7 +41,6 @@ import signbank.settings.server_specific from signbank.settings.base import * -from django.utils.translation import override, gettext_lazy as _ from urllib.parse import urlencode, urlparse from wsgiref.util import FileWrapper, request_uri @@ -51,10 +50,11 @@ from django.core.exceptions import PermissionDenied, ObjectDoesNotExist, BadRequest from signbank.gloss_update import api_update_gloss_fields -from django.utils.translation import gettext_lazy as _, activate +from django.utils.translation import gettext_lazy as _, activate, override from signbank.abstract_machine import get_interface_language_api from signbank.api_token import put_api_user_in_request +from signbank.dictionary.gloss_revision import pretty_print_revisions def login_required_config(f): @@ -2530,116 +2530,9 @@ def gloss_revision_history(request,gloss_pk): show_query_parameters_as_button = getattr(settings, 'SHOW_QUERY_PARAMETERS_AS_BUTTON', False) use_regular_expressions = getattr(settings, 'USE_REGULAR_EXPRESSIONS', False) - revisions = [] - for revision in GlossRevision.objects.filter(gloss=gloss): - if revision.field_name.startswith('sense_'): - prefix, order, language_2char = revision.field_name.split('_') - language = Language.objects.get(language_code_2char=language_2char) - revision_verbose_fieldname = gettext('Sense') + ' ' + order + " (%s)" % language.name - elif revision.field_name.startswith('description_'): - prefix, language_2char = revision.field_name.split('_') - language = Language.objects.get(language_code_2char=language_2char) - revision_verbose_fieldname = gettext('NME Video Description') + " (%s)" % language.name - elif revision.field_name.startswith('nmevideo_'): - prefix, operation = revision.field_name.split('_') - if operation == 'create': - revision_verbose_fieldname = gettext("NME Video") + ' ' + gettext("Create") - elif operation == 'delete': - revision_verbose_fieldname = gettext("NME Video") + ' ' + gettext("Delete") - else: - revision_verbose_fieldname = gettext("NME Video") + ' ' + gettext("Update") - elif revision.field_name in Gloss.get_field_names(): - revision_verbose_fieldname = gettext(Gloss.get_field(revision.field_name).verbose_name) - elif revision.field_name == 'sequential_morphology': - revision_verbose_fieldname = gettext("Sequential Morphology") - elif revision.field_name == 'simultaneous_morphology': - revision_verbose_fieldname = gettext("Simultaneous Morphology") - elif revision.field_name == 'blend_morphology': - revision_verbose_fieldname = gettext("Blend Morphology") - elif revision.field_name.startswith('lemma'): - language_2char = revision.field_name[-2:] - language = Language.objects.get(language_code_2char=language_2char) - revision_verbose_fieldname = gettext('Lemma ID Gloss') + " (%s)" % language.name - elif revision.field_name.startswith('annotation'): - language_2char = revision.field_name[-2:] - language = Language.objects.get(language_code_2char=language_2char) - revision_verbose_fieldname = gettext('Annotation ID Gloss') + " (%s)" % language.name - elif revision.field_name == 'archived': - revision_verbose_fieldname = gettext("Deleted") - elif revision.field_name == 'restored': - revision_verbose_fieldname = gettext("Restored") - else: - revision_verbose_fieldname = gettext(revision.field_name) - - # field name qualification is stored separately here - # Django was having a bit of trouble translating it when embeded in the field_name string below - if revision.field_name == 'Tags': - if revision.old_value: - # this translation exists in the interface of Gloss Edit View - delete_command = gettext('delete this tag') - field_name_qualification = ' (' + delete_command + ')' - elif revision.new_value: - # this translation exists in the interface of Gloss Edit View - add_command = gettext('Add Tag') - field_name_qualification = ' (' + add_command + ')' - else: - # this shouldn't happen - field_name_qualification = '' - elif revision.field_name in ['Sense', 'Senses', 'senses']: - if revision.old_value and not revision.new_value: - # this translation exists in the interface of Gloss Edit View - delete_command = gettext('Delete') - field_name_qualification = ' (' + delete_command + ')' - elif revision.new_value and not revision.old_value: - # this translation exists in the interface of Gloss Edit View - add_command = gettext('Create') - field_name_qualification = ' (' + add_command + ')' - else: - # this translation exists in the interface of Gloss Edit View - add_command = gettext('Update') - field_name_qualification = ' (' + add_command + ')' - elif revision.field_name == 'Sentence': - if revision.old_value and not revision.new_value: - # this translation exists in the interface of Gloss Edit View - delete_command = gettext('Delete') - field_name_qualification = ' (' + delete_command + ')' - elif revision.new_value and not revision.old_value: - # this translation exists in the interface of Gloss Edit View - add_command = gettext('Create') - field_name_qualification = ' (' + add_command + ')' - else: - # this translation exists in the interface of Gloss Edit View - add_command = gettext('Update') - field_name_qualification = ' (' + add_command + ')' - elif revision.field_name in ['sequential_morphology', 'simultaneous_morphology', 'blend_morphology']: - if revision.old_value and not revision.new_value: - # this translation exists in the interface of Gloss Edit View - delete_command = gettext('Delete') - field_name_qualification = ' (' + delete_command + ')' - elif revision.new_value and not revision.old_value: - # this translation exists in the interface of Gloss Edit View - add_command = gettext('Create') - field_name_qualification = ' (' + add_command + ')' - else: - # this translation exists in the interface of Gloss Edit View - add_command = gettext('Update') - field_name_qualification = ' (' + add_command + ')' - elif revision.field_name.startswith('lemma') or revision.field_name.startswith('annotation'): - field_name_qualification = '' - elif revision.field_name.startswith('sense'): - field_name_qualification = '' - else: - field_name_qualification = '' - revision_dict = { - 'is_tag': revision.field_name == 'Tags', - 'gloss': revision.gloss, - 'user': revision.user, - 'time': revision.time, - 'field_name': revision_verbose_fieldname, - 'field_name_qualification': field_name_qualification, - 'old_value': check_value_to_translated_human_value(revision.field_name, revision.old_value), - 'new_value': check_value_to_translated_human_value(revision.field_name, revision.new_value) } - revisions.append(revision_dict) + interface_language_code = get_interface_language_api(request, request.user) + + revisions = pretty_print_revisions(gloss) if 'search_type' in request.session.keys(): if request.session['search_type'] not in ['sign', 'morpheme', 'annotatedsentence', @@ -2650,7 +2543,7 @@ def gloss_revision_history(request,gloss_pk): request.session['search_type'] = 'sign' return render(request, 'dictionary/gloss_revision_history.html', - {'gloss': gloss, 'revisions':revisions, + {'gloss': gloss, 'revisions': revisions, 'dataset_languages': dataset_languages, 'selected_datasets': selected_datasets, 'active_id': gloss_pk, From 319190882e7537d3429f627d268ef269b7f83013 Mon Sep 17 00:00:00 2001 From: susanodd Date: Wed, 12 Feb 2025 13:29:05 +0100 Subject: [PATCH 3/3] #1501: Added admin search revision history field name, old, new values --- signbank/dictionary/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/signbank/dictionary/admin.py b/signbank/dictionary/admin.py index a012cc24e..d3df60257 100755 --- a/signbank/dictionary/admin.py +++ b/signbank/dictionary/admin.py @@ -697,7 +697,7 @@ class GlossRevisionAdmin(VersionAdmin): list_display = ['time', 'user', 'dataset', 'gloss', 'field_name', 'old_value', 'new_value'] readonly_fields = ['user', 'gloss', 'field_name', 'old_value', 'new_value', 'time', 'old_value'] list_filter = (GlossRevisionDatasetFilter, GlossRevisionUserFilter,) - search_fields = ['gloss__lemma__lemmaidglosstranslation__text'] + search_fields = ['gloss__lemma__lemmaidglosstranslation__text', 'field_name', 'old_value', 'new_value'] def has_add_permission(self, request): return False