+
-
+
From 695153bf0bf32eb51cea1a853e97e089a8c12e58 Mon Sep 17 00:00:00 2001 From: Jennifer Kuenning <72825410+jkueloc@users.noreply.github.com> Date: Mon, 12 Aug 2024 07:19:22 -0400 Subject: [PATCH 01/28] revert bad merge to release (#2484) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Install non-English languages with tesseract (#2421) * Removed inadvertent addition of review_count to user admin[B (#2474) * Fixed misspelling (#2476) * Revert "Merge branch 'main' into release" This reverts commit 0d1b6f047bfa539f9c174a7c61d8527ca7df1910, reversing changes made to 2ad61b57b61a0c3c552e62d5758c25f76531cf07. This merge to release was a mistake and needs to be reverted. --------- Co-authored-by: Josh Stegmaier <104993387+joshuastegmaier@users.noreply.github.com> --- .eslintrc.yaml | 1 - concordia/admin/__init__.py | 44 +- concordia/admin/filters.py | 21 +- concordia/admin/forms.py | 18 +- concordia/documents.py | 3 - ...tions_userprofile_review_count_and_more.py | 43 - .../0098_userprofile_create_and_population.py | 40 - concordia/models.py | 154 +- concordia/signals/handlers.py | 8 +- concordia/static/js/src/asset-reservation.js | 16 +- concordia/static/js/src/banner.js | 18 - concordia/static/js/src/contribute.js | 23 - concordia/static/js/src/guide.js | 86 -- concordia/static/js/src/ocr.js | 7 - concordia/static/js/src/quick-tips.js | 38 - concordia/static/js/src/viewer-split.js | 146 -- concordia/static/js/src/viewer.js | 200 --- concordia/tasks.py | 12 +- .../admin/concordia/item/change_form.html | 15 + .../admin/concordia/topic/change_form.html | 19 + .../templates/emails/unusual_activity.txt | 2 +- .../transcriptions/asset_detail.html | 1239 ++++++++++++++++- .../asset_reservation_failure_modal.html | 19 - .../asset_detail/captcha_modal.html | 38 - .../transcriptions/asset_detail/editor.html | 118 -- .../language_selection_modal.html | 33 - .../asset_detail/navigation.html | 49 - .../asset_detail/ocr_help_modal.html | 27 - .../asset_detail/ocr_transcription_modal.html | 24 - .../asset_detail/quick_tips_modal.html | 42 - .../asset_detail/review_accepted_modal.html | 35 - .../successful_submission_modal.html | 35 - .../transcriptions/asset_detail/tags.html | 47 - .../transcriptions/asset_detail/viewer.html | 121 -- .../asset_detail/viewer_filters.html | 125 -- .../transcriptions/topic_detail.html | 4 +- concordia/tests/test_admin.py | 6 +- concordia/tests/test_models.py | 142 +- concordia/tests/test_views.py | 9 +- exporter/tabular_export/admin.py | 22 +- 40 files changed, 1377 insertions(+), 1672 deletions(-) delete mode 100644 concordia/migrations/0097_alter_sitereport_options_userprofile_review_count_and_more.py delete mode 100644 concordia/migrations/0098_userprofile_create_and_population.py delete mode 100644 concordia/static/js/src/banner.js delete mode 100644 concordia/static/js/src/guide.js delete mode 100644 concordia/static/js/src/ocr.js delete mode 100644 concordia/static/js/src/quick-tips.js delete mode 100644 concordia/static/js/src/viewer-split.js delete mode 100644 concordia/static/js/src/viewer.js create mode 100644 concordia/templates/admin/concordia/topic/change_form.html delete mode 100644 concordia/templates/transcriptions/asset_detail/asset_reservation_failure_modal.html delete mode 100644 concordia/templates/transcriptions/asset_detail/captcha_modal.html delete mode 100644 concordia/templates/transcriptions/asset_detail/editor.html delete mode 100644 concordia/templates/transcriptions/asset_detail/language_selection_modal.html delete mode 100644 concordia/templates/transcriptions/asset_detail/navigation.html delete mode 100644 concordia/templates/transcriptions/asset_detail/ocr_help_modal.html delete mode 100644 concordia/templates/transcriptions/asset_detail/ocr_transcription_modal.html delete mode 100644 concordia/templates/transcriptions/asset_detail/quick_tips_modal.html delete mode 100644 concordia/templates/transcriptions/asset_detail/review_accepted_modal.html delete mode 100644 concordia/templates/transcriptions/asset_detail/successful_submission_modal.html delete mode 100644 concordia/templates/transcriptions/asset_detail/tags.html delete mode 100644 concordia/templates/transcriptions/asset_detail/viewer.html delete mode 100644 concordia/templates/transcriptions/asset_detail/viewer_filters.html diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 56959c746..989beb8c0 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -15,7 +15,6 @@ rules: 'unicorn/prefer-query-selector': off # See https://github.com/sindresorhus/eslint-plugin-unicorn/issues/276 'unicorn/prefer-node-append': off 'unicorn/prefer-ternary': off - 'unicorn/no-lonely-if': off env: browser: true es2024: true diff --git a/concordia/admin/__init__.py b/concordia/admin/__init__.py index 46a8081e9..0462e8891 100644 --- a/concordia/admin/__init__.py +++ b/concordia/admin/__init__.py @@ -9,6 +9,7 @@ from django.contrib.auth.admin import UserAdmin from django.contrib.auth.decorators import permission_required from django.contrib.auth.models import User +from django.db.models import Count from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.template.defaultfilters import truncatechars @@ -79,7 +80,6 @@ SubmittedFilter, TagCampaignListFilter, TagCampaignStatusListFilter, - TopicListFilter, TranscriptionCampaignListFilter, TranscriptionCampaignStatusListFilter, TranscriptionProjectListFilter, @@ -93,9 +93,8 @@ CampaignAdminForm, CardAdminForm, GuideAdminForm, - ItemAdminForm, ProjectAdminForm, - TopicAdminForm, + SanitizedDescriptionAdminForm, ) logger = logging.getLogger(__name__) @@ -108,22 +107,16 @@ class ConcordiaUserAdmin(UserAdmin): "is_staff", "date_joined", "transcription_count", - "review_count", ) def get_queryset(self, request): - qs = super().get_queryset(request).select_related("profile") + qs = super().get_queryset(request) + qs = qs.annotate(Count("transcription")) return qs - @admin.display( - description="Transcription Count", ordering="profile__transcribe_count" - ) + @admin.display(ordering="transcription__count") def transcription_count(self, obj): - return obj.profile.transcribe_count - - @admin.display(description="Review Count", ordering="profile__review_count") - def review_count(self, obj): - return obj.profile.review_count + return obj.transcription__count EXPORT_FIELDS = ( "username", @@ -135,31 +128,17 @@ def review_count(self, obj): "is_superuser", "date_joined", "last_login", - "profile__transcribe_count", - "profile__review_count", + "transcription__count", ) - EXTRA_VERBOSE_NAMES = { - "profile__transcribe_count": "transcription count", - "profile__review_count": "review count", - } - def export_users_as_csv(self, request, queryset): return export_to_csv_action( - self, - request, - queryset, - field_names=self.EXPORT_FIELDS, - extra_verbose_names=self.EXTRA_VERBOSE_NAMES, + self, request, queryset, field_names=self.EXPORT_FIELDS ) def export_users_as_excel(self, request, queryset): return export_to_excel_action( - self, - request, - queryset, - field_names=self.EXPORT_FIELDS, - extra_verbose_names=self.EXTRA_VERBOSE_NAMES, + self, request, queryset, field_names=self.EXPORT_FIELDS ) actions = (anonymize_action, export_users_as_csv, export_users_as_excel) @@ -379,8 +358,8 @@ class ResourceAdmin(admin.ModelAdmin, CustomListDisplayFieldsMixin): list_filter = ( "resource_type", ResourceCampaignStatusListFilter, - TopicListFilter, ResourceCampaignListFilter, + "title", ) def formfield_for_foreignkey(self, db_field, request, **kwargs): @@ -417,7 +396,7 @@ def get_fields(self, request, obj=None): @admin.register(Topic) class TopicAdmin(admin.ModelAdmin): - form = TopicAdminForm + form = SanitizedDescriptionAdminForm list_display = ( "id", @@ -544,7 +523,6 @@ def item_import_view(self, request, object_id): @admin.register(Item) class ItemAdmin(admin.ModelAdmin): - form = ItemAdminForm list_display = ("title", "item_id", "campaign_title", "project", "published") list_display_links = ("title", "item_id") search_fields = [ diff --git a/concordia/admin/filters.py b/concordia/admin/filters.py index 4148a0d5b..236c31337 100644 --- a/concordia/admin/filters.py +++ b/concordia/admin/filters.py @@ -1,7 +1,7 @@ from django.contrib import admin from django.utils.translation import gettext_lazy as _ -from ..models import Campaign, Project, Topic +from ..models import Campaign, Project class NullableTimestampFilter(admin.SimpleListFilter): @@ -92,25 +92,6 @@ def queryset(self, request, queryset): return queryset -class TopicListFilter(admin.SimpleListFilter): - """ - Base class for admin topic filters - """ - - title = "Topic" - template = "admin/long_name_filter.html" - parameter_name = "topic__id__exact" - - def lookups(self, request, model_admin): - queryset = Topic.objects.all() - return queryset.values_list("id", "title").order_by("title") - - def queryset(self, request, queryset): - if self.value(): - return queryset.filter(**{self.parameter_name: self.value()}) - return queryset - - class ProjectCampaignListFilter(CampaignListFilter): parameter_name = "campaign__id__exact" status_filter_parameter = "campaign__status" diff --git a/concordia/admin/forms.py b/concordia/admin/forms.py index 49df703b5..ff3011a36 100644 --- a/concordia/admin/forms.py +++ b/concordia/admin/forms.py @@ -3,7 +3,7 @@ from django.core.cache import caches from tinymce.widgets import TinyMCE -from ..models import Campaign, Card, Guide, Item, Project, Topic +from ..models import Campaign, Card, Guide, Project FRAGMENT_ALLOWED_TAGS = { "a", @@ -90,15 +90,6 @@ def clean_short_description(self): ) -class TopicAdminForm(SanitizedDescriptionAdminForm): - class Meta(SanitizedDescriptionAdminForm.Meta): - model = Topic - widgets = { - "description": TinyMCE(), - "short_description": TinyMCE(), - } - - class CampaignAdminForm(SanitizedDescriptionAdminForm): class Meta(SanitizedDescriptionAdminForm.Meta): model = Campaign @@ -117,13 +108,6 @@ class Meta(SanitizedDescriptionAdminForm.Meta): } -class ItemAdminForm(forms.ModelForm): - class Meta: - model = Item - widgets = {"description": TinyMCE()} - fields = "__all__" - - class CardAdminForm(forms.ModelForm): class Meta: model = Card diff --git a/concordia/documents.py b/concordia/documents.py index 1060e3a6e..1a0620914 100644 --- a/concordia/documents.py +++ b/concordia/documents.py @@ -41,7 +41,6 @@ class Django: fields = [ "created_on", - "report_name", "assets_total", "assets_published", "assets_not_started", @@ -55,14 +54,12 @@ class Django: "projects_unpublished", "anonymous_transcriptions", "transcriptions_saved", - "daily_review_actions", "distinct_tags", "tag_uses", "campaigns_published", "campaigns_unpublished", "users_registered", "users_activated", - "registered_contributors", "daily_active_users", ] diff --git a/concordia/migrations/0097_alter_sitereport_options_userprofile_review_count_and_more.py b/concordia/migrations/0097_alter_sitereport_options_userprofile_review_count_and_more.py deleted file mode 100644 index 73264a261..000000000 --- a/concordia/migrations/0097_alter_sitereport_options_userprofile_review_count_and_more.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 4.2.13 on 2024-07-29 17:30 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("concordia", "0096_transcription_source"), - ] - - operations = [ - migrations.AlterModelOptions( - name="sitereport", - options={"get_latest_by": "created_on", "ordering": ("-created_on",)}, - ), - migrations.AddField( - model_name="userprofile", - name="review_count", - field=models.IntegerField( - default=0, verbose_name="transcription review count" - ), - ), - migrations.AddField( - model_name="userprofile", - name="transcribe_count", - field=models.IntegerField( - default=0, verbose_name="transcription save/submit count" - ), - ), - migrations.AlterField( - model_name="userprofile", - name="user", - field=models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="profile", - to=settings.AUTH_USER_MODEL, - ), - ), - ] diff --git a/concordia/migrations/0098_userprofile_create_and_population.py b/concordia/migrations/0098_userprofile_create_and_population.py deleted file mode 100644 index 36efd5bc7..000000000 --- a/concordia/migrations/0098_userprofile_create_and_population.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 4.2.13 on 2024-07-29 17:40 - -from django.conf import settings -from django.db import migrations - - -def create_and_populate_profiles(apps, schema_editor): - User = apps.get_model("auth", "User") - UserProfile = apps.get_model("concordia", "UserProfile") - db_alias = schema_editor.connection.alias - for user in User.objects.using(db_alias).all().iterator(chunk_size=10000): - profile, created = UserProfile.objects.using(db_alias).get_or_create(user=user) - for activity in user.userprofileactivity_set.all(): - profile.transcribe_count += activity.transcribe_count - profile.review_count += activity.review_count - profile.save() - - -def revert_create_and_populate_profiles(apps, schema_editor): - # We can't actually revert the data to the state it was before, - # and there's no actual need to, but we need this function to be - # able to reverse this migration - pass - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ( - "concordia", - "0097_alter_sitereport_options_userprofile_review_count_and_more", - ), - ] - - operations = [ - migrations.RunPython( - create_and_populate_profiles, revert_create_and_populate_profiles - ), - ] diff --git a/concordia/models.py b/concordia/models.py index e9c8b7939..495082209 100644 --- a/concordia/models.py +++ b/concordia/models.py @@ -11,7 +11,7 @@ from django.core.cache import cache from django.core.exceptions import ValidationError from django.core.validators import RegexValidator -from django.db import models +from django.db import connection, models from django.db.models import Count, ExpressionWrapper, F, JSONField, Q from django.db.models.functions import Round from django.db.models.signals import post_save @@ -27,10 +27,8 @@ User._meta.get_field("email").__dict__["_unique"] = True -ONE_MINUTE = datetime.timedelta(minutes=1) ONE_DAY = datetime.timedelta(days=1) ONE_DAY_AGO = timezone.now() - ONE_DAY -THRESHOLD = 3 def resource_file_upload_path(instance, filename): @@ -74,53 +72,9 @@ def get_email_reconfirmation_key(self): def validate_reconfirmation_email(self, email): return email == self.get_email_for_reconfirmation() - def review_incidents(self, recent_accepts, recent_rejects, threshold=THRESHOLD): - accepts = recent_accepts.filter(reviewed_by=self).values_list( - "accepted", flat=True - ) - rejects = recent_rejects.filter(reviewed_by=self).values_list( - "rejected", flat=True - ) - timestamps = list(accepts) + list(rejects) - timestamps.sort() - incidents = 0 - for i in range(len(timestamps)): - count = 1 - for j in range(i + 1, len(timestamps)): - if (timestamps[j] - timestamps[i]).seconds <= 60: - count += 1 - if count == threshold: - incidents += 1 - break - else: - break - return incidents - - def transcribe_incidents(self, transcriptions, threshold=THRESHOLD): - recent_transcriptions = transcriptions.filter(user=self).order_by("submitted") - timestamps = recent_transcriptions.values_list("submitted", flat=True) - incidents = 0 - for i in range(len(timestamps)): - count = 1 - for j in range(i + 1, len(timestamps)): - if (timestamps[j] - timestamps[i]).seconds <= 60: - count += 1 - if count == threshold: - incidents += 1 - break - else: - break - return incidents - class UserProfile(MetricsModelMixin("userprofile"), models.Model): - user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") - transcribe_count = models.IntegerField( - default=0, verbose_name="transcription save/submit count" - ) - review_count = models.IntegerField( - default=0, verbose_name="transcription review count" - ) + user = models.ForeignKey(User, on_delete=models.CASCADE) class OverlayPosition(object): @@ -838,49 +792,63 @@ def recent_review_actions(self, days=1): START = timezone.now() - datetime.timedelta(days=days) return self.review_actions(START) - def review_incidents(self): - user_incident_count = [] - recent_accepts = self.filter( - accepted__gte=ONE_DAY_AGO, - reviewed_by__is_superuser=False, - reviewed_by__is_staff=False, - ) - recent_rejects = self.filter( - rejected__gte=ONE_DAY_AGO, - reviewed_by__is_superuser=False, - reviewed_by__is_staff=False, - ) - recent_actions = recent_accepts.union(recent_rejects) - user_ids = set( - recent_actions.order_by("reviewed_by").values_list("reviewed_by", flat=True) - ) - - for user_id in user_ids: - user = ConcordiaUser.objects.get(id=user_id) - incident_count = user.review_incidents(recent_accepts, recent_rejects) - if incident_count > 0: - user_incident_count.append((user.id, user.username, incident_count)) - - return user_incident_count - - def transcribe_incidents(self): - user_incident_count = [] - transcriptions = self.get_queryset().filter( - submitted__gte=ONE_DAY_AGO, user__is_superuser=False, user__is_staff=False - ) - user_ids = ( - transcriptions.order_by("user") - .distinct("user") - .values_list("user", flat=True) - ) - - for user_id in user_ids: - user = ConcordiaUser.objects.get(id=user_id) - incident_count = user.transcribe_incidents(transcriptions) - if incident_count > 0: - user_incident_count.append((user.id, user.username, incident_count)) - - return user_incident_count + def reviewing_too_quickly(self, start=ONE_DAY_AGO): + with connection.cursor() as cursor: + cursor.execute( + f"""SELECT u.id, u.username, COUNT(*) + FROM concordia_transcription t1 + JOIN concordia_transcription t2 + ON t1.id < t2.id + JOIN concordia_transcription t3 + ON t2.id < t3.id + AND t1.reviewed_by_id = t2.reviewed_by_id + AND t2.reviewed_by_id = t3.reviewed_by_id + AND t1.accepted >= '{start}' + AND t2.accepted >= '{start}' + AND t3.accepted >= '{start}' + AND ABS( + EXTRACT(EPOCH FROM (t1.updated_on - t2.updated_on)) + ) < 60 + AND ABS( + EXTRACT(EPOCH FROM (t1.updated_on - t3.updated_on)) + ) < 60 + AND ABS(EXTRACT( + EPOCH FROM (t2.updated_on - t3.updated_on)) + ) < 60 + JOIN auth_user u on t1.reviewed_by_id = u.id + WHERE u.is_superuser = FALSE and u.is_staff = False + GROUP BY u.id, u.username""" # nosec B608 + ) + return cursor.fetchall() + + def transcribing_too_quickly(self, start=ONE_DAY_AGO): + with connection.cursor() as cursor: + cursor.execute( + f"""SELECT u.id, u.username, COUNT(*) + FROM concordia_transcription t1 + JOIN concordia_transcription t2 + ON t1.id < t2.id + JOIN concordia_transcription t3 + ON t2.id < t3.id + AND t1.user_id = t2.user_id + AND t2.user_id = t3.user_id + AND t1.submitted >= '{start}' + AND t2.submitted >= '{start}' + AND t3.submitted >= '{start}' + AND ABS( + EXTRACT(EPOCH FROM (t1.created_on - t2.created_on)) + ) < 60 + AND ABS( + EXTRACT(EPOCH FROM (t1.created_on - t3.created_on)) + ) < 60 + AND ABS( + EXTRACT(EPOCH FROM (t2.created_on - t3.created_on)) + ) < 60 + JOIN auth_user u on t1.user_id = u.id + WHERE u.is_superuser = FALSE and u.is_staff = False + GROUP BY u.id, u.username""" # nosec B608 + ) + return cursor.fetchall() class Transcription(MetricsModelMixin("transcription"), models.Model): @@ -994,13 +962,10 @@ def on_transcription_save(sender, instance, **kwargs): user=user, campaign=instance.asset.item.project.campaign, ) - profile, created = UserProfile.objects.get_or_create(user=user) if created: setattr(user_profile_activity, attr_name, 1) - setattr(profile, attr_name, 1) else: setattr(user_profile_activity, attr_name, F(attr_name) + 1) - setattr(profile, attr_name, F(attr_name) + 1) q = Q(transcription__user=user) | Q(transcription__reviewed_by=user) user_profile_activity.asset_count = ( Asset.objects.filter(q) @@ -1009,7 +974,6 @@ def on_transcription_save(sender, instance, **kwargs): .count() ) user_profile_activity.save() - profile.save() post_save.connect(on_transcription_save, sender=Transcription) diff --git a/concordia/signals/handlers.py b/concordia/signals/handlers.py index 72f12620b..47451cfae 100644 --- a/concordia/signals/handlers.py +++ b/concordia/signals/handlers.py @@ -14,7 +14,7 @@ from django_registration.signals import user_activated, user_registered from flags.state import flag_enabled -from ..models import Asset, Transcription, TranscriptionStatus, UserProfile +from ..models import Asset, Transcription, TranscriptionStatus from ..tasks import calculate_difficulty_values from .signals import reservation_obtained, reservation_released @@ -191,9 +191,3 @@ def send_asset_reservation_message( @receiver(post_delete, sender=Asset) def remove_file_from_s3(sender, instance, using, **kwargs): instance.storage_image.delete(save=False) - - -@receiver(post_save, sender=settings.AUTH_USER_MODEL) -def create_user_profile(sender, instance, *args, **kwargs): - if not hasattr(instance, "profile"): - UserProfile.objects.create(user=instance) diff --git a/concordia/static/js/src/asset-reservation.js b/concordia/static/js/src/asset-reservation.js index 9e2299b59..bb1ef4cfe 100644 --- a/concordia/static/js/src/asset-reservation.js +++ b/concordia/static/js/src/asset-reservation.js @@ -1,7 +1,5 @@ /* global jQuery displayMessage displayHtmlMessage buildErrorMessage Sentry */ -/* exported attemptToReserveAsset reserveAssetForEditing */ - -const assetData = document.currentScript.dataset; +/* exported attemptToReserveAsset */ function attemptToReserveAsset(reservationURL, findANewPageURL, actionType) { var $transcriptionEditor = jQuery('#transcription-editor'); @@ -97,15 +95,3 @@ function attemptToReserveAsset(reservationURL, findANewPageURL, actionType) { } }); } - -function reserveAssetForEditing() { - if (assetData.reserveAssetUrl) { - attemptToReserveAsset(assetData.reserveAssetUrl, '', 'transcribe'); - } -} - -jQuery(function () { - if (assetData.reserveForEditing) { - reserveAssetForEditing(); - } -}); diff --git a/concordia/static/js/src/banner.js b/concordia/static/js/src/banner.js deleted file mode 100644 index a46e3ea2a..000000000 --- a/concordia/static/js/src/banner.js +++ /dev/null @@ -1,18 +0,0 @@ -/* global $ */ - -if (typeof Storage !== 'undefined') { - if (!(window.screen.width < 1024 || window.screen.height < 768)) { - for (var key in localStorage) { - if (key.startsWith('banner-')) { - if ($('#' + key).hasClass('alert')) { - $('#' + key).attr('hidden', true); - } - } - } - } -} - -$('#no-interface-banner').click(function (event) { - localStorage.setItem(event.target.parentElement.id, true); - $('#' + event.target.parentElement.id).attr('hidden', true); -}); diff --git a/concordia/static/js/src/contribute.js b/concordia/static/js/src/contribute.js index f477ef02e..30f9c8942 100644 --- a/concordia/static/js/src/contribute.js +++ b/concordia/static/js/src/contribute.js @@ -766,27 +766,4 @@ function setupPage() { } } -let transcriptionForm = document.getElementById('transcription-editor'); -let ocrForm = document.getElementById('ocr-transcription-form'); - -let formChanged = false; -transcriptionForm.addEventListener('change', function () { - formChanged = true; -}); -transcriptionForm.addEventListener('submit', function () { - formChanged = false; -}); -if (ocrForm) { - ocrForm.addEventListener('submit', function () { - formChanged = false; - }); -} -window.addEventListener('beforeunload', function (event) { - if (formChanged) { - // Some browsers ignore this value and always display a built-in message instead - return (event.returnValue = - "The transcription you've started has not been saved."); - } -}); - setupPage(); diff --git a/concordia/static/js/src/guide.js b/concordia/static/js/src/guide.js deleted file mode 100644 index 2bfe28570..000000000 --- a/concordia/static/js/src/guide.js +++ /dev/null @@ -1,86 +0,0 @@ -/* global $ trackUIInteraction */ -/* exported openOffcanvas closeOffcanvas showPane hidePane */ - -function openOffcanvas() { - var guide = document.getElementById('guide-sidebar'); - guide.classList.remove('offscreen'); - guide.style.borderWidth = '0 0 thick thick'; - guide.style.borderStyle = 'solid'; - guide.style.borderColor = '#0076ad'; - document.getElementById('open-guide').style.display = 'none'; - document.addEventListener('keydown', function (event) { - if (event.key == 'Escape') { - closeOffcanvas(); - } - }); -} - -function closeOffcanvas() { - var guide = document.getElementById('guide-sidebar'); - guide.classList.add('offscreen'); - - guide.style.border = 'none'; - document.getElementById('open-guide').style.display = 'block'; -} - -function showPane(elementId) { - document.getElementById(elementId).classList.add('show', 'active'); - document.getElementById('guide-nav').classList.remove('show', 'active'); -} - -function hidePane(elementId) { - document.getElementById(elementId).classList.remove('show', 'active'); - document.getElementById('guide-nav').classList.add('show', 'active'); -} - -function trackHowToInteraction(element, label) { - trackUIInteraction(element, 'How To Guide', 'click', label); -} - -$('#open-guide').on('click', function () { - trackHowToInteraction($(this), 'Open'); -}); -$('#close-guide').on('click', function () { - trackHowToInteraction($(this), 'Close'); -}); -$('#previous-guide').on('click', function () { - trackHowToInteraction($(this), 'Back'); -}); -$('#next-guide').on('click', function () { - trackHowToInteraction($(this), 'Next'); -}); -$('#guide-bars').on('click', function () { - trackHowToInteraction($(this), 'Hamburger Menu'); -}); -$('#guide-sidebar .nav-link').on('click', function () { - let label = $(this).text().trim(); - trackHowToInteraction($(this), label); -}); - -$('#guide-carousel') - .carousel({ - interval: false, - wrap: false, - }) - .on('slide.bs.carousel', function (event) { - if (event.to == 0) { - $('#guide-bars').addClass('d-none'); - } else { - $('#guide-bars').removeClass('d-none'); - } - }); - -$('#previous-card').hide(); - -$('#card-carousel').on('slid.bs.carousel', function () { - if ($('#card-carousel .carousel-item:first').hasClass('active')) { - $('#previous-card').hide(); - $('#next-card').show(); - } else if ($('#card-carousel .carousel-item:last').hasClass('active')) { - $('#previous-card').show(); - $('#next-card').hide(); - } else { - $('#previous-card').show(); - $('#next-card').show(); - } -}); diff --git a/concordia/static/js/src/ocr.js b/concordia/static/js/src/ocr.js deleted file mode 100644 index 8c5cf0d07..000000000 --- a/concordia/static/js/src/ocr.js +++ /dev/null @@ -1,7 +0,0 @@ -/* global $ */ -/* exported selectLanguage */ - -function selectLanguage() { - $('#ocr-transcription-modal').modal('hide'); - $('#language-selection-modal').modal('show'); -} diff --git a/concordia/static/js/src/quick-tips.js b/concordia/static/js/src/quick-tips.js deleted file mode 100644 index 139eb3acf..000000000 --- a/concordia/static/js/src/quick-tips.js +++ /dev/null @@ -1,38 +0,0 @@ -/* global $ trackUIInteraction setTutorialHeight */ - -function trackQuickTipsInteraction(element, label) { - trackUIInteraction(element, 'Quick Tips', 'click', label); -} - -var mainContentHeight = $('#contribute-main-content').height(); -if (mainContentHeight < 710) { - $('.sidebar').height(mainContentHeight - 130); -} - -$('#tutorial-popup').on('shown.bs.modal', function () { - setTutorialHeight(); -}); - -$('#quick-tips').on('click', function () { - trackQuickTipsInteraction($(this), 'Open'); -}); -$('#previous-card').on('click', function () { - trackQuickTipsInteraction($(this), 'Back'); -}); -$('#next-card').on('click', function () { - trackQuickTipsInteraction($(this), 'Next'); -}); -$('.carousel-indicators li').on('click', function () { - let index = [...this.parentElement.children].indexOf(this); - trackQuickTipsInteraction($(this), `Carousel ${index}`); -}); -$('#tutorial-popup').on('hidden.bs.modal', function () { - // We're tracking whenever the popup closes, so we don't separately track the close button being clicked - trackUIInteraction($(this), 'Quick Tips', 'click', 'Close'); -}); -$('#tutorial-popup').on('shown-on-load', function () { - // We set a timeout to make sure the analytics code is loaded before trying to track - setTimeout(function () { - trackUIInteraction($(this), 'Quick Tips', 'load', 'Open'); - }, 1000); -}); diff --git a/concordia/static/js/src/viewer-split.js b/concordia/static/js/src/viewer-split.js deleted file mode 100644 index 2769b7b49..000000000 --- a/concordia/static/js/src/viewer-split.js +++ /dev/null @@ -1,146 +0,0 @@ -/* global Split seadragonViewer */ - -let pageSplit; -let contributeContainer = document.getElementById('contribute-container'); -let ocrSection = document.getElementById('ocr-section'); -let editorColumn = document.getElementById('editor-column'); -let viewerColumn = document.getElementById('viewer-column'); -let layoutColumns = ['#viewer-column', '#editor-column']; -let verticalKey = 'transcription-split-sizes-vertical'; -let horizontalKey = 'transcription-split-sizes-horizontal'; - -let sizesVertical = localStorage.getItem(verticalKey); - -if (sizesVertical) { - sizesVertical = JSON.parse(sizesVertical); -} else { - sizesVertical = [50, 50]; -} - -let sizesHorizontal = localStorage.getItem(horizontalKey); - -if (sizesHorizontal) { - sizesHorizontal = JSON.parse(sizesHorizontal); -} else { - sizesHorizontal = [50, 50]; -} - -let splitDirection = localStorage.getItem('transcription-split-direction'); - -if (splitDirection) { - splitDirection = JSON.parse(splitDirection); -} else { - splitDirection = 'h'; -} - -function saveSizes(sizes) { - let sizeKey; - if (splitDirection == 'h') { - sizeKey = horizontalKey; - sizesHorizontal = sizes; - } else { - sizeKey = verticalKey; - sizesVertical = sizes; - } - localStorage.setItem(sizeKey, JSON.stringify(sizes)); -} - -function saveDirection(direction) { - localStorage.setItem( - 'transcription-split-direction', - JSON.stringify(direction), - ); -} - -function verticalSplit() { - splitDirection = 'v'; - saveDirection(splitDirection); - contributeContainer.classList.remove('flex-row'); - contributeContainer.classList.add('flex-column'); - viewerColumn.classList.remove('h-100'); - if (ocrSection != undefined) { - editorColumn.prepend(ocrSection); - } - - return Split(layoutColumns, { - sizes: sizesVertical, - minSize: 100, - gutterSize: 8, - direction: 'vertical', - elementStyle: function (dimension, size, gutterSize) { - return { - 'flex-basis': 'calc(' + size + '% - ' + gutterSize + 'px)', - }; - }, - gutterStyle: function (dimension, gutterSize) { - return { - 'flex-basis': gutterSize + 'px', - }; - }, - onDragEnd: saveSizes, - }); -} -function horizontalSplit() { - splitDirection = 'h'; - saveDirection(splitDirection); - contributeContainer.classList.remove('flex-column'); - contributeContainer.classList.add('flex-row'); - viewerColumn.classList.add('h-100'); - if (ocrSection != undefined) { - viewerColumn.append(ocrSection); - } - return Split(layoutColumns, { - sizes: sizesHorizontal, - minSize: 100, - gutterSize: 8, - elementStyle: function (dimension, size, gutterSize) { - return { - 'flex-basis': 'calc(' + size + '% - ' + gutterSize + 'px)', - }; - }, - gutterStyle: function (dimension, gutterSize) { - return { - 'flex-basis': gutterSize + 'px', - }; - }, - onDragEnd: saveSizes, - }); -} - -document - .getElementById('viewer-layout-horizontal') - .addEventListener('click', function () { - if (splitDirection != 'h') { - if (pageSplit != undefined) { - pageSplit.destroy(); - } - pageSplit = horizontalSplit(); - setTimeout(function () { - // Some quirk in the viewer makes this - // sometimes not work depending on - // the rotation, unless it's delayed. - // Less than 10ms didn't reliable work. - seadragonViewer.viewport.zoomTo(1); - }, 10); - } - }); - -document - .getElementById('viewer-layout-vertical') - .addEventListener('click', function () { - if (splitDirection != 'v') { - if (pageSplit != undefined) { - pageSplit.destroy(); - } - pageSplit = verticalSplit(); - setTimeout(function () { - seadragonViewer.viewport.zoomTo(1); - }, 10); - } - }); - -if (splitDirection == 'v') { - pageSplit = verticalSplit(); -} else { - pageSplit = horizontalSplit(); -} diff --git a/concordia/static/js/src/viewer.js b/concordia/static/js/src/viewer.js deleted file mode 100644 index 9ad073409..000000000 --- a/concordia/static/js/src/viewer.js +++ /dev/null @@ -1,200 +0,0 @@ -/* global OpenSeadragon screenfull debounce */ -/* exported seadragonView stepUp stepDown resetImageFilterForms */ - -const viewerData = document.currentScript.dataset; - -var seadragonViewer = OpenSeadragon({ - id: 'asset-image', - prefixUrl: viewerData.prefixUrl, - tileSources: { - type: 'image', - url: viewerData.tileSourceUrl, - }, - gestureSettingsTouch: { - pinchRotate: true, - }, - showNavigator: true, - showRotationControl: true, - showFlipControl: true, - toolbar: 'viewer-controls', - zoomInButton: 'viewer-zoom-in', - zoomOutButton: 'viewer-zoom-out', - homeButton: 'viewer-home', - rotateLeftButton: 'viewer-rotate-left', - rotateRightButton: 'viewer-rotate-right', - flipButton: 'viewer-flip', - crossOriginPolicy: 'Anonymous', -}); - -// We need to define our own fullscreen function rather than using OpenSeadragon's -// because the built-in fullscreen function overwrites the DOM with the viewer, -// breaking our extra controls, such as the image filters. -if (screenfull.isEnabled) { - let fullscreenButton = document.querySelector('#viewer-fullscreen'); - fullscreenButton.addEventListener('click', function (event) { - event.preventDefault(); - let targetElement = document.querySelector( - fullscreenButton.dataset.target, - ); - if (screenfull.isFullscreen) { - screenfull.exit(); - } else { - screenfull.request(targetElement); - } - }); -} - -// The buttons configured as controls for the viewer don't properly get focus -// when clicked. This mostly isn't a problem, but causes odd-looking behavior -// when one of the extra buttons in the control bar is clicked (and therefore -// focused) first--clicking the control button leaves the focus on the extra -// button. -// TODO: Attempting to add focus to the clicked button here doesn't consistently -// work for unknown reasons, so it just removes focus from the extra buttons -// for now -let viewerControlButtons = document.querySelectorAll('.viewer-control-button'); -for (const node of viewerControlButtons) { - node.addEventListener('click', function () { - let focusedButton = document.querySelector( - '.extra-control-button:focus', - ); - if (focusedButton) { - focusedButton.blur(); - } - }); -} - -/* - * Image filter handling - */ - -let availableFilters = [ - { - formId: 'gamma-form', - inputId: 'gamma', - getFilter: function () { - let value = document.getElementById(this.inputId).value; - if ( - !Number.isNaN(value) && - value != 1 && - value >= 0 && - value <= 5 - ) { - return OpenSeadragon.Filters.GAMMA(value); - } - }, - }, - { - formId: 'invert-form', - inputId: 'invert', - getFilter: function () { - let value = document.getElementById(this.inputId).checked; - if (value) { - return OpenSeadragon.Filters.INVERT(); - } - }, - }, - { - formId: 'threshold-form', - inputId: 'threshold', - getFilter: function () { - let value = document.getElementById(this.inputId).value; - if (!Number.isNaN(value) && value > 0 && value <= 255) { - return OpenSeadragon.Filters.THRESHOLDING(value); - } - }, - }, -]; - -function updateFilters() { - let filters = []; - for (const filterData of availableFilters) { - let filter = filterData.getFilter(); - if (filter) { - filters.push(filter); - } - } - - seadragonViewer.setFilterOptions({ - filters: { - processors: filters, - }, - }); -} - -for (const filterData of availableFilters) { - let form = document.getElementById(filterData.formId); - if (form) { - form.addEventListener('change', updateFilters); - form.addEventListener('reset', function () { - // We use setTimeout to push the updateFilters - // call to the next event cycle in order to - // call it after the form is reset, instead - // of before, which is when this listener - // triggers - setTimeout(updateFilters); - }); - } - - let input = document.getElementById(filterData.inputId); - if (input) { - // We use debounce here so that updateFilters is only called once, - // after the user stops typing or scrolling with their mousewheel - input.addEventListener( - 'keyup', - debounce(() => updateFilters()), - ); - input.addEventListener( - 'wheel', - debounce(() => updateFilters()), - ); - } -} - -/* - * Image filter form handling - */ -function stepUp(id) { - let input = document.getElementById(id); - input.stepUp(); - input.dispatchEvent(new Event('input', {bubbles: true})); - input.dispatchEvent(new Event('change', {bubbles: true})); - return false; -} - -function stepDown(id) { - let input = document.getElementById(id); - input.stepDown(); - input.dispatchEvent(new Event('input', {bubbles: true})); - input.dispatchEvent(new Event('change', {bubbles: true})); - return false; -} - -function resetImageFilterForms() { - for (const filterData of availableFilters) { - let form = document.getElementById(filterData.formId); - form.reset(); - } -} - -let gammaNumber = document.getElementById('gamma'); -let gammaRange = document.getElementById('gamma-range'); - -gammaNumber.addEventListener('input', function () { - gammaRange.value = gammaNumber.value; -}); - -gammaRange.addEventListener('input', function () { - gammaNumber.value = gammaRange.value; -}); - -let thresholdNumber = document.getElementById('threshold'); -let thresholdRange = document.getElementById('threshold-range'); - -thresholdNumber.addEventListener('input', function () { - thresholdRange.value = thresholdNumber.value; -}); - -thresholdRange.addEventListener('input', function () { - thresholdNumber.value = thresholdRange.value; -}); diff --git a/concordia/tasks.py b/concordia/tasks.py index 8445b0670..bb3054dad 100644 --- a/concordia/tasks.py +++ b/concordia/tasks.py @@ -1043,6 +1043,14 @@ def fix_storage_images(campaign_slug=None, asset_start_id=None): logger.debug("%s / %s (%s%%)", count, full_count, str(count / full_count * 100)) +def transcribing_too_quickly(): + return Transcription.objects.transcribing_too_quickly() + + +def reviewing_too_quickly(): + return Transcription.objects.reviewing_too_quickly() + + @celery_app.task(ignore_result=True) def clear_sessions(): # This clears expired Django sessions in the database @@ -1059,8 +1067,8 @@ def unusual_activity(): "title": "Unusual User Activity Report for " + timezone.now().strftime("%b %d %Y, %I:%M %p"), "domain": "https://" + site.domain, - "transcriptions": Transcription.objects.transcribe_incidents(), - "reviews": Transcription.objects.review_incidents(), + "transcriptions": transcribing_too_quickly(), + "reviews": reviewing_too_quickly(), } text_body_template = loader.get_template("emails/unusual_activity.txt") diff --git a/concordia/templates/admin/concordia/item/change_form.html b/concordia/templates/admin/concordia/item/change_form.html index 835d87b2e..4e0dc3352 100644 --- a/concordia/templates/admin/concordia/item/change_form.html +++ b/concordia/templates/admin/concordia/item/change_form.html @@ -29,3 +29,18 @@ {% endif %} {{ block.super }} {% endblock %} + +{% block extrahead %} + {{ block.super }} + + {% include 'fragments/codemirror.html' %} +{% endblock extrahead %} + + +{% block content %} + {{ block.super }} + + +{% endblock content %} diff --git a/concordia/templates/admin/concordia/topic/change_form.html b/concordia/templates/admin/concordia/topic/change_form.html new file mode 100644 index 000000000..e461b658c --- /dev/null +++ b/concordia/templates/admin/concordia/topic/change_form.html @@ -0,0 +1,19 @@ +{% extends "admin/change_form.html" %} + +{% load i18n admin_urls static %} + +{% block extrahead %} + {{ block.super }} + + {% include 'fragments/codemirror.html' %} +{% endblock extrahead %} + + +{% block content %} + {{ block.super }} + + +{% endblock content %} diff --git a/concordia/templates/emails/unusual_activity.txt b/concordia/templates/emails/unusual_activity.txt index daba6e152..eb2bf8549 100644 --- a/concordia/templates/emails/unusual_activity.txt +++ b/concordia/templates/emails/unusual_activity.txt @@ -6,7 +6,7 @@ Incidents of two or more transcriptions submitted within a single minute: No transcriptions fell within the window. {% endfor %} Incidents of two or more transcriptions reviewed within a single minute: -{% for row in reviews %} +{% for row in reviews_by %} {{ row.1 }} | {{ row.2 }} {% empty %} No reviews fell within the window. diff --git a/concordia/templates/transcriptions/asset_detail.html b/concordia/templates/transcriptions/asset_detail.html index f67474313..5ed967fbf 100644 --- a/concordia/templates/transcriptions/asset_detail.html +++ b/concordia/templates/transcriptions/asset_detail.html @@ -18,25 +18,6 @@ - - - - - - - - - - - {% endblock head_content %} {% block breadcrumbs %} @@ -54,13 +35,330 @@
Campaigns
+Your saves, submits, and reviews
Pages Worked On
+Campaigns you've worked on
Actions
+Pages you've worked on
Campaigns
-Your saves, submits, and reviews
+Campaigns you've worked on
Pages Worked On
-Campaigns you've worked on
+Pages you've worked on
Actions
-Pages you've worked on
+Your saves, submits, and reviews
Campaigns
-Campaigns you've worked on
+Projects you've worked on
Pages Worked On
+Pages
Pages you've worked on