diff --git a/django/anthology/settings/__init__.py b/django/anthology/settings/__init__.py index 4ce272504d0a4337c5d48a84383eb4b6e3a507be..1c0f6451eb0ab5610d8f78540ff0eec9fec2a778 100644 --- a/django/anthology/settings/__init__.py +++ b/django/anthology/settings/__init__.py @@ -8,13 +8,15 @@ BASE_DIR = Path(__file__).resolve(strict=True).parents[2] # Application definition INSTALLED_APPS = [ - "user.apps.UserConfig", + "meleager_user.apps.MeleagerUserConfig", "web.apps.WebConfig", "meleager", "guardian", "django_extensions", "django_select2", "rest_framework", + "django_filters", + "graphene_django", "django.contrib.gis", "django.contrib.admin", "django.contrib.auth", @@ -121,7 +123,7 @@ USE_TZ = True STATIC_URL = "/static/" -AUTH_USER_MODEL = "user.User" +AUTH_USER_MODEL = "meleager_user.User" CACHES = { "default": { @@ -141,7 +143,8 @@ CACHES = { REST_FRAMEWORK = { "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", - "PAGE_SIZE": 10, + 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], + "PAGE_SIZE": 50, } LOGGING = { diff --git a/django/anthology/settings/antholont.py b/django/anthology/settings/antholont.py index 0f2ca41b8d2f1e810ddcd13e09a309be895accfb..3bacdf454c2714822e0f02182f5b2b15f333b3d3 100644 --- a/django/anthology/settings/antholont.py +++ b/django/anthology/settings/antholont.py @@ -6,6 +6,7 @@ SECRET_KEY = "qv!wbd*(@1wx&et0djf#2w_rjww_c$$((u0z@4uck1faf=3!mq" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True +USE_X_FORWARDED_HOST = True ALLOWED_HOSTS = ["antholont.ecrituresnumeriques.ca", "localhost", "127.0.0.1"] # The number of validation level permissions in this application. @@ -14,3 +15,9 @@ ALLOWED_HOSTS = ["antholont.ecrituresnumeriques.ca", "localhost", "127.0.0.1"] MAX_VALIDATION_LEVEL = 2 AP_API_IMPORT_DIR = "import_ap_api_data" +# Configure Graphene schema +GRAPHENE = { + "SCHEMA": "web.schema.schema" +} + +LOGIN_REDIRECT_URL = "/user/profile/" diff --git a/django/anthology/settings/local.py b/django/anthology/settings/local.py index 8dd4869361ed1d791bde3d2be8413187a91a6ed4..cd3318f2159aa9de58ad6897a5550722467b4951 100644 --- a/django/anthology/settings/local.py +++ b/django/anthology/settings/local.py @@ -1,3 +1,12 @@ from .antholont import * DATABASES["default"]["HOST"] = "localhost" + +INSTALLED_APPS.append("debug_toolbar") +MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") +INTERNAL_IPS = [ + "127.0.0.1", + "192.168.0.0", +] + +ROOT_URLCONF="anthology.urls_local" \ No newline at end of file diff --git a/django/anthology/urls.py b/django/anthology/urls.py index 57588784363a85c6b3528ed9d656be643e3c772c..7533305f9d3d92fee648d549c5b49746f5bd0537 100644 --- a/django/anthology/urls.py +++ b/django/anthology/urls.py @@ -3,10 +3,17 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path +from django.views.decorators.csrf import csrf_exempt +from graphene_django.views import GraphQLView + urlpatterns = [ - path("api", include("meleager.api_urls")), - path("api-auth", include("rest_framework.urls", namespace="rest_framework")), + path("api/", include("meleager.api_urls")), + path("api-auth/", include("rest_framework.urls", namespace="rest_framework")), path("", include("web.urls")), path("admin/", admin.site.urls), path("select2/", include("django_select2.urls")), + path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))), + path("auth/", include("meleager_user.auth_urls")), + path("user/", include("meleager_user.user_urls")), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + diff --git a/django/anthology/urls_local.py b/django/anthology/urls_local.py new file mode 100644 index 0000000000000000000000000000000000000000..d031bff99ca70821af4a4daa7e12e6b9ba971a2f --- /dev/null +++ b/django/anthology/urls_local.py @@ -0,0 +1,8 @@ +import debug_toolbar +from django.urls import include, path + +from .urls import urlpatterns + +urlpatterns.append( + path('__debug__/', include(debug_toolbar.urls)), +) \ No newline at end of file diff --git a/django/meleager/admin/passage.py b/django/meleager/admin/passage.py index 1b324017ed133b9e79418eb025c071d37c03928c..ac0a9511ed50a153fe80bbfdddcbe8375d47c7aa 100644 --- a/django/meleager/admin/passage.py +++ b/django/meleager/admin/passage.py @@ -6,8 +6,8 @@ class PassageAdmin(admin.ModelAdmin): ordering = ('book__number', 'fragment', 'sub_fragment') list_filter = ('book__number', ) search_fields = ('descriptions__description', ) - autocomplete_fields = ('descriptions', 'authors', 'keywords', 'city') - fields = ('descriptions', 'creator', 'last_editor', 'city', 'authors', 'book', 'fragment', 'sub_fragment', 'keywords') + autocomplete_fields = ('descriptions', 'authors', 'keywords', 'cities') + fields = ('descriptions', 'creator', 'last_editor', 'cities', 'authors', 'book', 'fragment', 'sub_fragment', 'keywords') def get_reference(self, obj): return f"{obj.book.number}.{obj.fragment}{obj.sub_fragment}" diff --git a/django/meleager/fixtures/test_data.json b/django/meleager/fixtures/test_data.json index 43c751c1dc69b128ae4edde0dd53af8269f569c3..6b76762bafc02740402964ade640b427897ca487 100644 --- a/django/meleager/fixtures/test_data.json +++ b/django/meleager/fixtures/test_data.json @@ -1 +1,2456 @@ -[{"model": "meleager.work", "pk": 1, "fields": {"created_at": "2020-11-14T02:33:59.818Z", "updated_at": "2020-11-14T02:33:59.818Z", "creator": null, "last_editor": null, "urn": null, "descriptions": [], "alternative_urns": [], "names": []}}, {"model": "meleager.book", "fields": {"created_at": "2020-11-14T02:34:10.052Z", "updated_at": "2020-11-14T02:34:10.052Z", "creator": null, "last_editor": null, "work": 1, "number": 12, "descriptions": []}}, {"model": "meleager.passage", "fields": {"created_at": "2020-11-14T02:34:28.654Z", "updated_at": "2020-11-14T02:34:28.654Z", "creator": null, "last_editor": null, "urn": null, "validation": 0, "city": null, "book": [12, 1], "fragment": 42, "sub_fragment": "", "descriptions": [], "alternative_urns": [], "authors": [], "images": [], "comments": [], "external_references": [], "internal_references": [], "manuscripts": [], "keywords": []}}, {"model": "meleager.passage", "fields": {"created_at": "2020-11-14T02:34:49.041Z", "updated_at": "2020-11-14T02:34:49.041Z", "creator": null, "last_editor": null, "urn": null, "validation": 0, "city": null, "book": [12, 1], "fragment": 12, "sub_fragment": "abc", "descriptions": [], "alternative_urns": [], "authors": [], "images": [], "comments": [], "external_references": [], "internal_references": [], "manuscripts": [], "keywords": []}}] \ No newline at end of file +[ +{ + "model": "meleager.language", + "pk": "eng", + "fields": { + "iso_name": "English", + "preferred": true + } +}, +{ + "model": "meleager.language", + "pk": "fra", + "fields": { + "iso_name": "French", + "preferred": true + } +}, +{ + "model": "meleager.language", + "pk": "grc", + "fields": { + "iso_name": "Ancient Greek (to 1453)", + "preferred": true + } +}, +{ + "model": "meleager.language", + "pk": "ita", + "fields": { + "iso_name": "Italian", + "preferred": true + } +}, +{ + "model": "meleager.language", + "pk": "por", + "fields": { + "iso_name": "Portuguese", + "preferred": true + } +}, +{ + "model": "meleager.language", + "pk": "spa", + "fields": { + "iso_name": "Spanish", + "preferred": true + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "user", + "model": "user" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "alignment" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "alternativeuri" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "author" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "book" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "city" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "comment" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "description" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "edition" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "editor" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "externalreference" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "keyword" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "keywordcategory" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "language" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "manuscript" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "medium" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "name" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "passage" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "scholium" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "text" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "urn" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "validationlevelname" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "meleager", + "model": "work" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "guardian", + "model": "groupobjectpermission" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "guardian", + "model": "userobjectpermission" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "admin", + "model": "logentry" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "auth", + "model": "permission" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "auth", + "model": "group" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "contenttypes", + "model": "contenttype" + } +}, +{ + "model": "contenttypes.contenttype", + "fields": { + "app_label": "sessions", + "model": "session" + } +}, +{ + "model": "sessions.session", + "pk": "xzmiojspaetc6g1x7znntd70kmsrds84", + "fields": { + "session_data": ".eJxVjMsOwiAQRf-FtSEwyGNcuu83kIGhUjU0Ke3K-O_apAvd3nPOfYlI21rj1ssSJxYXAeL0uyXKj9J2wHdqt1nmua3LlOSuyIN2OcxcntfD_Tuo1Ou3DsaEgg6Zi1Nj8N6y9oAAJrC1lCzpBJy1wxEAz95jQWWNAeXBmkTi_QG_tDab:1ku3Qa:lfpskGqkwDlraC26L9QdbALuse6Tw6P16RSgcmuUwHE", + "expire_date": "2021-01-12T01:03:56.674Z" + } +}, +{ + "model": "sessions.session", + "pk": "zwaydufosp2ebp90fy0b56wxvpuijq99", + "fields": { + "session_data": ".eJxVjMsOwiAQRf-FtSEwyGNcuu83kIGhUjU0Ke3K-O_apAvd3nPOfYlI21rj1ssSJxYXAeL0uyXKj9J2wHdqt1nmua3LlOSuyIN2OcxcntfD_Tuo1Ou3DsaEgg6Zi1Nj8N6y9oAAJrC1lCzpBJy1wxEAz95jQWWNAeXBmkTi_QG_tDab:1l36Ut:MXlIVzpjmswAOj5ZWlO5iEGQ8v8ZfsXydilNRgtTF98", + "expire_date": "2021-02-06T00:09:47.321Z" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add user", + "content_type": [ + "user", + "user" + ], + "codename": "add_user" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change user", + "content_type": [ + "user", + "user" + ], + "codename": "change_user" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete user", + "content_type": [ + "user", + "user" + ], + "codename": "delete_user" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view user", + "content_type": [ + "user", + "user" + ], + "codename": "view_user" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add alignment", + "content_type": [ + "meleager", + "alignment" + ], + "codename": "add_alignment" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change alignment", + "content_type": [ + "meleager", + "alignment" + ], + "codename": "change_alignment" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete alignment", + "content_type": [ + "meleager", + "alignment" + ], + "codename": "delete_alignment" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view alignment", + "content_type": [ + "meleager", + "alignment" + ], + "codename": "view_alignment" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "The permission to set validation level 0, see ValidationLevelName.validation_level = 0 for the validation level name.", + "content_type": [ + "meleager", + "alignment" + ], + "codename": "set_validation_level_0" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "The permission to set validation level 1, see ValidationLevelName.validation_level = 1 for the validation level name.", + "content_type": [ + "meleager", + "alignment" + ], + "codename": "set_validation_level_1" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add alternative uri", + "content_type": [ + "meleager", + "alternativeuri" + ], + "codename": "add_alternativeuri" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change alternative uri", + "content_type": [ + "meleager", + "alternativeuri" + ], + "codename": "change_alternativeuri" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete alternative uri", + "content_type": [ + "meleager", + "alternativeuri" + ], + "codename": "delete_alternativeuri" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view alternative uri", + "content_type": [ + "meleager", + "alternativeuri" + ], + "codename": "view_alternativeuri" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add author", + "content_type": [ + "meleager", + "author" + ], + "codename": "add_author" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change author", + "content_type": [ + "meleager", + "author" + ], + "codename": "change_author" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete author", + "content_type": [ + "meleager", + "author" + ], + "codename": "delete_author" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view author", + "content_type": [ + "meleager", + "author" + ], + "codename": "view_author" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add book", + "content_type": [ + "meleager", + "book" + ], + "codename": "add_book" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change book", + "content_type": [ + "meleager", + "book" + ], + "codename": "change_book" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete book", + "content_type": [ + "meleager", + "book" + ], + "codename": "delete_book" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view book", + "content_type": [ + "meleager", + "book" + ], + "codename": "view_book" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add city", + "content_type": [ + "meleager", + "city" + ], + "codename": "add_city" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change city", + "content_type": [ + "meleager", + "city" + ], + "codename": "change_city" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete city", + "content_type": [ + "meleager", + "city" + ], + "codename": "delete_city" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view city", + "content_type": [ + "meleager", + "city" + ], + "codename": "view_city" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add comment", + "content_type": [ + "meleager", + "comment" + ], + "codename": "add_comment" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change comment", + "content_type": [ + "meleager", + "comment" + ], + "codename": "change_comment" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete comment", + "content_type": [ + "meleager", + "comment" + ], + "codename": "delete_comment" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view comment", + "content_type": [ + "meleager", + "comment" + ], + "codename": "view_comment" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add description", + "content_type": [ + "meleager", + "description" + ], + "codename": "add_description" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change description", + "content_type": [ + "meleager", + "description" + ], + "codename": "change_description" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete description", + "content_type": [ + "meleager", + "description" + ], + "codename": "delete_description" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view description", + "content_type": [ + "meleager", + "description" + ], + "codename": "view_description" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add edition", + "content_type": [ + "meleager", + "edition" + ], + "codename": "add_edition" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change edition", + "content_type": [ + "meleager", + "edition" + ], + "codename": "change_edition" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete edition", + "content_type": [ + "meleager", + "edition" + ], + "codename": "delete_edition" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view edition", + "content_type": [ + "meleager", + "edition" + ], + "codename": "view_edition" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add editor", + "content_type": [ + "meleager", + "editor" + ], + "codename": "add_editor" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change editor", + "content_type": [ + "meleager", + "editor" + ], + "codename": "change_editor" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete editor", + "content_type": [ + "meleager", + "editor" + ], + "codename": "delete_editor" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view editor", + "content_type": [ + "meleager", + "editor" + ], + "codename": "view_editor" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add external reference", + "content_type": [ + "meleager", + "externalreference" + ], + "codename": "add_externalreference" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change external reference", + "content_type": [ + "meleager", + "externalreference" + ], + "codename": "change_externalreference" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete external reference", + "content_type": [ + "meleager", + "externalreference" + ], + "codename": "delete_externalreference" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view external reference", + "content_type": [ + "meleager", + "externalreference" + ], + "codename": "view_externalreference" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add keyword", + "content_type": [ + "meleager", + "keyword" + ], + "codename": "add_keyword" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change keyword", + "content_type": [ + "meleager", + "keyword" + ], + "codename": "change_keyword" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete keyword", + "content_type": [ + "meleager", + "keyword" + ], + "codename": "delete_keyword" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view keyword", + "content_type": [ + "meleager", + "keyword" + ], + "codename": "view_keyword" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add keyword category", + "content_type": [ + "meleager", + "keywordcategory" + ], + "codename": "add_keywordcategory" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change keyword category", + "content_type": [ + "meleager", + "keywordcategory" + ], + "codename": "change_keywordcategory" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete keyword category", + "content_type": [ + "meleager", + "keywordcategory" + ], + "codename": "delete_keywordcategory" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view keyword category", + "content_type": [ + "meleager", + "keywordcategory" + ], + "codename": "view_keywordcategory" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add language", + "content_type": [ + "meleager", + "language" + ], + "codename": "add_language" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change language", + "content_type": [ + "meleager", + "language" + ], + "codename": "change_language" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete language", + "content_type": [ + "meleager", + "language" + ], + "codename": "delete_language" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view language", + "content_type": [ + "meleager", + "language" + ], + "codename": "view_language" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add manuscript", + "content_type": [ + "meleager", + "manuscript" + ], + "codename": "add_manuscript" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change manuscript", + "content_type": [ + "meleager", + "manuscript" + ], + "codename": "change_manuscript" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete manuscript", + "content_type": [ + "meleager", + "manuscript" + ], + "codename": "delete_manuscript" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view manuscript", + "content_type": [ + "meleager", + "manuscript" + ], + "codename": "view_manuscript" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add medium", + "content_type": [ + "meleager", + "medium" + ], + "codename": "add_medium" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change medium", + "content_type": [ + "meleager", + "medium" + ], + "codename": "change_medium" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete medium", + "content_type": [ + "meleager", + "medium" + ], + "codename": "delete_medium" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view medium", + "content_type": [ + "meleager", + "medium" + ], + "codename": "view_medium" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add name", + "content_type": [ + "meleager", + "name" + ], + "codename": "add_name" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change name", + "content_type": [ + "meleager", + "name" + ], + "codename": "change_name" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete name", + "content_type": [ + "meleager", + "name" + ], + "codename": "delete_name" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view name", + "content_type": [ + "meleager", + "name" + ], + "codename": "view_name" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add passage", + "content_type": [ + "meleager", + "passage" + ], + "codename": "add_passage" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change passage", + "content_type": [ + "meleager", + "passage" + ], + "codename": "change_passage" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete passage", + "content_type": [ + "meleager", + "passage" + ], + "codename": "delete_passage" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view passage", + "content_type": [ + "meleager", + "passage" + ], + "codename": "view_passage" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add scholium", + "content_type": [ + "meleager", + "scholium" + ], + "codename": "add_scholium" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change scholium", + "content_type": [ + "meleager", + "scholium" + ], + "codename": "change_scholium" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete scholium", + "content_type": [ + "meleager", + "scholium" + ], + "codename": "delete_scholium" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view scholium", + "content_type": [ + "meleager", + "scholium" + ], + "codename": "view_scholium" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add text", + "content_type": [ + "meleager", + "text" + ], + "codename": "add_text" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change text", + "content_type": [ + "meleager", + "text" + ], + "codename": "change_text" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete text", + "content_type": [ + "meleager", + "text" + ], + "codename": "delete_text" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view text", + "content_type": [ + "meleager", + "text" + ], + "codename": "view_text" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add urn", + "content_type": [ + "meleager", + "urn" + ], + "codename": "add_urn" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change urn", + "content_type": [ + "meleager", + "urn" + ], + "codename": "change_urn" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete urn", + "content_type": [ + "meleager", + "urn" + ], + "codename": "delete_urn" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view urn", + "content_type": [ + "meleager", + "urn" + ], + "codename": "view_urn" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add validation level name", + "content_type": [ + "meleager", + "validationlevelname" + ], + "codename": "add_validationlevelname" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change validation level name", + "content_type": [ + "meleager", + "validationlevelname" + ], + "codename": "change_validationlevelname" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete validation level name", + "content_type": [ + "meleager", + "validationlevelname" + ], + "codename": "delete_validationlevelname" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view validation level name", + "content_type": [ + "meleager", + "validationlevelname" + ], + "codename": "view_validationlevelname" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add work", + "content_type": [ + "meleager", + "work" + ], + "codename": "add_work" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change work", + "content_type": [ + "meleager", + "work" + ], + "codename": "change_work" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete work", + "content_type": [ + "meleager", + "work" + ], + "codename": "delete_work" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view work", + "content_type": [ + "meleager", + "work" + ], + "codename": "view_work" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add group object permission", + "content_type": [ + "guardian", + "groupobjectpermission" + ], + "codename": "add_groupobjectpermission" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change group object permission", + "content_type": [ + "guardian", + "groupobjectpermission" + ], + "codename": "change_groupobjectpermission" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete group object permission", + "content_type": [ + "guardian", + "groupobjectpermission" + ], + "codename": "delete_groupobjectpermission" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view group object permission", + "content_type": [ + "guardian", + "groupobjectpermission" + ], + "codename": "view_groupobjectpermission" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add user object permission", + "content_type": [ + "guardian", + "userobjectpermission" + ], + "codename": "add_userobjectpermission" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change user object permission", + "content_type": [ + "guardian", + "userobjectpermission" + ], + "codename": "change_userobjectpermission" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete user object permission", + "content_type": [ + "guardian", + "userobjectpermission" + ], + "codename": "delete_userobjectpermission" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view user object permission", + "content_type": [ + "guardian", + "userobjectpermission" + ], + "codename": "view_userobjectpermission" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add log entry", + "content_type": [ + "admin", + "logentry" + ], + "codename": "add_logentry" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change log entry", + "content_type": [ + "admin", + "logentry" + ], + "codename": "change_logentry" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete log entry", + "content_type": [ + "admin", + "logentry" + ], + "codename": "delete_logentry" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view log entry", + "content_type": [ + "admin", + "logentry" + ], + "codename": "view_logentry" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add permission", + "content_type": [ + "auth", + "permission" + ], + "codename": "add_permission" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change permission", + "content_type": [ + "auth", + "permission" + ], + "codename": "change_permission" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete permission", + "content_type": [ + "auth", + "permission" + ], + "codename": "delete_permission" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view permission", + "content_type": [ + "auth", + "permission" + ], + "codename": "view_permission" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add group", + "content_type": [ + "auth", + "group" + ], + "codename": "add_group" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change group", + "content_type": [ + "auth", + "group" + ], + "codename": "change_group" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete group", + "content_type": [ + "auth", + "group" + ], + "codename": "delete_group" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view group", + "content_type": [ + "auth", + "group" + ], + "codename": "view_group" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add content type", + "content_type": [ + "contenttypes", + "contenttype" + ], + "codename": "add_contenttype" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change content type", + "content_type": [ + "contenttypes", + "contenttype" + ], + "codename": "change_contenttype" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete content type", + "content_type": [ + "contenttypes", + "contenttype" + ], + "codename": "delete_contenttype" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view content type", + "content_type": [ + "contenttypes", + "contenttype" + ], + "codename": "view_contenttype" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can add session", + "content_type": [ + "sessions", + "session" + ], + "codename": "add_session" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can change session", + "content_type": [ + "sessions", + "session" + ], + "codename": "change_session" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can delete session", + "content_type": [ + "sessions", + "session" + ], + "codename": "delete_session" + } +}, +{ + "model": "auth.permission", + "fields": { + "name": "Can view session", + "content_type": [ + "sessions", + "session" + ], + "codename": "view_session" + } +}, +{ + "model": "meleager_user.user", + "fields": { + "password": "!cftze7QXrzidvow2x53dqyGZFqBHkZg63vEFGzJw", + "last_login": null, + "is_superuser": false, + "username": "AnonymousUser", + "first_name": "", + "last_name": "", + "email": "", + "is_staff": false, + "is_active": true, + "date_joined": "2020-11-12T06:14:06.173Z", + "institution": "", + "groups": [], + "user_permissions": [] + } +}, +{ + "model": "meleager_user.user", + "fields": { + "password": "pbkdf2_sha256$216000$itKY4GPhYWHR$d1pd9QDaZZosNEzNcul6UCE/M3BtvK3DhRQiO1y6Ga4=", + "last_login": "2021-01-23T00:09:47.317Z", + "is_superuser": true, + "username": "test_admin", + "first_name": "", + "last_name": "", + "email": "admin@test.com", + "is_staff": true, + "is_active": true, + "date_joined": "2020-11-12T06:59:06.702Z", + "institution": "", + "groups": [], + "user_permissions": [] + } +}, +{ + "model": "meleager_user.user", + "fields": { + "password": "pbkdf2_sha256$216000$NWmmXTMZTxoW$afq3dy1KRUu0nTeoKihjhB2kaa7BQPxON+D+rvLXkGQ=", + "last_login": null, + "is_superuser": false, + "username": "meleager", + "first_name": "", + "last_name": "", + "email": "", + "is_staff": false, + "is_active": true, + "date_joined": "2020-12-29T01:07:01.535Z", + "institution": "", + "groups": [], + "user_permissions": [] + } +}, +{ + "model": "meleager.author", + "pk": 1, + "fields": { + "created_at": "2020-12-29T01:13:53Z", + "updated_at": "2020-12-29T01:13:53Z", + "creator": [ + "meleager" + ], + "last_editor": [ + "meleager" + ], + "urn": null, + "city_born": null, + "born_range_year_date": "{\"bounds\": \"[)\", \"lower\": \"-42\", \"upper\": null}", + "city_died": null, + "died_range_year_date": "{\"bounds\": \"[)\", \"lower\": \"42\", \"upper\": null}", + "descriptions": [ + 5 + ], + "alternative_urns": [], + "names": [ + 1 + ], + "images": [] + } +}, +{ + "model": "meleager.city", + "pk": 1, + "fields": { + "created_at": "2020-12-29T01:14:36Z", + "updated_at": "2020-12-29T01:14:36Z", + "creator": [ + "meleager" + ], + "last_editor": [ + "meleager" + ], + "location": "SRID=4326;POINT (-156.5457497324263 -35.17381082366806)", + "descriptions": [ + 6 + ], + "names": [ + 2 + ] + } +}, +{ + "model": "meleager.comment", + "pk": 1, + "fields": { + "created_at": "2020-12-29T02:23:29Z", + "updated_at": "2020-12-29T02:23:29Z", + "creator": [ + "meleager" + ], + "last_editor": [ + "meleager" + ], + "comment_type": "user_note", + "comment_title": "COMMENT TITLE", + "descriptions": [ + 8 + ], + "images": [] + } +}, +{ + "model": "meleager.description", + "pk": 1, + "fields": { + "created_at": "2020-12-29T01:07:32Z", + "updated_at": "2020-12-29T01:07:32Z", + "creator": [ + "meleager" + ], + "last_editor": [ + "meleager" + ], + "description": "WORK DESC FR", + "language": "fra" + } +}, +{ + "model": "meleager.description", + "pk": 2, + "fields": { + "created_at": "2020-12-29T01:08:38Z", + "updated_at": "2020-12-29T01:08:38Z", + "creator": [ + "meleager" + ], + "last_editor": [ + "meleager" + ], + "description": "WORK DESC EN", + "language": "eng" + } +}, +{ + "model": "meleager.description", + "pk": 3, + "fields": { + "created_at": "2020-12-29T01:09:20Z", + "updated_at": "2020-12-29T01:09:20Z", + "creator": [ + "meleager" + ], + "last_editor": [ + "meleager" + ], + "description": "BOOK DESC FR", + "language": "fra" + } +}, +{ + "model": "meleager.description", + "pk": 4, + "fields": { + "created_at": "2020-12-29T01:09:59Z", + "updated_at": "2020-12-29T01:09:59Z", + "creator": [ + "meleager" + ], + "last_editor": [ + "meleager" + ], + "description": "PASSAGE DESC FR", + "language": "fra" + } +}, +{ + "model": "meleager.description", + "pk": 5, + "fields": { + "created_at": "2020-12-29T01:11:18Z", + "updated_at": "2020-12-29T01:11:18Z", + "creator": [ + "meleager" + ], + "last_editor": [ + "meleager" + ], + "description": "AUTHOR DESC FR", + "language": "fra" + } +}, +{ + "model": "meleager.description", + "pk": 6, + "fields": { + "created_at": "2020-12-29T01:15:10Z", + "updated_at": "2020-12-29T01:15:10Z", + "creator": [ + "meleager" + ], + "last_editor": [ + "meleager" + ], + "description": "CITY DESC EN", + "language": "eng" + } +}, +{ + "model": "meleager.description", + "pk": 7, + "fields": { + "created_at": "2020-12-29T01:17:24Z", + "updated_at": "2020-12-29T01:17:24Z", + "creator": [ + "meleager" + ], + "last_editor": [ + "meleager" + ], + "description": "PASSAGE DESC POR", + "language": "por" + } +}, +{ + "model": "meleager.description", + "pk": 8, + "fields": { + "created_at": "2020-12-29T02:23:37Z", + "updated_at": "2020-12-29T02:23:37Z", + "creator": [ + "meleager" + ], + "last_editor": [ + "meleager" + ], + "description": "COMMENT DESC FR", + "language": "fra" + } +}, +{ + "model": "meleager.description", + "pk": 9, + "fields": { + "created_at": "2021-01-23T00:26:30Z", + "updated_at": "2021-01-23T00:26:30Z", + "creator": [ + "meleager" + ], + "last_editor": [ + "meleager" + ], + "description": "Scholium description", + "language": "eng" + } +}, +{ + "model": "meleager.name", + "pk": 1, + "fields": { + "created_at": "2020-12-29T01:10:55Z", + "updated_at": "2020-12-29T01:10:55Z", + "creator": [ + "meleager" + ], + "last_editor": [ + "meleager" + ], + "name": "AUTHOR FR", + "language": "fra" + } +}, +{ + "model": "meleager.name", + "pk": 2, + "fields": { + "created_at": "2020-12-29T01:15:50Z", + "updated_at": "2020-12-29T01:15:50Z", + "creator": [ + "meleager" + ], + "last_editor": [ + "meleager" + ], + "name": "CITY SPA", + "language": "spa" + } +}, +{ + "model": "meleager.work", + "pk": 1, + "fields": { + "created_at": "2019-04-01T10:00:00Z", + "updated_at": "2020-04-01T20:00:00Z", + "creator": [ + "meleager" + ], + "last_editor": [ + "meleager" + ], + "urn": null, + "alternative_urns": [], + "descriptions": [ + 1, + 2 + ] + } +}, +{ + "model": "admin.logentry", + "pk": 1, + "fields": { + "action_time": "2020-12-29T01:07:01.652Z", + "user": [ + "test_admin" + ], + "content_type": [ + "user", + "user" + ], + "object_id": "3", + "object_repr": "meleager", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 2, + "fields": { + "action_time": "2020-12-29T01:08:36.295Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "description" + ], + "object_id": "1", + "object_repr": "WORK DESC FR [Lang: fra]", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 3, + "fields": { + "action_time": "2020-12-29T01:08:54.672Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "description" + ], + "object_id": "2", + "object_repr": "WORK DESC EN [Lang: eng]", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 4, + "fields": { + "action_time": "2020-12-29T01:08:57.239Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "work" + ], + "object_id": "1", + "object_repr": "WORK DESC FR", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 5, + "fields": { + "action_time": "2020-12-29T01:09:42.035Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "description" + ], + "object_id": "3", + "object_repr": "BOOK DESC FR [Lang: fra]", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 6, + "fields": { + "action_time": "2020-12-29T01:09:46.567Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "book" + ], + "object_id": "1", + "object_repr": "Book 42", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 7, + "fields": { + "action_time": "2020-12-29T01:10:17.872Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "description" + ], + "object_id": "4", + "object_repr": "PASSAGE DESC FR [Lang: fra]", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 8, + "fields": { + "action_time": "2020-12-29T01:11:09.284Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "name" + ], + "object_id": "1", + "object_repr": "AUTHOR FR (Lang: fra)", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 9, + "fields": { + "action_time": "2020-12-29T01:11:33.053Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "description" + ], + "object_id": "5", + "object_repr": "AUTHOR DESC FR [Lang: fra]", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 10, + "fields": { + "action_time": "2020-12-29T01:14:06.834Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "author" + ], + "object_id": "1", + "object_repr": "AUTHOR FR (1)", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 11, + "fields": { + "action_time": "2020-12-29T01:14:10.148Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "passage" + ], + "object_id": "1", + "object_repr": "Passage 42.12", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 12, + "fields": { + "action_time": "2020-12-29T01:14:31.005Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "author" + ], + "object_id": "1", + "object_repr": "AUTHOR FR (1)", + "action_flag": 2, + "change_message": "[{\"changed\": {\"fields\": [\"Born range year date\", \"Died range year date\"]}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 13, + "fields": { + "action_time": "2020-12-29T01:15:23.152Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "description" + ], + "object_id": "6", + "object_repr": "CITY DESC EN [Lang: eng]", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 14, + "fields": { + "action_time": "2020-12-29T01:16:13.652Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "name" + ], + "object_id": "2", + "object_repr": "CITY SPA (Lang: spa)", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 15, + "fields": { + "action_time": "2020-12-29T01:16:21.653Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "city" + ], + "object_id": "1", + "object_repr": "CITY SPA", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 16, + "fields": { + "action_time": "2020-12-29T01:17:54.345Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "description" + ], + "object_id": "7", + "object_repr": "PASSAGE DESC POR [Lang: por]", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 17, + "fields": { + "action_time": "2020-12-29T01:33:34.786Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "passage" + ], + "object_id": "2", + "object_repr": "Passage 42.69abc", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 18, + "fields": { + "action_time": "2020-12-29T02:23:49.377Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "description" + ], + "object_id": "8", + "object_repr": "COMMENT DESC FR [Lang: fra]", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 19, + "fields": { + "action_time": "2020-12-29T02:23:54.373Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "comment" + ], + "object_id": "1", + "object_repr": "Comment COMMENT DESC FR", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 20, + "fields": { + "action_time": "2021-01-23T00:26:44.868Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "description" + ], + "object_id": "9", + "object_repr": "Scholium description [Lang: eng]", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 21, + "fields": { + "action_time": "2021-01-23T00:28:09.474Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "scholium" + ], + "object_id": "1", + "object_repr": "Scholium 42.12.666", + "action_flag": 1, + "change_message": "[{\"added\": {}}]" + } +}, +{ + "model": "admin.logentry", + "pk": 22, + "fields": { + "action_time": "2021-01-25T06:21:29.859Z", + "user": [ + "test_admin" + ], + "content_type": [ + "meleager", + "passage" + ], + "object_id": "2", + "object_repr": "Passage 42.69abc", + "action_flag": 2, + "change_message": "[{\"changed\": {\"fields\": [\"Cities\"]}}]" + } +}, +{ + "model": "meleager.book", + "fields": { + "created_at": "2020-12-29T01:09:05Z", + "updated_at": "2020-12-29T01:09:05Z", + "creator": [ + "meleager" + ], + "last_editor": [ + "meleager" + ], + "work": 1, + "number": 42, + "descriptions": [ + 3 + ] + } +}, +{ + "model": "meleager.passage", + "fields": { + "created_at": "2020-12-29T01:14:10.123Z", + "updated_at": "2020-12-29T01:14:10.123Z", + "creator": [ + "meleager" + ], + "last_editor": [ + "meleager" + ], + "urn": null, + "validation": 0, + "book": [ + 42, + 1 + ], + "fragment": 12, + "sub_fragment": "", + "descriptions": [ + 4 + ], + "alternative_urns": [], + "authors": [ + 1 + ], + "cities": [], + "images": [], + "comments": [], + "alternative_uris": [], + "external_references": [], + "internal_references": [], + "manuscripts": [], + "keywords": [] + } +}, +{ + "model": "meleager.passage", + "fields": { + "created_at": "2020-12-29T01:33:34.720Z", + "updated_at": "2020-12-29T01:33:34.720Z", + "creator": [ + "meleager" + ], + "last_editor": [ + "meleager" + ], + "urn": null, + "validation": 0, + "book": [ + 42, + 1 + ], + "fragment": 69, + "sub_fragment": "abc", + "descriptions": [ + 7 + ], + "alternative_urns": [], + "authors": [ + 1 + ], + "cities": [ + 1 + ], + "images": [], + "comments": [], + "alternative_uris": [], + "external_references": [], + "internal_references": [], + "manuscripts": [], + "keywords": [] + } +}, +{ + "model": "meleager.scholium", + "pk": 1, + "fields": { + "created_at": "2021-01-23T00:26:04Z", + "updated_at": "2021-01-23T00:26:04Z", + "creator": [ + "meleager" + ], + "last_editor": [ + "meleager" + ], + "urn": null, + "validation": 0, + "number": 666, + "passage": [ + 12, + "", + 42, + 1 + ], + "city": null, + "descriptions": [ + 9 + ], + "alternative_urns": [], + "manuscripts": [], + "images": [], + "comments": [], + "keywords": [] + } +} +] diff --git a/django/meleager/management/commands/ap_api_manager/managers/books.py b/django/meleager/management/commands/ap_api_manager/managers/books.py index 083a57aeea164651b5d51ba1034463f31c318e54..f7b15a030a00de7de5aeb397577d80be3c28165c 100644 --- a/django/meleager/management/commands/ap_api_manager/managers/books.py +++ b/django/meleager/management/commands/ap_api_manager/managers/books.py @@ -7,12 +7,14 @@ class BookFromApi: def __new__(cls, work, number): if (work, number) not in cls._books: # Fetch the Book object, create it if it does not exist - let's consider numbers are kind of unique - book_obj = Book.objects.filter( + books = Book.objects.filter( work__descriptions__description=work, number=number ) - if not book_obj: + if not books.count(): work_obj_pk = Work.objects.get(descriptions__description=work).pk book_obj = Book.objects.create(work_id=work_obj_pk, number=number) + else: + book_obj = books.first() cls._books[(work, number)] = book_obj else: diff --git a/django/meleager/management/commands/ap_api_manager/managers/passages.py b/django/meleager/management/commands/ap_api_manager/managers/passages.py index 2b0ebbb050e0b3a717de73f616be22499c90fcb3..f01af44e6a571437866f1cfd6c3cc60b440768e7 100644 --- a/django/meleager/management/commands/ap_api_manager/managers/passages.py +++ b/django/meleager/management/commands/ap_api_manager/managers/passages.py @@ -84,18 +84,33 @@ class PassageFromApi: # CREATE THE ALIGNMENTS for item in passage_json["alignements"]: + text_1 = None + text_2 = None + + try: + text_1 = TextFromApi.get_by_id(item["source"]) + except KeyError: + logger.info( + "Warning - cannot find text ID %s in alignment for entity ID %s", item["source"], passage_json["id_entity"] + ) + try: + text_2 = TextFromApi.get_by_id(item["target"]) + except KeyError: + logger.info( + "Warning - cannot find text ID %s in alignment for entity ID %s", item["target"], passage_json["id_entity"] + ) + + if text_1 or text_2: align_obj = Alignment.objects.create( - text_1=TextFromApi.get_by_id(item["source"]), - text_2=TextFromApi.get_by_id(item["target"]), + text_1=text_1, text_2=text_2, alignment_data=item["json"], ) BaseManager.update_obj_user_dates_from_json(align_obj, item) - except KeyError: + else: logger.info( - "Alignment skipped - Entity ID %s", passage_json["id_entity"] + "Both texts ID (%s, %s) from alignment do not exist", item["source"], item["target"] ) - continue # CREATE THE IMAGES for image_json in passage_json["images"]: diff --git a/django/meleager/management/commands/ap_api_manager/managers/scholies.py b/django/meleager/management/commands/ap_api_manager/managers/scholies.py index 41df7ab1ead46e861d6e7d24f35ab3bf4ac9bd16..1a34f692b97f8d88fd51c56efce7c22793eec2f4 100644 --- a/django/meleager/management/commands/ap_api_manager/managers/scholies.py +++ b/django/meleager/management/commands/ap_api_manager/managers/scholies.py @@ -7,6 +7,7 @@ from ..helpers import to_date from .data_manager import DataManager from .media import ImageFromApi, ManuscriptFromApi from .users import UserFromApi +from .texts import ScholiumTextFromApi logger = logging.getLogger(__name__) @@ -16,7 +17,7 @@ class ScholiumFromApi: def __new__(cls, id_scholie, passage_pk): if id_scholie not in cls._scholies: scholium_obj = cls._create_scholium(id_scholie, passage_pk) - logger.info("NEW | Scholium (AP API %s)", id_scholie) + logger.info("NEW | Scholium PK %s (AP API %s)", scholium_obj.pk, id_scholie) else: scholium_obj = cls._scholies[id_scholie] @@ -35,6 +36,10 @@ class ScholiumFromApi: updated_at=to_date(json_data["updatedAt"]), ) + for text in json_data["versions"]: + text_obj = ScholiumTextFromApi(text) + scholium_obj.texts.add(text_obj) + for image in json_data["images"]: image_obj = ManuscriptFromApi(key=None, json_data=image, key_name="id_image") scholium_obj.manuscripts.add(image_obj) diff --git a/django/meleager/management/commands/ap_api_manager/managers/texts.py b/django/meleager/management/commands/ap_api_manager/managers/texts.py index 8fe8935bd92f04dd39b036b00c99b7ad055263fb..1a8011a514540fae632e96d306c5ff0a5db01b9f 100644 --- a/django/meleager/management/commands/ap_api_manager/managers/texts.py +++ b/django/meleager/management/commands/ap_api_manager/managers/texts.py @@ -1,22 +1,23 @@ import logging -from meleager.models import Text +from meleager.models import Text, Work, Description, Edition from ..constants import LANG_API2CODE from .editions import EditionFromApi logger = logging.getLogger(__name__) - -class TextFromApi: - _texts = dict() +class AbstractTextFromApi: + # To be overriden by children classes + json_key = None + text_key = None def __new__(cls, json_data, work_obj_pk): - if json_data["id_entity_version"] not in cls._texts: + if json_data[cls.json_key] not in cls._texts: text_obj = cls._create_text(json_data, work_obj_pk) - cls._texts[json_data["id_entity_version"]] = text_obj + cls._texts[json_data[cls.json_key]] = text_obj else: - text_obj = cls._texts[json_data["id_entity_version"]] + text_obj = cls._texts[json_data[cls.json_key]] return text_obj @@ -25,12 +26,12 @@ class TextFromApi: try: return cls._texts[text_id] except KeyError as exc: - logging.warn("!!! Error: cannot find text %s for alignment.", text_id) + logging.warn("!!! Error: cannot find text %s", text_id) raise KeyError from exc @classmethod def _create_text(cls, json_data, work_obj_pk): - text = json_data["text_translated"] + text = json_data[cls.text_key] lang_code = LANG_API2CODE[json_data["id_language"]] edition_obj = EditionFromApi( @@ -44,3 +45,56 @@ class TextFromApi: # TODO: add edition information logger.info("NEW | Text (%s) %s", lang_code.upper(), text[0:25]) return text_obj + +class TextFromApi(AbstractTextFromApi): + json_key = "id_entity_version" + text_key = "text_translated" + # This is very poor (#116) + _texts = dict() + +class ScholiumTextFromApi(AbstractTextFromApi): + json_key = "id_scholie_version" + text_key = "text" + # This is very poor (#116) + _texts = dict() + + def __new__(cls, json_data): + """ We don't use work_obj_pk here because scholia have a set work / edition (see issue #116) """ + return super().__new__(cls, json_data, None) + + @classmethod + def _create_text(cls, json_data, work_obj_pk=None): + """ We don't use work_obj_pk here because scholia have a set work / edition (see issue #116) """ + + text = json_data[cls.text_key] + lang_code = LANG_API2CODE[json_data["id_language"]] + + desc, _ = Description.objects.get_or_create(description="Work Scholia", language_id="eng") + + if not desc.works.count(): + work = Work.objects.create() + work.descriptions.add(desc) + else: + work = desc.works.first() + + if not work.editions.count(): + desc, _ = Description.objects.get_or_create(description="Edition Scholia", language_id="eng") + editions = Edition.objects.filter(descriptions=desc).count() + if not editions: + edition, _ = Edition.objects.get_or_create( + work=work, + edition_type=Edition.EditionType.SCHOLIA, + ) + edition.descriptions.add(desc) + else: + edition = editions.first() + else: + edition = work.editions.first() + + text_obj = Text.objects.create( + text=text, language_id=lang_code, edition=edition + ) + + # TODO: add edition information + logger.info("NEW | Text (%s) %s", lang_code.upper(), text[0:25]) + return text_obj \ No newline at end of file diff --git a/django/meleager/management/commands/ap_api_manager/managers/users.py b/django/meleager/management/commands/ap_api_manager/managers/users.py index ef69e0f545b3b9aa40a26a0de323814ccf5f823b..fc1fd081673e2a36fad10629c0162cf5b2a37bf1 100644 --- a/django/meleager/management/commands/ap_api_manager/managers/users.py +++ b/django/meleager/management/commands/ap_api_manager/managers/users.py @@ -1,6 +1,6 @@ import logging -from user.models import User +from meleager_user.models import User from ..constants import API_URL from .data_manager import DataManager diff --git a/django/meleager/management/commands/check_database.py b/django/meleager/management/commands/check_database.py new file mode 100644 index 0000000000000000000000000000000000000000..9321f0f42576f16fc6a4853dbe30dc62d55dc6a1 --- /dev/null +++ b/django/meleager/management/commands/check_database.py @@ -0,0 +1,37 @@ +from django.core.management.base import BaseCommand +from django.db.models import Q + +from meleager.models import Text, Alignment + +def check_texts(): + texts_associated_twice = Text.objects.exclude(passage=None).exclude(scholium=None) + if not texts_associated_twice.count(): + return True + + print("The following texts are linked to both a Passage and a Scholium:") + for text in texts_associated_twice: + print(text) + + return False + +def check_alignments(): + text_1_or_2_empty = Q(text_1=None) | Q(text_2=None) + incorrect_alignments = Alignment.objects.filter(text_1_or_2_empty) + if not incorrect_alignments: + return True + + for alignment in incorrect_alignments: + print(f"{alignment.pk} - {alignment.text_1} {alignment.text_2}") + + return False + +class Command(BaseCommand): + help = "Checks database integrity" + + def handle(self, *args, **options): + texts_ok = check_texts() + alignments_ok = check_alignments() + + if texts_ok and alignments_ok: + print("All good.") + diff --git a/django/meleager/migrations/0001_initial.py b/django/meleager/migrations/0001_initial.py index f44006c6b32324ceb7bbf3a0bb67c79a467b84a8..531dd50d53f8c3c752b6977b76c7ce48145ed706 100644 --- a/django/meleager/migrations/0001_initial.py +++ b/django/meleager/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.1.3 on 2020-11-28 03:15 +# Generated by Django 3.1.3 on 2021-02-05 22:20 import django.contrib.gis.db.models.fields import django.contrib.postgres.fields.ranges @@ -92,6 +92,14 @@ class Migration(migrations.Migration): 'abstract': False, }, ), + migrations.CreateModel( + name='CommentTextAnchor', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('anchor_word', models.CharField(default=' ', max_length=255)), + ('occurrence', models.IntegerField(default=0)), + ], + ), migrations.CreateModel( name='Description', fields=[ @@ -163,7 +171,11 @@ class Migration(migrations.Migration): fields=[ ('code', models.CharField(max_length=3, primary_key=True, serialize=False, verbose_name='Language code (ISO639)')), ('iso_name', models.CharField(max_length=100, verbose_name='Language name')), + ('preferred', models.BooleanField(db_index=True, default=False, verbose_name='Favorite language')), ], + options={ + 'ordering': ('-preferred', 'code'), + }, ), migrations.CreateModel( name='Manuscript', @@ -215,7 +227,7 @@ class Migration(migrations.Migration): ('sub_fragment', models.CharField(blank=True, db_index=True, default='', help_text='The sub fragment number in book.', max_length=3)), ], options={ - 'abstract': False, + 'permissions': [('manage_own_passage', 'Can manage (edit/delete) their own Passage items')], }, ), migrations.CreateModel( @@ -273,7 +285,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created_at', models.DateTimeField(default=django.utils.timezone.now)), ('updated_at', models.DateTimeField(default=django.utils.timezone.now)), - ('alternative_urns', models.ManyToManyField(help_text='The alternative URNs of this resource.', related_name='meleager_work_alternative_urns', related_query_name='meleager_works_alternative_urns', to='meleager.URN')), + ('alternative_urns', models.ManyToManyField(blank=True, help_text='The alternative URNs of this resource.', related_name='meleager_work_alternative_urns', related_query_name='meleager_works_alternative_urns', to='meleager.URN')), ], options={ 'abstract': False, diff --git a/django/meleager/migrations/0002_auto_20201128_0315.py b/django/meleager/migrations/0002_auto_20210205_2220.py similarity index 87% rename from django/meleager/migrations/0002_auto_20201128_0315.py rename to django/meleager/migrations/0002_auto_20210205_2220.py index 3cba8e8b71e800e2b60a2261eb482d402b338b6d..03d57019a8ff175afa240a108f487e5c1f109e8c 100644 --- a/django/meleager/migrations/0002_auto_20201128_0315.py +++ b/django/meleager/migrations/0002_auto_20210205_2220.py @@ -1,4 +1,4 @@ -# Generated by Django 3.1.3 on 2020-11-28 03:15 +# Generated by Django 3.1.3 on 2021-02-05 22:20 from django.conf import settings from django.db import migrations, models @@ -23,18 +23,13 @@ class Migration(migrations.Migration): migrations.AddField( model_name='work', name='descriptions', - field=models.ManyToManyField(help_text='The multilingual descriptions for this object.', related_name='meleager_work', related_query_name='meleager_works', to='meleager.Description'), + field=models.ManyToManyField(help_text='Descriptions for the work.', related_name='works', to='meleager.Description'), ), migrations.AddField( model_name='work', name='last_editor', field=models.ForeignKey(help_text='The last editor of this resource.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='meleager_work_last_editor', related_query_name='meleager_works_last_editor', to=settings.AUTH_USER_MODEL), ), - migrations.AddField( - model_name='work', - name='names', - field=models.ManyToManyField(help_text='The multilingual names for this object.', related_name='mlgr_works', to='meleager.Name'), - ), migrations.AddField( model_name='work', name='urn', @@ -53,7 +48,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='urn', name='keywords', - field=models.ManyToManyField(help_text='The keywords that tag this resource.', related_name='mlgr_urns', to='meleager.Keyword'), + field=models.ManyToManyField(help_text='The keywords that tag this resource.', related_name='urns', to='meleager.Keyword'), ), migrations.AddField( model_name='urn', @@ -63,7 +58,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='text', name='comments', - field=models.ManyToManyField(help_text='The comments of this resource.', related_name='texts', to='meleager.Comment'), + field=models.ManyToManyField(help_text='The comments of this resource.', related_name='texts', through='meleager.CommentTextAnchor', to='meleager.Comment'), ), migrations.AddField( model_name='text', @@ -98,17 +93,17 @@ class Migration(migrations.Migration): migrations.AddField( model_name='scholium', name='alternative_urns', - field=models.ManyToManyField(help_text='The alternative URNs of this resource.', related_name='meleager_scholium_alternative_urns', related_query_name='meleager_scholiums_alternative_urns', to='meleager.URN'), + field=models.ManyToManyField(blank=True, help_text='The alternative URNs of this resource.', related_name='meleager_scholium_alternative_urns', related_query_name='meleager_scholiums_alternative_urns', to='meleager.URN'), ), migrations.AddField( model_name='scholium', name='city', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mlgr_scholia', to='meleager.city'), + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='scholia', to='meleager.city'), ), migrations.AddField( model_name='scholium', name='comments', - field=models.ManyToManyField(help_text='The comments of this resource.', related_name='mlgr_scholia', to='meleager.Comment'), + field=models.ManyToManyField(blank=True, help_text='The comments of this resource.', related_name='scholia', to='meleager.Comment'), ), migrations.AddField( model_name='scholium', @@ -123,12 +118,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='scholium', name='images', - field=models.ManyToManyField(help_text='The images associated to this resource.', related_name='mlgr_scholia', to='meleager.Medium'), + field=models.ManyToManyField(blank=True, help_text='The images associated to this resource.', related_name='scholia', to='meleager.Medium'), ), migrations.AddField( model_name='scholium', name='keywords', - field=models.ManyToManyField(help_text='The keywords that tag this resource.', related_name='mlgr_scholia', to='meleager.Keyword'), + field=models.ManyToManyField(blank=True, help_text='The keywords that tag this resource.', related_name='scholia', to='meleager.Keyword'), ), migrations.AddField( model_name='scholium', @@ -138,12 +133,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='scholium', name='manuscripts', - field=models.ManyToManyField(help_text='The image of the manuscript for this passage.', to='meleager.Manuscript'), + field=models.ManyToManyField(blank=True, help_text='The image of the manuscript for this passage.', to='meleager.Manuscript'), ), migrations.AddField( model_name='scholium', name='passage', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='mlgr_scholia', to='meleager.passage'), + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scholia', to='meleager.passage'), ), migrations.AddField( model_name='scholium', @@ -158,7 +153,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='passage', name='alternative_urns', - field=models.ManyToManyField(help_text='The alternative URNs of this resource.', related_name='meleager_passage_alternative_urns', related_query_name='meleager_passages_alternative_urns', to='meleager.URN'), + field=models.ManyToManyField(blank=True, help_text='The alternative URNs of this resource.', related_name='meleager_passage_alternative_urns', related_query_name='meleager_passages_alternative_urns', to='meleager.URN'), ), migrations.AddField( model_name='passage', @@ -172,13 +167,13 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='passage', - name='city', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='passages', to='meleager.city'), + name='cities', + field=models.ManyToManyField(related_name='passages', to='meleager.City'), ), migrations.AddField( model_name='passage', name='comments', - field=models.ManyToManyField(help_text='The comments of this resource.', related_name='mlgr_passages', to='meleager.Comment'), + field=models.ManyToManyField(help_text='The comments of this resource.', related_name='passages', to='meleager.Comment'), ), migrations.AddField( model_name='passage', @@ -193,12 +188,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='passage', name='external_references', - field=models.ManyToManyField(related_name='mlgr_passages', to='meleager.ExternalReference'), + field=models.ManyToManyField(related_name='passages', to='meleager.ExternalReference'), ), migrations.AddField( model_name='passage', name='images', - field=models.ManyToManyField(help_text='The images associated to this resource.', related_name='mlgr_passages', to='meleager.Medium'), + field=models.ManyToManyField(help_text='The images associated to this resource.', related_name='passages', to='meleager.Medium'), ), migrations.AddField( model_name='passage', @@ -208,7 +203,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='passage', name='keywords', - field=models.ManyToManyField(help_text='The keywords that tag this resource.', related_name='mlgr_passages', to='meleager.Keyword'), + field=models.ManyToManyField(blank=True, help_text='The keywords that tag this resource.', related_name='passages', to='meleager.Keyword'), ), migrations.AddField( model_name='passage', @@ -218,7 +213,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='passage', name='manuscripts', - field=models.ManyToManyField(help_text='The image of the manuscript for this passage.', related_name='mlgr_passages', to='meleager.Manuscript'), + field=models.ManyToManyField(help_text='The image of the manuscript for this passage.', related_name='passages', to='meleager.Manuscript'), ), migrations.AddField( model_name='passage', @@ -253,7 +248,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='medium', name='keywords', - field=models.ManyToManyField(help_text='The keywords that tag this resource.', related_name='mlgr_media', to='meleager.Keyword'), + field=models.ManyToManyField(help_text='The keywords that tag this resource.', related_name='media', to='meleager.Keyword'), ), migrations.AddField( model_name='medium', @@ -273,7 +268,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='manuscript', name='keywords', - field=models.ManyToManyField(help_text='The keywords that tag this resource.', related_name='mlgr_manuscripts', to='meleager.Keyword'), + field=models.ManyToManyField(help_text='The keywords that tag this resource.', related_name='manuscripts', to='meleager.Keyword'), ), migrations.AddField( model_name='manuscript', @@ -298,12 +293,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='keywordcategory', name='names', - field=models.ManyToManyField(help_text='The multilingual names for this object.', related_name='mlgr_keyword_categories', to='meleager.Name'), + field=models.ManyToManyField(help_text='The multilingual names for this object.', related_name='keyword_categories', to='meleager.Name'), ), migrations.AddField( model_name='keyword', name='category', - field=models.ForeignKey(help_text='The category this keyword belongs to.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='meleager.keywordcategory'), + field=models.ForeignKey(help_text='The category this keyword belongs to.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='keywords', to='meleager.keywordcategory'), ), migrations.AddField( model_name='keyword', @@ -323,7 +318,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='keyword', name='names', - field=models.ManyToManyField(help_text='The multilingual names for this object.', related_name='mlgr_keywords', to='meleager.Name'), + field=models.ManyToManyField(help_text='The multilingual names for this object.', related_name='keywords', to='meleager.Name'), ), migrations.AddField( model_name='editor', @@ -363,7 +358,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='edition', name='work', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='meleager.work'), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='editions', to='meleager.work'), ), migrations.AddField( model_name='description', @@ -380,6 +375,16 @@ class Migration(migrations.Migration): name='last_editor', field=models.ForeignKey(help_text='The last editor of this resource.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='meleager_description_last_editor', related_query_name='meleager_descriptions_last_editor', to=settings.AUTH_USER_MODEL), ), + migrations.AddField( + model_name='commenttextanchor', + name='comment', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='meleager.comment'), + ), + migrations.AddField( + model_name='commenttextanchor', + name='text', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='meleager.text'), + ), migrations.AddField( model_name='comment', name='creator', @@ -393,7 +398,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='comment', name='images', - field=models.ManyToManyField(help_text='The images associated to this resource.', related_name='mlgr_comments', to='meleager.Medium'), + field=models.ManyToManyField(blank=True, help_text='The images associated to this resource.', related_name='comments', to='meleager.Medium'), ), migrations.AddField( model_name='comment', @@ -418,7 +423,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='city', name='names', - field=models.ManyToManyField(help_text='The multilingual names for this object.', related_name='mlgr_cities', to='meleager.Name'), + field=models.ManyToManyField(help_text='The multilingual names for this object.', related_name='cities', to='meleager.Name'), ), migrations.AddField( model_name='book', @@ -443,17 +448,17 @@ class Migration(migrations.Migration): migrations.AddField( model_name='author', name='alternative_urns', - field=models.ManyToManyField(help_text='The alternative URNs of this resource.', related_name='meleager_author_alternative_urns', related_query_name='meleager_authors_alternative_urns', to='meleager.URN'), + field=models.ManyToManyField(blank=True, help_text='The alternative URNs of this resource.', related_name='meleager_author_alternative_urns', related_query_name='meleager_authors_alternative_urns', to='meleager.URN'), ), migrations.AddField( model_name='author', name='city_born', - field=models.ForeignKey(blank=True, help_text='The city where this author was born.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='mlgr_authors_born', related_query_name='meleager_authors_city_born', to='meleager.city'), + field=models.ForeignKey(blank=True, help_text='The city where this author was born.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='authors_born_at', related_query_name='meleager_authors_city_born', to='meleager.city'), ), migrations.AddField( model_name='author', name='city_died', - field=models.ForeignKey(blank=True, help_text='The city where this author died.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='mlgr_authors_died', to='meleager.city'), + field=models.ForeignKey(blank=True, help_text='The city where this author died.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='authors_died_at', to='meleager.city'), ), migrations.AddField( model_name='author', @@ -468,7 +473,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='author', name='images', - field=models.ManyToManyField(help_text='The images associated to this resource.', related_name='mlgr_authors', to='meleager.Medium'), + field=models.ManyToManyField(blank=True, help_text='The images associated to this resource.', related_name='authors', to='meleager.Medium'), ), migrations.AddField( model_name='author', @@ -478,7 +483,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='author', name='names', - field=models.ManyToManyField(help_text='The multilingual names for this object.', related_name='mlgr_authors', to='meleager.Name'), + field=models.ManyToManyField(help_text='The multilingual names for this object.', related_name='authors', to='meleager.Name'), ), migrations.AddField( model_name='author', diff --git a/django/meleager/migrations/0003_auto_20201216_0308.py b/django/meleager/migrations/0003_auto_20201216_0308.py deleted file mode 100644 index 4c92f7c83e54030c8e3b9536660b0470d7b5a920..0000000000000000000000000000000000000000 --- a/django/meleager/migrations/0003_auto_20201216_0308.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 3.1.3 on 2020-12-16 03:08 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('meleager', '0002_auto_20201128_0315'), - ] - - operations = [ - migrations.RemoveField( - model_name='work', - name='names', - ), - migrations.AddField( - model_name='language', - name='preferred', - field=models.BooleanField(db_index=True, default=False, verbose_name='Favorite language'), - ), - migrations.AlterField( - model_name='work', - name='descriptions', - field=models.ManyToManyField(help_text='Descriptions for the work.', related_name='mlgr_works', to='meleager.Description'), - ), - ] diff --git a/django/meleager/migrations/0004_auto_20201216_2034.py b/django/meleager/migrations/0004_auto_20201216_2034.py deleted file mode 100644 index 61dd34799c13f9e2369707432925b2b4df875e49..0000000000000000000000000000000000000000 --- a/django/meleager/migrations/0004_auto_20201216_2034.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.1.3 on 2020-12-16 20:34 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('meleager', '0003_auto_20201216_0308'), - ] - - operations = [ - migrations.AlterField( - model_name='keyword', - name='category', - field=models.ForeignKey(help_text='The category this keyword belongs to.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='keywords', to='meleager.keywordcategory'), - ), - ] diff --git a/django/meleager/models/author.py b/django/meleager/models/author.py index a5d7aff43f4538b9cc6c4583f829d515bc929718..96facb873ec8943308cf85458bcef8b9c338995a 100644 --- a/django/meleager/models/author.py +++ b/django/meleager/models/author.py @@ -14,7 +14,7 @@ class Author( ): names = models.ManyToManyField( "meleager.Name", - related_name="mlgr_authors", + related_name="authors", help_text="The multilingual names for this object.", ) @@ -23,7 +23,7 @@ class Author( on_delete=models.PROTECT, null=True, blank=True, - related_name="mlgr_authors_born", + related_name="authors_born_at", related_query_name="%(app_label)s_%(class)ss_city_born", help_text="The city where this author was born.", ) @@ -37,14 +37,15 @@ class Author( on_delete=models.PROTECT, null=True, blank=True, - related_name="mlgr_authors_died", + related_name="authors_died_at", help_text="The city where this author died.", ) images = models.ManyToManyField( "meleager.Medium", - related_name="mlgr_authors", + related_name="authors", help_text="The images associated to this resource.", + blank=True, ) died_range_year_date = IntegerRangeField( diff --git a/django/meleager/models/city.py b/django/meleager/models/city.py index d11143d9d3ab6f5a4143ac7f7bf3a7069b886a0d..cba1322ffdd431c09d3acf7e1ad0312030047110 100644 --- a/django/meleager/models/city.py +++ b/django/meleager/models/city.py @@ -7,7 +7,7 @@ from .mixins import DescriptableResourceMixin, EditableResourceMixin class City(EditableResourceMixin, DescriptableResourceMixin, models.Model): names = models.ManyToManyField( "meleager.Name", - related_name="mlgr_cities", + related_name="cities", help_text="The multilingual names for this object.", ) diff --git a/django/meleager/models/comment.py b/django/meleager/models/comment.py index 47ccb1dde2f96f3faab2701611e913bcbb9175fe..a9a5d4d5c35d90b4d6976fd559cafb426cf8de6c 100644 --- a/django/meleager/models/comment.py +++ b/django/meleager/models/comment.py @@ -10,8 +10,9 @@ class CommentType(models.TextChoices): class Comment(EditableResourceMixin, DescriptableResourceMixin, models.Model): images = models.ManyToManyField( "meleager.Medium", - related_name="mlgr_comments", + related_name="comments", help_text="The images associated to this resource.", + blank=True, ) comment_type = models.CharField(max_length=20, choices=CommentType.choices, default=CommentType.USER_NOTE, null=False, db_index=True) comment_title = models.TextField() diff --git a/django/meleager/models/edition.py b/django/meleager/models/edition.py index 46e2d2640bbf5bc228f6d3257853f3bd6e84622c..8e382f37d8f65c2c0ee1940b1ee06c457c9e1cd1 100644 --- a/django/meleager/models/edition.py +++ b/django/meleager/models/edition.py @@ -11,7 +11,7 @@ class Edition(EditableResourceMixin, DescriptableResourceMixin, models.Model): SCHOLIA = 3 ALIGNMENT = 4 - work = models.ForeignKey("meleager.Work", on_delete=models.CASCADE) + work = models.ForeignKey("meleager.Work", on_delete=models.CASCADE, related_name="editions") edition_type = models.IntegerField(choices=EditionType.choices) metadata = models.JSONField("Metadata for Edition", null=False, default=dict) diff --git a/django/meleager/models/keyword.py b/django/meleager/models/keyword.py index 775825b98ae69f29a41610cd9a0ec042c74451fa..aebbbcdf12a4d958e317c9c36e6545fe1d29c3f0 100644 --- a/django/meleager/models/keyword.py +++ b/django/meleager/models/keyword.py @@ -6,7 +6,7 @@ from .mixins import DescriptableResourceMixin, EditableResourceMixin class KeywordCategory(EditableResourceMixin, DescriptableResourceMixin, models.Model): names = models.ManyToManyField( "meleager.Name", - related_name="mlgr_keyword_categories", + related_name="keyword_categories", help_text="The multilingual names for this object.", ) @@ -23,7 +23,7 @@ class KeywordCategory(EditableResourceMixin, DescriptableResourceMixin, models.M class Keyword(EditableResourceMixin, DescriptableResourceMixin, models.Model): names = models.ManyToManyField( "meleager.Name", - related_name="mlgr_keywords", + related_name="keywords", help_text="The multilingual names for this object.", ) diff --git a/django/meleager/models/medium.py b/django/meleager/models/medium.py index 11297a4df9a80165e19f377f27f71df3f045565e..89e009bc62beb9b0f9399e886db916b2d9e6f93a 100644 --- a/django/meleager/models/medium.py +++ b/django/meleager/models/medium.py @@ -13,7 +13,7 @@ class MediaTypes(models.TextChoices): class Medium(EditableResourceMixin, DescriptableResourceMixin, models.Model): keywords = models.ManyToManyField( "meleager.Keyword", - related_name="mlgr_media", + related_name="media", help_text="The keywords that tag this resource.", ) type = models.CharField( @@ -31,7 +31,7 @@ class Manuscript(EditableResourceMixin, DescriptableResourceMixin, models.Model) credit = models.CharField(max_length=255, null=False, blank=True, default="") keywords = models.ManyToManyField( "meleager.Keyword", - related_name="mlgr_manuscripts", + related_name="manuscripts", help_text="The keywords that tag this resource.", ) diff --git a/django/meleager/models/mixins/urn_mixin.py b/django/meleager/models/mixins/urn_mixin.py index 2c991aa891ea9d6ff129f5da59197c46e6871026..0af37576eb4b8435b80969bfeb3a5b9f24d1216a 100644 --- a/django/meleager/models/mixins/urn_mixin.py +++ b/django/meleager/models/mixins/urn_mixin.py @@ -21,6 +21,7 @@ class AlternativeURNResourceMixin(models.Model): related_name="%(app_label)s_%(class)s_alternative_urns", related_query_name="%(app_label)s_%(class)ss_alternative_urns", help_text="The alternative URNs of this resource.", + blank=True, ) class Meta: diff --git a/django/meleager/models/passage.py b/django/meleager/models/passage.py index 3c15e4a45e8eb1e0b8065fd1070769b7755712bb..11565f2f2cca980ff73f62e625ad66621d8c503f 100644 --- a/django/meleager/models/passage.py +++ b/django/meleager/models/passage.py @@ -1,5 +1,6 @@ import re from urllib.parse import urlparse +from pathlib import Path from django.apps import apps from django.conf import settings @@ -49,12 +50,9 @@ class Passage( objects = PassageManager() authors = models.ManyToManyField("meleager.Author") - city = models.ForeignKey( + cities = models.ManyToManyField( "meleager.City", related_name="passages", - on_delete=models.SET_NULL, - null=True, - blank=True, ) book = models.ForeignKey( @@ -80,35 +78,51 @@ class Passage( images = models.ManyToManyField( "meleager.Medium", - related_name="mlgr_passages", + related_name="passages", help_text="The images associated to this resource.", ) comments = models.ManyToManyField( "meleager.Comment", - related_name="mlgr_passages", + related_name="passages", help_text="The comments of this resource.", ) alternative_uris = models.ManyToManyField("meleager.AlternativeUri") - external_references = models.ManyToManyField("meleager.ExternalReference", related_name="mlgr_passages") + external_references = models.ManyToManyField("meleager.ExternalReference", related_name="passages") internal_references = models.ManyToManyField("self", symmetrical=True) manuscripts = models.ManyToManyField( "meleager.Manuscript", help_text="The image of the manuscript for this passage.", - related_name="mlgr_passages", + related_name="passages", ) keywords = models.ManyToManyField( "meleager.Keyword", - related_name="mlgr_passages", + related_name="passages", help_text="The keywords that tag this resource.", + blank=True, ) + def get_absolute_url(self): + return reverse( + "web:passage-detail", + args=(self.book.number, self.fragment, self.sub_fragment), + ) + + @property + def urn_value(self): + return Path(urlparse(self.get_absolute_url()).path).name + def natural_key(self): return (self.fragment, self.sub_fragment) + self.book.natural_key() - natural_key.dependencies = ['meleager.Work', 'meleager.Book'] + natural_key.dependencies = ['meleager.Work', 'meleager.Book', 'meleager.Author'] def __str__(self): return f"Passage {self.book.number}.{self.fragment}{self.sub_fragment}" + + class Meta: + permissions = [ + ("manage_own_passage", "Can manage (edit/delete) their own Passage items") + ] \ No newline at end of file diff --git a/django/meleager/models/scholium.py b/django/meleager/models/scholium.py index 3e8763d256e5519dd65ab69f636708afbcbd68a5..167e55cea9ad2eb48aa1355356b45c11a7ce7018 100644 --- a/django/meleager/models/scholium.py +++ b/django/meleager/models/scholium.py @@ -1,4 +1,5 @@ from django.db import models +from django.urls import reverse from .mixins import (AlternativeURNResourceMixin, DescriptableResourceMixin, EditableResourceMixin, ValidableResourceMixin) @@ -15,39 +16,47 @@ class Scholium( passage = models.ForeignKey( "meleager.Passage", - related_name="mlgr_scholia", + related_name="scholia", on_delete=models.CASCADE, null=True, ) city = models.ForeignKey( "meleager.City", - related_name="mlgr_scholia", + related_name="scholia", on_delete=models.SET_NULL, - null=True, + null=True, blank=True, ) manuscripts = models.ManyToManyField( - "meleager.Manuscript", help_text="The image of the manuscript for this passage." + "meleager.Manuscript", help_text="The image of the manuscript for this passage.", blank=True, ) images = models.ManyToManyField( "meleager.Medium", - related_name="mlgr_scholia", - help_text="The images associated to this resource.", + related_name="scholia", + help_text="The images associated to this resource.", blank=True, ) comments = models.ManyToManyField( "meleager.Comment", - related_name="mlgr_scholia", - help_text="The comments of this resource.", + related_name="scholia", + help_text="The comments of this resource.", blank=True, ) keywords = models.ManyToManyField( "meleager.Keyword", - related_name="mlgr_scholia", - help_text="The keywords that tag this resource.", + related_name="scholia", + help_text="The keywords that tag this resource.", blank=True, ) + def get_absolute_url(self): + return reverse( + "web:scholium", + self.passage.book.number, + self.passage.fragment, + self.passage.sub_fragment, + ) + def __str__(self): return f"Scholium {self.passage.book.number}.{self.passage.fragment}{self.passage.sub_fragment}.{self.number}" diff --git a/django/meleager/models/text.py b/django/meleager/models/text.py index 512ea3bd6320792a33918f7e4516d584540d919c..f96b6fbffb2568a465c0240721806dc0a7b85e74 100644 --- a/django/meleager/models/text.py +++ b/django/meleager/models/text.py @@ -4,6 +4,11 @@ from django.template.defaultfilters import truncatewords from .helpers import Language from .mixins import EditableResourceMixin, ValidableResourceMixin +class CommentTextAnchor(models.Model): + text = models.ForeignKey('meleager.Text', on_delete=models.CASCADE, null=False) + comment = models.ForeignKey('meleager.Comment', on_delete=models.CASCADE, null=False) + anchor_word = models.CharField(max_length=255, default=" ", null=False) + occurrence = models.IntegerField(default=0) class Text(EditableResourceMixin, ValidableResourceMixin, models.Model): class Status(models.IntegerChoices): @@ -43,7 +48,9 @@ class Text(EditableResourceMixin, ValidableResourceMixin, models.Model): "meleager.Comment", related_name="texts", help_text="The comments of this resource.", + through=CommentTextAnchor, ) def __str__(self): return f"Text ({truncatewords(self.text, 5)})" + diff --git a/django/meleager/models/urn.py b/django/meleager/models/urn.py index 1b6d1ffe0a89b6ba24d7af853886bd0822f960f3..98bc39b3e8eb313334ad65714f4faadb8154b9ef 100644 --- a/django/meleager/models/urn.py +++ b/django/meleager/models/urn.py @@ -6,7 +6,7 @@ from .mixins import DescriptableResourceMixin, EditableResourceMixin class URN(EditableResourceMixin, DescriptableResourceMixin, models.Model): keywords = models.ManyToManyField( "meleager.Keyword", - related_name="mlgr_urns", + related_name="urns", help_text="The keywords that tag this resource.", ) # example of perseus URN: urn:cts:greekLit:tlg7000.tlg001.perseus-grc2:7.281 diff --git a/django/meleager/models/work.py b/django/meleager/models/work.py index ef38a6678df3ed49bf0c4cf3f376d1d5f321e957..7a264f605bc272f8eed89ae743f9c51b376de028 100644 --- a/django/meleager/models/work.py +++ b/django/meleager/models/work.py @@ -13,7 +13,7 @@ class Work( descriptions = models.ManyToManyField( "meleager.Description", - related_name="mlgr_works", + related_name="works", help_text="Descriptions for the work.", ) diff --git a/django/meleager/schema.py b/django/meleager/schema.py new file mode 100644 index 0000000000000000000000000000000000000000..0db9d4e4a539e04af7d86729771b926e955c4feb --- /dev/null +++ b/django/meleager/schema.py @@ -0,0 +1,63 @@ +import graphene +from graphene import relay +from graphene_django import DjangoObjectType, DjangoListField, DjangoConnectionField +from graphene_django.filter import DjangoFilterConnectionField + +from meleager.models import Book, Passage, Scholium, City, Name, Author, Work, Description, Language + +class LanguageNode(DjangoObjectType): + class Meta: + model = Language + +class WorkNode(DjangoObjectType): + class Meta: + model = Work + +class DescriptionNode(DjangoObjectType): + class Meta: + model = Description + +class BookNode(DjangoObjectType): + class Meta: + model = Book + # filter_fields = ["id", "number"] + # interfaces = [relay.Node] + +class ScholiumNode(DjangoObjectType): + class Meta: + model = Scholium + # filter_fields = ["id", "number", "passage"] + # interfaces = [relay.Node] + +class PassageNode(DjangoObjectType): + class Meta: + model = Passage + # filter_fields = { + # "id": ["exact"], + # "fragment": ["exact"], + # "sub_fragment": ["exact"], + # "mlgr_scholia": ["exact"], + # } + # interfaces = [relay.Node] + +class CityNode(DjangoObjectType): + class Meta: + model = City + exclude = ["location"] + +class NameNode(DjangoObjectType): + class Meta: + model = Name + +class AuthorNode(DjangoObjectType): + class Meta: + model = Author + +class Query(graphene.ObjectType): + scholia = DjangoListField(ScholiumNode) + passages = DjangoListField(PassageNode) + names = DjangoListField(NameNode) + cities = DjangoListField(CityNode) + authors = DjangoListField(AuthorNode) + +schema = graphene.Schema(query=Query) \ No newline at end of file diff --git a/django/meleager/serializers/comment.py b/django/meleager/serializers/comment.py index e4643c9da03e5a63ac5c624fd8c0437306e1d124..9e95840ec83c9c728400f68f00eba943fef47796 100644 --- a/django/meleager/serializers/comment.py +++ b/django/meleager/serializers/comment.py @@ -9,5 +9,5 @@ class CommentSerializer(serializers.HyperlinkedModelSerializer): fields = "__all__" passages = serializers.HyperlinkedRelatedField( - many=True, read_only=True, source="mlgr_passages", view_name="passage-detail" + many=True, read_only=True, source="passages", view_name="passage-detail" ) diff --git a/django/meleager/serializers/passage.py b/django/meleager/serializers/passage.py index 13ab77021f43ee2ee0f27e12cdd2342c8db84847..120fe1755007a32a85bee53ee1db9c85c56423b3 100644 --- a/django/meleager/serializers/passage.py +++ b/django/meleager/serializers/passage.py @@ -12,6 +12,10 @@ class PassageSerializer(serializers.HyperlinkedModelSerializer): many=True, read_only=True, view_name="text-detail" ) keywords = MinimalLinkedKeywordSerializer(many=True) + scholia = serializers.HyperlinkedRelatedField( + source="scholia", + many=True, read_only=True, view_name="scholium-detail", + ) class Meta: model = Passage diff --git a/django/meleager/serializers/work.py b/django/meleager/serializers/work.py index 5936f4e833b087aa57b638e4f514e9d6113e7d90..a213de6ea271ca6440d5a789db8c0b2511f5de86 100644 --- a/django/meleager/serializers/work.py +++ b/django/meleager/serializers/work.py @@ -2,11 +2,11 @@ from rest_framework import serializers from meleager.models import Work -from ..serializers.name import NameInlineSerializer +from .description import DescriptionSerializer class WorkSerializer(serializers.HyperlinkedModelSerializer): - names = NameInlineSerializer(many=True) + descriptions = DescriptionSerializer(many=True) class Meta: model = Work diff --git a/django/meleager/viewsets/passage.py b/django/meleager/viewsets/passage.py index 07f453aee4455ea56061960c23409cb57f5d568b..7e8f213c61d1b7061e0d40db61260230d7d05e34 100644 --- a/django/meleager/viewsets/passage.py +++ b/django/meleager/viewsets/passage.py @@ -1,4 +1,7 @@ +import re + from rest_framework import viewsets +import django_filters.rest_framework from ..models import Passage from ..serializers.passage import PassageSerializer @@ -8,3 +11,27 @@ class PassageViewSet(viewsets.ReadOnlyModelViewSet): queryset = Passage.objects.all() serializer_class = PassageSerializer http_method_names = ['get', 'head'] + filter_backends = [django_filters.rest_framework.DjangoFilterBackend] + filterset_fields = ["book", "fragment", "sub_fragment"] + + # def get_queryset(self): + # qs = Passage.objects.all() + # book = self.request.query_params.get('book', None) + # fragment = self.request.query_params.get('fragment', None) + # sub_fragment = None + + # if book is not None: + # qs = qs.filter(book__number=book) + + # if fragment is not None: + # regexp = r"(\d+)(\w*)" + # matches = re.match(regexp, fragment) + # if matches: + # fragment = int(matches[1]) + # sub_fragment = matches[2] + + # qs = qs.filter(fragment=fragment) + # if sub_fragment: + # qs = qs.filter(sub_fragment=sub_fragment) + + # return qs \ No newline at end of file diff --git a/django/user/__init__.py b/django/meleager_user/__init__.py similarity index 100% rename from django/user/__init__.py rename to django/meleager_user/__init__.py diff --git a/django/user/admin.py b/django/meleager_user/admin.py similarity index 100% rename from django/user/admin.py rename to django/meleager_user/admin.py diff --git a/django/meleager_user/apps.py b/django/meleager_user/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..a96e5f82696670210a482c9403c863cf11a8f0ba --- /dev/null +++ b/django/meleager_user/apps.py @@ -0,0 +1,4 @@ +from django.apps import AppConfig + +class MeleagerUserConfig(AppConfig): + name = "meleager_user" diff --git a/django/meleager_user/auth_urls.py b/django/meleager_user/auth_urls.py new file mode 100644 index 0000000000000000000000000000000000000000..8927dff47907156f791d799a0859a1ced6715b17 --- /dev/null +++ b/django/meleager_user/auth_urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from .views import LoginView, LogoutView, PasswordResetView + +urlpatterns = [ + path('login/', LoginView.as_view(), name="login"), + path('logout/', LogoutView.as_view(), name="logout"), + path('reset/', PasswordResetView.as_view(), name="password_reset"), +] diff --git a/django/meleager_user/fixtures/admin_user.json b/django/meleager_user/fixtures/admin_user.json new file mode 100644 index 0000000000000000000000000000000000000000..7a6cb664bc80ac05c92760234e46359cde16ac9e --- /dev/null +++ b/django/meleager_user/fixtures/admin_user.json @@ -0,0 +1,38 @@ +[ + { + "model": "meleager_user.user", + "fields": { + "password": "!cftze7QXrzidvow2x53dqyGZFqBHkZg63vEFGzJw", + "last_login": null, + "is_superuser": false, + "username": "AnonymousUser", + "first_name": "", + "last_name": "", + "email": "", + "is_staff": false, + "is_active": true, + "date_joined": "2020-11-12T06:14:06.173Z", + "institution": "", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "meleager_user.user", + "fields": { + "password": "pbkdf2_sha256$216000$itKY4GPhYWHR$d1pd9QDaZZosNEzNcul6UCE/M3BtvK3DhRQiO1y6Ga4=", + "last_login": null, + "is_superuser": true, + "username": "test_admin", + "first_name": "", + "last_name": "", + "email": "admin@test.com", + "is_staff": true, + "is_active": true, + "date_joined": "2020-11-12T06:59:06.702Z", + "institution": "", + "groups": [], + "user_permissions": [] + } + } +] \ No newline at end of file diff --git a/django/meleager_user/fixtures/test_admin.json b/django/meleager_user/fixtures/test_admin.json new file mode 100644 index 0000000000000000000000000000000000000000..cf4254fae35658fbd851494811820fedd2557835 --- /dev/null +++ b/django/meleager_user/fixtures/test_admin.json @@ -0,0 +1,40 @@ +[ + { + "model": "meleager_user.user", + "pk": 1, + "fields": { + "password": "!xeTTOcyuHKdRJyOPkNaiXosuRkntzKOZKV26QBVo", + "last_login": null, + "is_superuser": false, + "username": "AnonymousUser", + "first_name": "", + "last_name": "", + "email": "", + "is_staff": false, + "is_active": true, + "date_joined": "2020-11-14T02:31:42.180Z", + "institution": "", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "meleager_user.user", + "pk": 2, + "fields": { + "password": "pbkdf2_sha256$216000$1NDKDHZcziZa$sxZD899PegMg0IOiWOFso+F+gmSF5RXBPOkyrC6OvZ4=", + "last_login": null, + "is_superuser": true, + "username": "test_admin", + "first_name": "", + "last_name": "", + "email": "admin@admin.com", + "is_staff": true, + "is_active": true, + "date_joined": "2020-11-14T02:32:13.175Z", + "institution": "", + "groups": [], + "user_permissions": [] + } + } +] \ No newline at end of file diff --git a/django/user/migrations/0001_initial.py b/django/meleager_user/migrations/0001_initial.py similarity index 95% rename from django/user/migrations/0001_initial.py rename to django/meleager_user/migrations/0001_initial.py index 44c39599eaf2e8005451ab2664fab8bf5cd6caef..aa4e2ca68d66bab6aca53bd64262d76ce79d7adb 100644 --- a/django/user/migrations/0001_initial.py +++ b/django/meleager_user/migrations/0001_initial.py @@ -1,9 +1,9 @@ -# Generated by Django 3.1.3 on 2020-11-28 03:15 +# Generated by Django 3.1.3 on 2021-02-05 22:20 import django.contrib.auth.validators from django.db import migrations, models import django.utils.timezone -import user.models +import meleager_user.models class Migration(migrations.Migration): @@ -39,7 +39,7 @@ class Migration(migrations.Migration): 'abstract': False, }, managers=[ - ('objects', user.models.CustomUserManager()), + ('objects', meleager_user.models.CustomUserManager()), ], ), ] diff --git a/django/user/migrations/__init__.py b/django/meleager_user/migrations/__init__.py similarity index 100% rename from django/user/migrations/__init__.py rename to django/meleager_user/migrations/__init__.py diff --git a/django/user/models.py b/django/meleager_user/models.py similarity index 100% rename from django/user/models.py rename to django/meleager_user/models.py diff --git a/django/meleager_user/templates/registration/login.html b/django/meleager_user/templates/registration/login.html new file mode 100644 index 0000000000000000000000000000000000000000..2c39ee27e734463bccc3cb577adc94ea178171dc --- /dev/null +++ b/django/meleager_user/templates/registration/login.html @@ -0,0 +1,35 @@ +<html> +<title>Login page</title> +{% if form.errors %} +<p>Your username and password didn't match. Please try again.</p> +{% endif %} + +{% if next %} +{% if user.is_authenticated %} +<p>Your account doesn't have access to this page. To proceed, + please login with an account that has access.</p> +{% else %} +<p>Please login to see this page.</p> +{% endif %} +{% endif %} + +<form method="post" action="{% url 'login' %}"> + {% csrf_token %} + <table> + <tr> + <td>{{ form.username.label_tag }}</td> + <td>{{ form.username }}</td> + </tr> + <tr> + <td>{{ form.password.label_tag }}</td> + <td>{{ form.password }}</td> + </tr> + </table> + + <input type="submit" value="login"> + <input type="hidden" name="next" value="{{ next }}"> +</form> + +<!-- <p><a href="{% url 'password_reset' %}">Lost password?</a></p> --> + +</html> \ No newline at end of file diff --git a/django/meleager_user/templates/user/profile.html b/django/meleager_user/templates/user/profile.html new file mode 100644 index 0000000000000000000000000000000000000000..4e40554573626e5fdb8927e0681bc16344c3c7ab --- /dev/null +++ b/django/meleager_user/templates/user/profile.html @@ -0,0 +1,11 @@ +<html> + <title> + Profile page + </title> + <body> + <h2> + Profile page for {{ user }} + </h2> + <a href="{% url 'logout' %}">Logout</a> + </body> +</html> \ No newline at end of file diff --git a/django/meleager_user/tests/__init__.py b/django/meleager_user/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/django/meleager_user/tests/test_login_form.py b/django/meleager_user/tests/test_login_form.py new file mode 100644 index 0000000000000000000000000000000000000000..dc83363dffe40189ee81bb57de14e2c41fe71afe --- /dev/null +++ b/django/meleager_user/tests/test_login_form.py @@ -0,0 +1,24 @@ +from django.test import TestCase +from django.urls import reverse +from meleager_user.models import User + +class CommentsForms(TestCase): + fixtures = ["test_data.json"] + + @classmethod + def setUpTestData(cls): + pass + + def test_login_form_post(self): + response = self.client.post( + reverse("login"), + data={ + "username": "test_admin", + "password": "admin12345", + }, + follow=True + ) + + self.assertTrue( + response.context['user'].is_authenticated + ) diff --git a/django/meleager_user/user_urls.py b/django/meleager_user/user_urls.py new file mode 100644 index 0000000000000000000000000000000000000000..440aade0f9a2259c65a97c12a26a763448d14c4a --- /dev/null +++ b/django/meleager_user/user_urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from .views import ProfileView + +urlpatterns = [ + path('profile/', ProfileView.as_view(), name="profile"), +] diff --git a/django/meleager_user/views.py b/django/meleager_user/views.py new file mode 100644 index 0000000000000000000000000000000000000000..bb147d04018651527eb8c5a1bd029f77a670dd75 --- /dev/null +++ b/django/meleager_user/views.py @@ -0,0 +1,23 @@ +from django.views.generic import DetailView +from django.contrib.auth.views import LoginView as DLoginView +from django.contrib.auth.views import LogoutView as DLogoutView +from django.contrib.auth.views import PasswordResetView as DPwdResetView +from django.contrib.auth import get_user_model + +class LoginView(DLoginView): + template_name = "registration/login.html" + +class LogoutView(DLogoutView): + pass + +class PasswordResetView(DPwdResetView): + template_name = "registration/password_reset.html" + +class ProfileView(DetailView): + template_name = "user/profile.html" + model = get_user_model() + + def get_object(self): + user = self.request.user + print(user) + return user \ No newline at end of file diff --git a/django/user/apps.py b/django/user/apps.py deleted file mode 100644 index 1f2369a409abc934a14dfd5c460d9470639a9342..0000000000000000000000000000000000000000 --- a/django/user/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class UserConfig(AppConfig): - name = "user" diff --git a/django/user/fixtures/admin_user.json b/django/user/fixtures/admin_user.json deleted file mode 100644 index 9a9c8807394850a9794ea3d1658bdc39e27449ae..0000000000000000000000000000000000000000 --- a/django/user/fixtures/admin_user.json +++ /dev/null @@ -1 +0,0 @@ -[{"model": "user.user", "fields": {"password": "!cftze7QXrzidvow2x53dqyGZFqBHkZg63vEFGzJw", "last_login": null, "is_superuser": false, "username": "AnonymousUser", "first_name": "", "last_name": "", "email": "", "is_staff": false, "is_active": true, "date_joined": "2020-11-12T06:14:06.173Z", "institution": "", "groups": [], "user_permissions": []}},{"model": "user.user", "fields": {"password": "pbkdf2_sha256$216000$itKY4GPhYWHR$d1pd9QDaZZosNEzNcul6UCE/M3BtvK3DhRQiO1y6Ga4=", "last_login": null, "is_superuser": true, "username": "test_admin", "first_name": "", "last_name": "", "email": "admin@test.com", "is_staff": true, "is_active": true, "date_joined": "2020-11-12T06:59:06.702Z", "institution": "", "groups": [], "user_permissions": []}}] \ No newline at end of file diff --git a/django/user/fixtures/test_admin.json b/django/user/fixtures/test_admin.json deleted file mode 100644 index 97df4b486f2fa0b7d65208a55bff44dfc3fd8ae4..0000000000000000000000000000000000000000 --- a/django/user/fixtures/test_admin.json +++ /dev/null @@ -1 +0,0 @@ -[{"model": "user.user", "pk": 1, "fields": {"password": "!xeTTOcyuHKdRJyOPkNaiXosuRkntzKOZKV26QBVo", "last_login": null, "is_superuser": false, "username": "AnonymousUser", "first_name": "", "last_name": "", "email": "", "is_staff": false, "is_active": true, "date_joined": "2020-11-14T02:31:42.180Z", "institution": "", "groups": [], "user_permissions": []}}, {"model": "user.user", "pk": 2, "fields": {"password": "pbkdf2_sha256$216000$1NDKDHZcziZa$sxZD899PegMg0IOiWOFso+F+gmSF5RXBPOkyrC6OvZ4=", "last_login": null, "is_superuser": true, "username": "test_admin", "first_name": "", "last_name": "", "email": "admin@admin.com", "is_staff": true, "is_active": true, "date_joined": "2020-11-14T02:32:13.175Z", "institution": "", "groups": [], "user_permissions": []}}] \ No newline at end of file diff --git a/django/web/forms/author.py b/django/web/forms/author.py index 62bfbcd08cba465613726e09f7ffcea73096f84b..432ca61bc7dddacb99f4420cf54b8afd3edf49cb 100644 --- a/django/web/forms/author.py +++ b/django/web/forms/author.py @@ -5,7 +5,7 @@ from web.forms.name import NameWidget class AuthorForm(forms.ModelForm): names = forms.ModelMultipleChoiceField( - queryset=Name.objects.exclude(mlgr_authors=None), widget=NameWidget() + queryset=Name.objects.exclude(authors=None), widget=NameWidget() ) class Meta: diff --git a/django/web/forms/city.py b/django/web/forms/city.py index 3b0d3c2a0b7dfe27a5cbc554a5bbb0e3b4d06c95..358e47cde3a361da9c069a1580efb1205b0f0a3b 100644 --- a/django/web/forms/city.py +++ b/django/web/forms/city.py @@ -6,7 +6,7 @@ from web.forms.name import NameWidget class CityForm(forms.ModelForm): names = forms.ModelMultipleChoiceField( - queryset=Name.objects.exclude(mlgr_cities=None), widget=NameWidget() + queryset=Name.objects.exclude(cities=None), widget=NameWidget() ) descriptions = forms.ModelMultipleChoiceField( diff --git a/django/web/forms/comment_description.py b/django/web/forms/comment_description.py index e1638382fdee499565fa3415515ee0f973f1ea5c..b3e15f34d6daf44059e64144d805b7e7716f3d65 100644 --- a/django/web/forms/comment_description.py +++ b/django/web/forms/comment_description.py @@ -1,12 +1,18 @@ from django import forms -from django.utils.translation import gettext_lazy as _ +from meleager.models import Language -class CommentDescriptionForm(forms.Form): + +class CommentCreateForm(forms.Form): comment_title = forms.CharField() description = forms.CharField(widget=forms.Textarea(attrs={"rows": 2, "cols": 40})) - language = forms.CharField( - min_length=3, - max_length=3, - help_text=_("The language must be 3 characters long."), - ) + language = forms.ModelChoiceField(queryset=Language.objects.preferred()) + + +class CommentUpdateForm(forms.Form): + description = forms.CharField(widget=forms.Textarea(attrs={"rows": 2, "cols": 40})) + language = forms.ModelChoiceField(queryset=Language.objects.preferred()) + + +class CommentDeleteForm(forms.Form): + pk = forms.CharField(widget=forms.widgets.HiddenInput()) diff --git a/django/web/forms/passage.py b/django/web/forms/passage.py index 5c73502c33573fa8508f4e87fa2ed05315e054a4..0b24bc66b449f814b2fe08796d806219fd402724 100644 --- a/django/web/forms/passage.py +++ b/django/web/forms/passage.py @@ -1,7 +1,8 @@ +from itertools import groupby + from django import forms -from web.models import APPassage -from meleager.models import Author, Keyword -from django.core.exceptions import ValidationError + +from meleager.models import Author, City, Keyword, Passage class PassageForm(forms.ModelForm): @@ -9,23 +10,74 @@ class PassageForm(forms.ModelForm): sub_fragment = forms.CharField(disabled=True) class Meta: - model = APPassage + model = Passage fields = ["fragment", "sub_fragment"] exclude = ["urn", "images", "manuscripts", "keywords"] -class AddEntityToPassageForm(forms.Form): - model = None - entity_pk = forms.IntegerField(widget=forms.TextInput()) - def clean_entity_pk(self): - pk = self.cleaned_data["entity_pk"] - if not self.model.objects.filter(pk=pk).count(): - raise ValidationError(f"Unknown {self.model._meta.verbose_name} {pk} in the database.") +class AddAuthorToPassageForm(forms.Form): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + choices = [("", "-------------")] + choices += sorted( + [ + (author.pk, ", ".join(name.name for name in author.names.all())) + for author in Author.objects.all() + ], + # Sort by name. + key=lambda x: x[1], + ) + self.fields["author"] = forms.ChoiceField(choices=choices) + + +class AddCityToPassageForm(forms.Form): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + choices = [("", "-------------")] + choices += sorted( + [ + ( + city.pk, + ", ".join( + name.name for name in city.names.exclude(cities=None) + ), + ) + for city in City.objects.all() + ], + # Sort by name. + key=lambda x: x[1], + ) + self.fields["city"] = forms.ChoiceField(choices=choices) + - return pk +class AddKeywordToPassageForm(forms.Form): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + choices = [("", "-------------")] + # We sort choices, first by `category` then by `name` to be able + # to propose a consistent optgroup on the HTML side. + keywords = sorted( + [ + ( + keyword.pk, + ", ".join(name.name for name in keyword.names.all()), + keyword.category.names.first().name, + ) + for keyword in Keyword.objects.select_related("category") + ], + # Sort by category. + key=lambda x: x[2], + ) + choices += ( + ( + category, + # Sort by name, only keep pk (0) and name (1). + sorted([(item[0], item[1]) for item in items], key=lambda x: x[1]), + ) + for category, items in groupby(keywords, key=lambda x: x[2]) + ) + self.fields["keyword"] = forms.ChoiceField(choices=choices) -class AddAuthorToPassageForm(AddEntityToPassageForm): - model = Author -class AddKeywordToPassageForm(AddEntityToPassageForm): - model = Keyword \ No newline at end of file +class RemoveFromPassageForm(forms.Form): + pk = forms.CharField(widget=forms.widgets.HiddenInput()) diff --git a/django/web/forms/passage_description.py b/django/web/forms/passage_description.py new file mode 100644 index 0000000000000000000000000000000000000000..88a971aab0a26e4a02e076a27fb7532b4d241e88 --- /dev/null +++ b/django/web/forms/passage_description.py @@ -0,0 +1,17 @@ +from django import forms + +from meleager.models import Language + + +class DescriptionCreateForm(forms.Form): + description = forms.CharField(widget=forms.Textarea(attrs={"rows": 2, "cols": 40})) + language = forms.ModelChoiceField(queryset=Language.objects.preferred()) + + +class DescriptionUpdateForm(forms.Form): + description = forms.CharField(widget=forms.Textarea(attrs={"rows": 2, "cols": 40})) + language = forms.ModelChoiceField(queryset=Language.objects.preferred()) + + +class DescriptionDeleteForm(forms.Form): + pk = forms.CharField(widget=forms.widgets.HiddenInput()) diff --git a/django/web/forms/text.py b/django/web/forms/text.py index 6f3a43542d0e9c640cc9ad88441aba50905f2916..b6fc5c8ef27437744ed5b31f819ab63652243dc0 100644 --- a/django/web/forms/text.py +++ b/django/web/forms/text.py @@ -1,8 +1,11 @@ from django import forms -from meleager.models import Text +from meleager.models import Language, Text + + +class AddTextToPassageForm(forms.ModelForm): + language = forms.ModelChoiceField(queryset=Language.objects.preferred()) -class TextForm(forms.ModelForm): class Meta: model = Text fields = ("text", "language") diff --git a/django/web/migrations/0001_initial.py b/django/web/migrations/0001_initial.py deleted file mode 100644 index e31345fd016e5703659cf945821f289282c5e39c..0000000000000000000000000000000000000000 --- a/django/web/migrations/0001_initial.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 3.1.3 on 2020-11-28 03:15 - -from django.db import migrations - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('meleager', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='APPassage', - fields=[ - ], - options={ - 'proxy': True, - 'indexes': [], - 'constraints': [], - }, - bases=('meleager.passage',), - ), - migrations.CreateModel( - name='APScholium', - fields=[ - ], - options={ - 'proxy': True, - 'indexes': [], - 'constraints': [], - }, - bases=('meleager.scholium',), - ), - ] diff --git a/django/web/models.py b/django/web/models.py deleted file mode 100644 index a7f93c2942a415d15eccd24718d21e5d67ac380e..0000000000000000000000000000000000000000 --- a/django/web/models.py +++ /dev/null @@ -1,33 +0,0 @@ -from pathlib import Path -from urllib.parse import urlparse - -from django.urls import reverse -from meleager.models import Passage, Scholium - - -class APPassage(Passage): - def get_absolute_url(self): - return reverse( - "web:passage-detail", - args=(self.book.number, self.fragment, self.sub_fragment), - ) - - @property - def urn_value(self): - return Path(urlparse(self.get_absolute_url()).path).name - - class Meta: - proxy = True - - -class APScholium(Scholium): - def get_absolute_url(self): - return reverse( - "web:scholium", - self.passage.book.number, - self.passage.fragment, - self.passage.sub_fragment, - ) - - class Meta: - proxy = True diff --git a/django/web/static/web/css/styles.css b/django/web/static/web/css/styles.css index c975917ce1140ab4d75cf6f95abf10d44f3bb81c..edbe95b36de5bc5aac9372c64507898da986e4de 100644 --- a/django/web/static/web/css/styles.css +++ b/django/web/static/web/css/styles.css @@ -2,6 +2,9 @@ .content h2 { font-family: 'montserrat-medium'; } +.content h2 { + border-bottom: 1px solid var(--cirrus-gray); +} .border-orange-300 { border-color: #f6b65c!important; @@ -31,3 +34,68 @@ textarea { border-color:var(--cirrus-danger); background-color:rgba(244,67,54,.05)!important } + +.i18n-item { + border-radius: .25rem; + width: 100%; +} +.i18n-item:hover { + background: #fdeed1; +} +.i18n-item a { + margin-top: -2px; +} + +.tag.tag--halflarge { + font-size: 110%; + padding: .1rem .6rem; +} +.tag-container-expand .tag--hover { + width: 0 !important; + padding: .1rem 0 !important; + visibility: hidden; + transition-property: width, padding; + transition-duration: .4s; + transition-timing-function: ease-in-out; +} +.tag-container-expand .tag--update, +.tag-container-expand .tag--delete { + border-radius: .25rem !important; +} +.tag-container-expand:hover .tag--hover { + width: 6rem !important; + padding: .1rem .6rem !important; + visibility: visible; + transition-property: width, padding; + transition-duration: .4s; + transition-timing-function: ease-in-out; + border-radius: 0 .25rem .25rem 0 !important; +} +.tag-container-expand:hover .tag--update, +.tag-container-expand:hover .tag--delete { + border-radius: .25rem 0 0 .25rem !important; +} +h2.tag-container-expand a { + font-size: 1.1rem; +} + + +.tag a { + color: inherit; + font-weight: inherit; +} +.svgicon { + display: inline-block; + width: 1.25rem; + height: 1.25rem; + stroke-width: 0; + stroke: currentColor; + fill: currentColor; +} +.svgicon-cross { + color: white; +} + +.grid summary { + font-size: 1.7rem; +} diff --git a/django/web/static/web/img/symbol-defs.svg b/django/web/static/web/img/symbol-defs.svg new file mode 100644 index 0000000000000000000000000000000000000000..2854254ae36f59c6a9e027fa3d31e931d09190f7 --- /dev/null +++ b/django/web/static/web/img/symbol-defs.svg @@ -0,0 +1,17 @@ +<svg aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<defs> +<symbol id="icon-pencil" viewBox="0 0 32 32"> +<path d="M27 0c2.761 0 5 2.239 5 5 0 1.126-0.372 2.164-1 3l-2 2-7-7 2-2c0.836-0.628 1.874-1 3-1zM2 23l-2 9 9-2 18.5-18.5-7-7-18.5 18.5zM22.362 11.362l-14 14-1.724-1.724 14-14 1.724 1.724z"></path> +</symbol> +<symbol id="icon-plus" viewBox="0 0 32 32"> +<path d="M31 12h-11v-11c0-0.552-0.448-1-1-1h-6c-0.552 0-1 0.448-1 1v11h-11c-0.552 0-1 0.448-1 1v6c0 0.552 0.448 1 1 1h11v11c0 0.552 0.448 1 1 1h6c0.552 0 1-0.448 1-1v-11h11c0.552 0 1-0.448 1-1v-6c0-0.552-0.448-1-1-1z"></path> +</symbol> +<symbol id="icon-cancel-circle" viewBox="0 0 32 32"> +<path d="M16 0c-8.837 0-16 7.163-16 16s7.163 16 16 16 16-7.163 16-16-7.163-16-16-16zM16 29c-7.18 0-13-5.82-13-13s5.82-13 13-13 13 5.82 13 13-5.82 13-13 13z"></path> +<path d="M21 8l-5 5-5-5-3 3 5 5-5 5 3 3 5-5 5 5 3-3-5-5 5-5z"></path> +</symbol> +<symbol id="icon-cross" viewBox="0 0 32 32"> +<path d="M31.708 25.708c-0-0-0-0-0-0l-9.708-9.708 9.708-9.708c0-0 0-0 0-0 0.105-0.105 0.18-0.227 0.229-0.357 0.133-0.356 0.057-0.771-0.229-1.057l-4.586-4.586c-0.286-0.286-0.702-0.361-1.057-0.229-0.13 0.048-0.252 0.124-0.357 0.228 0 0-0 0-0 0l-9.708 9.708-9.708-9.708c-0-0-0-0-0-0-0.105-0.104-0.227-0.18-0.357-0.228-0.356-0.133-0.771-0.057-1.057 0.229l-4.586 4.586c-0.286 0.286-0.361 0.702-0.229 1.057 0.049 0.13 0.124 0.252 0.229 0.357 0 0 0 0 0 0l9.708 9.708-9.708 9.708c-0 0-0 0-0 0-0.104 0.105-0.18 0.227-0.229 0.357-0.133 0.355-0.057 0.771 0.229 1.057l4.586 4.586c0.286 0.286 0.702 0.361 1.057 0.229 0.13-0.049 0.252-0.124 0.357-0.229 0-0 0-0 0-0l9.708-9.708 9.708 9.708c0 0 0 0 0 0 0.105 0.105 0.227 0.18 0.357 0.229 0.356 0.133 0.771 0.057 1.057-0.229l4.586-4.586c0.286-0.286 0.362-0.702 0.229-1.057-0.049-0.13-0.124-0.252-0.229-0.357z"></path> +</symbol> +</defs> +</svg> diff --git a/django/web/templates/web/_comments.html b/django/web/templates/web/_comments.html deleted file mode 100644 index 2671e8c5163dda0dc61455c431a494f8b31552ae..0000000000000000000000000000000000000000 --- a/django/web/templates/web/_comments.html +++ /dev/null @@ -1,103 +0,0 @@ -{% load i18n %} -{% load formica %} - -<div class="content" data-controller="modals" data-action="keyup@window->modals#closeAll"> - <h2 class="mt-8"> - {% blocktranslate count counter=comments|length %} - Comment - {% plural %} - Comments - {% endblocktranslate %} - </h2> - - {% for comment in comments %} - <div id="comment-{{ comment.pk }}" class="p-4"> - <a href="#comment-{{ comment.pk }}" class="u-pull-right">#</a> - {{ comment.comment_title }} : - {% for description in comment.descriptions.all %} - <blockquote class="pb-10"> - [{{ description.language.code }}] {{ description.description }} - <div class="mt-6 u-pull-right"> - <a class="btn btn-dark outline" href="#comment-update-{{ comment.pk }}-{{ description.pk }}"> - {% translate "âœï¸ Edit this comment" %} - </a> - </div> - </blockquote> - - <div class="modal modal-large modal-animated--zoom-in" id="comment-update-{{ comment.pk }}-{{ description.pk }}"> - <a href="#back" data-target="modals.closebtn" class="modal-overlay close-btn u-text-right" aria-label="{% translate 'Close' %}"> - <span class="icon mt-2 mr-3"> - <svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="times" class="svg-inline--fa fa-times fa-w-11 fa-wrapper" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="#fff" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg> - </span> - </a> - <div class="modal-content" role="document"> - <form action="{#% url 'web:comment-update' passage_pk %#}" method="post"> - <div class="modal-header"> - <div class="modal-title"> - {% translate "Update comment" %} - </div> - </div> - <div class="modal-body"> - {% form "web/formica/base_form.html" comment.form %} - {% fields %} - {% endform %} - </div> - <div class="modal-footer"> - <div class="form-section u-flex u-justify-space-between"> - <a href="#back" class="btn u-inline-block"> - {% translate "Cancel" %} - </a> - <button class="btn-info u-inline-block" type="submit"> - {% translate "Submit & save" %} - </button> - </div> - </div> - </form> - </div> - </div> - {% endfor %} - </div> - {% endfor %} - - <div class="modal modal-large modal-animated--zoom-in" id="comment-new"> - <a href="#back" data-target="modals.closebtn" class="modal-overlay close-btn u-text-right" aria-label="{% translate 'Close' %}"> - <span class="icon mt-2 mr-3"> - <svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="times" class="svg-inline--fa fa-times fa-w-11 fa-wrapper" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="#fff" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg> - </span> - </a> - <div class="modal-content" role="document"> - <form - action="{% url 'web:comment-add' passage_pk %}" - data-controller="textareas" - data-action="input->textareas#expand" - method="post"> - <div class="modal-header"> - <div class="modal-title"> - {% translate "Add a new comment" %} - </div> - </div> - <div class="modal-body"> - {% form "web/formica/base_form.html" comment_description_form %} - {% fields %} - {% endform %} - </div> - <div class="modal-footer"> - <div class="form-section u-flex u-justify-space-between"> - <a href="#back" class="btn u-inline-block"> - {% translate "Cancel" %} - </a> - <button class="btn-info u-inline-block" type="submit"> - {% translate "Submit & save" %} - </button> - </div> - </div> - </form> - </div> - </div> - - <div class="u-center mt-4"> - <a class="btn btn-dark w-60" href="#comment-new"> - {% translate "+ Add a new comment" %} - </a> - </div> -</div> diff --git a/django/web/templates/web/_keywords.html b/django/web/templates/web/_keywords.html deleted file mode 100644 index 8d47662a0cbc4a9a8852f6cda89b41bcba91ba4f..0000000000000000000000000000000000000000 --- a/django/web/templates/web/_keywords.html +++ /dev/null @@ -1,28 +0,0 @@ -{% load i18n %} - -{% if keywords %} - <div class="content"> - <h2 class="mt-8"> - {% blocktranslate count counter=keywords|length %} - Keyword - {% plural %} - Keywords - {% endblocktranslate %} - </h2> - <div class="tag-container group-tags"> - {% for keyword in keywords %} - {% for description in keyword.names.all %} - <div class="ml-2"> - <div class="tag"> - {{ keyword.category.names.all.0.name }} - </div><div class="tag tag--info"> - {{ description.name }} - </div><div class="tag tag--dark"> - {{ description.language.code }} - </div> - </div> - {% endfor %} - {% endfor %} - </div> - </div> -{% endif %} diff --git a/django/web/templates/web/comment/detail.html b/django/web/templates/web/comment/detail.html new file mode 100644 index 0000000000000000000000000000000000000000..7821518aec485ef7123452d6b3767d90adaa5096 --- /dev/null +++ b/django/web/templates/web/comment/detail.html @@ -0,0 +1,12 @@ +{% extends "web/base.html" %} + +{% block content %} +<h2>Descriptions</h2> +<ul> + {% for desc in comment.descriptions.all %} + <li> + {{ desc.description }} ({{ desc.language }})</a> + </li> + {% endfor %} +</ul> +{% endblock %} \ No newline at end of file diff --git a/django/web/templates/web/index.html b/django/web/templates/web/index.html index 74773974b01271a8c22d2d41fb5fabed9befa65a..51d3e64084102387590f511fa07decdeb497b19d 100644 --- a/django/web/templates/web/index.html +++ b/django/web/templates/web/index.html @@ -1,13 +1,17 @@ {% extends "web/base.html" %} {% block content %} -<div class="container columns"> +<div class="grid grid-cols-2 grid-gap-6"> {% for book in books|dictsort:'number' %} - <h2>Livre {{ book.number }}</h2> - {% for passage in book.passages.all|dictsort:'fragment' %} - <a href="{% url 'web:passage-detail' book.number passage.fragment passage.sub_fragment %}">Epigramma - {{book.number}}.{{passage.fragment}}{{passage.sub_fragment}}</a><br> - {%endfor%} + <div class="mx-4"> + <details> + <summary>Livre {{ book.number }}</summary> + {% for passage in book.passages.all|dictsort:'fragment' %} + <a href="{% url 'web:passage-detail' book.number passage.fragment passage.sub_fragment %}">Epigramma + {{book.number}}.{{passage.fragment}}{{passage.sub_fragment}}</a><br> + {%endfor%} + </details> + </div> {% endfor %} </div> -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/django/web/templates/web/passage.html b/django/web/templates/web/passage.html deleted file mode 100644 index 87b13492f8e7c1c3d87812aabed3a4ca981c4714..0000000000000000000000000000000000000000 --- a/django/web/templates/web/passage.html +++ /dev/null @@ -1,47 +0,0 @@ -{% extends "web/base.html" %} -{% load static i18n %} - -{% block content %} - <h1 class="mt-6 u-center headline-4 font-alt"> - {{ passage }} - </h1> - - {% include "web/_metadata.html" with passage=passage only %} - {% include "web/_manuscripts.html" with manuscripts=passage.manuscripts.all only %} - {% include "web/_texts.html" with texts=texts text_form=text_form passage_pk=passage.pk authors=passage.authors.all csrf_token=csrf_token only %} - {% include "web/_keywords.html" with texts=texts keywords=passage.keywords.all only %} - {% include "web/_scholia.html" with scholia=passage.mlgr_scholia.all passage=passage only %} - {% include "web/_comments.html" with comments=comments comment_description_form=comment_description_form passage_pk=passage.pk csrf_token=csrf_token only %} - {% include "web/_alignments.html" with texts=texts only %} -{% endblock %} - -{% block footer %} - <div class="content"> - <div class="btn-group btn-group-fill"> - <a class="btn bg-orange-200 text-orange-700" href="{{ passage.get_previous_by_created_at.get_absolute_url }}"> - {% translate "↠Previous passage" %} - </a> - <a class="btn bg-orange-200 text-orange-700" href="{% url 'web:passage-update' passage.pk %}"> - {% translate "Edit this passage" %} - </a> - <a class="btn bg-orange-200 text-orange-700" href="{{ passage.get_next_by_created_at.get_absolute_url }}"> - {% translate "Next passage →" %} - </a> - </div> - </div> -{% endblock %} - -{% block extra_body %} - <script src="{% static 'web/js/tabs_controller.js' %}"></script> - <script src="{% static 'web/js/alignments_controller.js' %}"></script> - <script src="{% static 'web/js/modals_controller.js' %}"></script> - <script src="{% static 'web/js/textareas_controller.js' %}"></script> - <script type="text/javascript"> - ;(() => { - application.register('tabs', Tabs) - application.register('alignments', Alignments) - application.register('modals', Modals) - application.register('textareas', Textareas) - })() - </script> -{% endblock %} diff --git a/django/web/templates/web/_alignments.html b/django/web/templates/web/passage/_alignments.html similarity index 100% rename from django/web/templates/web/_alignments.html rename to django/web/templates/web/passage/_alignments.html diff --git a/django/web/templates/web/passage/_authors.html b/django/web/templates/web/passage/_authors.html new file mode 100644 index 0000000000000000000000000000000000000000..b31bb0ec51161e2b3fff13a8fc444e0a178f0c28 --- /dev/null +++ b/django/web/templates/web/passage/_authors.html @@ -0,0 +1,137 @@ +{% load i18n static %} +{% load formica %} + +<div id="authors" class="py-2"> + <div class="content"> + <h2 class="tag-container-expand mt-4 mb-2 u-flex u-items-flex-end u-justify-space-between pb-1"> + {% blocktranslate count counter=authors|length %} + Author + {% plural %} + Authors + {% endblocktranslate %} + <a class="text-light" href="#author-new"> + <span class="tag tag--halflarge tag--update tag--info bg-gray-700"> + <svg class="svgicon svgicon-plus"> + <use xlink:href="{% static 'web/img/symbol-defs.svg' %}#icon-plus"></use> + </svg> + </span><span class="tag tag--halflarge tag--hover tag--info bg-gray-700"> + {% translate 'Add' %} + </span> + </a> + </h2> + <div class="modal modal-large modal-animated--zoom-in" id="author-new"> + <a href="#back" data-target="modals.closebtn" class="modal-overlay close-btn u-text-right" aria-label="{% translate 'Close' %}"> + <span class="icon mt-2 mr-3"> + <svg class="svgicon svgicon-cross"> + <use xlink:href="{% static 'web/img/symbol-defs.svg' %}#icon-cross"></use> + </svg> + </span> + </a> + + <div class="modal-content" role="document"> + <form + action="{% url 'web:passage-add-author' passage_pk %}" + method="post"> + <div class="modal-header"> + <div class="modal-title"> + {% translate "Add an author" %} + </div> + </div> + <div class="modal-body"> + {% form "web/formica/base_form.html" add_author_to_passage_form %} + {% fields %} + {% endform %} + </div> + <div class="modal-footer"> + <div class="form-section u-flex u-justify-space-between"> + <a href="#back" class="btn u-inline-block"> + {% translate "Cancel" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Submit & save" %} + </button> + </div> + </div> + </form> + </div> + </div> + + <div class="tag-container group-tags"> + {% for author in authors %} + <p class="i18n-item tag-container-expand px-1 pt-1"> + {% for preferred_language in preferred_languages %} + {% for name in author.names.all %} + {% if name.language.code == preferred_language.code %} + <span class="mr-1"> + <span class="tag tag--halflarge tag--white bg-orange-300"> + {{ name.name }} + </span><span class="tag tag--halflarge tag--black bg-orange-800"> + {{ name.language.code }} + </span> + </span> + {% endif %} + {% endfor %} + {% endfor %} + <a class="u-pull-right" href="#author-delete-{{ author.pk }}"> + <span class="tag tag--halflarge tag--delete tag--danger"> + <svg class="svgicon svgicon-cancel-circle"> + <use xlink:href="{% static 'web/img/symbol-defs.svg' %}#icon-cancel-circle"></use> + </svg> + </span><span class="tag tag--halflarge tag--hover tag--danger"> + {% translate 'Remove' %} + </span> + </a> + </p> + + <div class="modal modal-large modal-animated--zoom-in" id="author-delete-{{ author.pk }}"> + <a href="#back" data-target="modals.closebtn" class="modal-overlay close-btn u-text-right" aria-label="{% translate 'Close' %}"> + <span class="icon mt-2 mr-3"> + <svg class="svgicon svgicon-cross"> + <use xlink:href="{% static 'web/img/symbol-defs.svg' %}#icon-cross"></use> + </svg> + </span> + </a> + + <div class="modal-content" role="document"> + <form + action="{% url 'web:passage-remove-author' passage_pk %}" + method="post"> + <div class="modal-header"> + <div class="modal-title"> + {% translate "Remove an author" %} + </div> + </div> + <div class="modal-body"> + <p> + {% translate "You are about to remove an author from this passage:" %} + </p> + <ul> + {% for preferred_language in preferred_languages %} + {% for name in author.names.all %} + {% if name.language.code == preferred_language.code %} + <li><strong>{{ name.name }}</strong> ({{ name.language.code }})</li> + {% endif %} + {% endfor %} + {% endfor %} + </ul> + {% form "web/formica/base_form.html" author.remove_form %} + {% fields %} + {% endform %} + </div> + <div class="modal-footer"> + <div class="form-section u-flex u-justify-space-between"> + <a href="#back" class="btn u-inline-block"> + {% translate "Keep it" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Confirm & remove" %} + </button> + </div> + </div> + </form> + </div> + </div> + {% endfor %} + </div> + </div> +</div> diff --git a/django/web/templates/web/passage/_cities.html b/django/web/templates/web/passage/_cities.html new file mode 100644 index 0000000000000000000000000000000000000000..45960c52d5a9d711c86322e8ec01d74503472f86 --- /dev/null +++ b/django/web/templates/web/passage/_cities.html @@ -0,0 +1,137 @@ +{% load i18n static %} +{% load formica %} + +<div id="cities" class="py-2"> + <div class="content"> + <h2 class="tag-container-expand mt-4 mb-2 u-flex u-items-flex-end u-justify-space-between pb-1"> + {% blocktranslate count counter=cities|length %} + City + {% plural %} + Cities + {% endblocktranslate %} + <a class="text-light" href="#city-new"> + <span class="tag tag--halflarge tag--update tag--info bg-gray-700"> + <svg class="svgicon svgicon-plus"> + <use xlink:href="{% static 'web/img/symbol-defs.svg' %}#icon-plus"></use> + </svg> + </span><span class="tag tag--halflarge tag--hover tag--info bg-gray-700"> + {% translate 'Add' %} + </span> + </a> + </h2> + <div class="modal modal-large modal-animated--zoom-in" id="city-new"> + <a href="#back" data-target="modals.closebtn" class="modal-overlay close-btn u-text-right" aria-label="{% translate 'Close' %}"> + <span class="icon mt-2 mr-3"> + <svg class="svgicon svgicon-cross"> + <use xlink:href="{% static 'web/img/symbol-defs.svg' %}#icon-cross"></use> + </svg> + </span> + </a> + + <div class="modal-content" role="document"> + <form + action="{% url 'web:passage-add-city' passage_pk %}" + method="post"> + <div class="modal-header"> + <div class="modal-title"> + {% translate "Add a city" %} + </div> + </div> + <div class="modal-body"> + {% form "web/formica/base_form.html" add_city_to_passage_form %} + {% fields %} + {% endform %} + </div> + <div class="modal-footer"> + <div class="form-section u-flex u-justify-space-between"> + <a href="#back" class="btn u-inline-block"> + {% translate "Cancel" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Submit & save" %} + </button> + </div> + </div> + </form> + </div> + </div> + + <div class="tag-container group-tags"> + {% for city in cities %} + <p class="i18n-item tag-container-expand px-1 pt-1"> + {% for preferred_language in preferred_languages %} + {% for name in city.names.all %} + {% if name.language.code == preferred_language.code %} + <span class="mr-1"> + <span class="tag tag--halflarge tag--white bg-orange-300"> + {{ name.name }} + </span><span class="tag tag--halflarge tag--black bg-orange-800"> + {{ name.language.code }} + </span> + </span> + {% endif %} + {% endfor %} + {% endfor %} + <a class="u-pull-right" href="#city-delete-{{ city.pk }}"> + <span class="tag tag--halflarge tag--delete tag--danger"> + <svg class="svgicon svgicon-cancel-circle"> + <use xlink:href="{% static 'web/img/symbol-defs.svg' %}#icon-cancel-circle"></use> + </svg> + </span><span class="tag tag--halflarge tag--hover tag--danger"> + {% translate 'Remove' %} + </span> + </a> + </p> + + <div class="modal modal-large modal-animated--zoom-in" id="city-delete-{{ city.pk }}"> + <a href="#back" data-target="modals.closebtn" class="modal-overlay close-btn u-text-right" aria-label="{% translate 'Close' %}"> + <span class="icon mt-2 mr-3"> + <svg class="svgicon svgicon-cross"> + <use xlink:href="{% static 'web/img/symbol-defs.svg' %}#icon-cross"></use> + </svg> + </span> + </a> + + <div class="modal-content" role="document"> + <form + action="{% url 'web:passage-remove-city' passage_pk %}" + method="post"> + <div class="modal-header"> + <div class="modal-title"> + {% translate "Remove a city" %} + </div> + </div> + <div class="modal-body"> + <p> + {% translate "You are about to remove a city from this passage:" %} + </p> + <ul> + {% for preferred_language in preferred_languages %} + {% for name in city.names.all %} + {% if name.language.code == preferred_language.code %} + <li><strong>{{ name.name }}</strong> ({{ name.language.code }})</li> + {% endif %} + {% endfor %} + {% endfor %} + </ul> + {% form "web/formica/base_form.html" city.remove_form %} + {% fields %} + {% endform %} + </div> + <div class="modal-footer"> + <div class="form-section u-flex u-justify-space-between"> + <a href="#back" class="btn u-inline-block"> + {% translate "Keep it" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Confirm & remove" %} + </button> + </div> + </div> + </form> + </div> + </div> + {% endfor %} + </div> + </div> +</div> diff --git a/django/web/templates/web/passage/_comments.html b/django/web/templates/web/passage/_comments.html new file mode 100644 index 0000000000000000000000000000000000000000..3041ce245f40b495a156fab456e8a51aa1820cbd --- /dev/null +++ b/django/web/templates/web/passage/_comments.html @@ -0,0 +1,159 @@ +{% load i18n static %} +{% load formica %} + +<div id="comments" class="content" data-controller="modals" data-action="keyup@window->modals#closeAll"> + <h2 class="tag-container-expand mt-4 mb-2 u-flex u-items-flex-end u-justify-space-between pb-1"> + {% blocktranslate count counter=comments|length %} + Comment + {% plural %} + Comments + {% endblocktranslate %} + <a class="text-light" href="#comment-create"> + <span class="tag tag--halflarge tag--update tag--info bg-gray-700"> + <svg class="svgicon svgicon-plus"> + <use xlink:href="{% static 'web/img/symbol-defs.svg' %}#icon-plus"></use> + </svg> + </span><span class="tag tag--halflarge tag--hover tag--info bg-gray-700"> + {% translate 'Add' %} + </span> + </a> + </h2> + + {% for comment in comments %} + <div id="comment-{{ comment.pk }}" class="p-4"> + <a href="#comment-{{ comment.pk }}" class="u-pull-right mr-2 lead">#{{ forloop.counter }}</a> + {# TODO: handle multiple descriptions? #} + {% with description=comment.descriptions.first %} + <blockquote class="pb-10"> + [{{ description.language.code }}] {{ description.description }} + <div class="mt-6 u-pull-right tag-container-expand"> + <a href="#comment-update-{{ comment.pk }}-{{ description.pk }}"> + <span class="tag tag--halflarge tag--update tag--info bg-gray-700"> + <svg class="svgicon svgicon-pencil"> + <use xlink:href="{% static 'web/img/symbol-defs.svg' %}#icon-pencil"></use> + </svg> + </span><span class="tag tag--halflarge tag--hover tag--info bg-gray-700"> + {% translate 'Edit' %} + </span> + </a> + <a href="#comment-delete-{{ comment.pk }}-{{ description.pk }}"> + <span class="tag tag--halflarge tag--delete tag--danger"> + <svg class="svgicon svgicon-cancel-circle"> + <use xlink:href="{% static 'web/img/symbol-defs.svg' %}#icon-cancel-circle"></use> + </svg> + </span><span class="tag tag--halflarge tag--hover tag--danger"> + {% translate 'Remove' %} + </span> + </a> + </div> + </blockquote> + + <div class="modal modal-large modal-animated--zoom-in" id="comment-update-{{ comment.pk }}-{{ description.pk }}"> + <a href="#back" data-target="modals.closebtn" class="modal-overlay close-btn u-text-right" aria-label="{% translate 'Close' %}"> + <span class="icon mt-2 mr-3"> + <svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="times" class="svg-inline--fa fa-times fa-w-11 fa-wrapper" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="#fff" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg> + </span> + </a> + <div class="modal-content" role="document"> + <form action="{% url 'web:comment-update' passage_pk comment.pk %}" method="post"> + <div class="modal-header"> + <div class="modal-title"> + {% translate "Update comment" %} + </div> + </div> + <div class="modal-body"> + {% form "web/formica/base_form.html" comment.update_form %} + {% fields %} + {% endform %} + </div> + <div class="modal-footer"> + <div class="form-section u-flex u-justify-space-between"> + <a href="#back" class="btn u-inline-block"> + {% translate "Cancel" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Submit & save" %} + </button> + </div> + </div> + </form> + </div> + </div> + + <div class="modal modal-large modal-animated--zoom-in" id="comment-delete-{{ comment.pk }}-{{ description.pk }}"> + <a href="#back" data-target="modals.closebtn" class="modal-overlay close-btn u-text-right" aria-label="{% translate 'Close' %}"> + <span class="icon mt-2 mr-3"> + <svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="times" class="svg-inline--fa fa-times fa-w-11 fa-wrapper" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="#fff" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg> + </span> + </a> + <div class="modal-content" role="document"> + <form action="{% url 'web:comment-delete' passage_pk comment.pk %}" method="post"> + <div class="modal-header"> + <div class="modal-title"> + {% translate "Delete comment" %} + </div> + </div> + <div class="modal-body"> + <p> + {% translate "You are about to delete a comment from this passage:" %} + </p> + <p> + {{ description.description }} + </p> + {% form "web/formica/base_form.html" comment.delete_form %} + {% fields %} + {% endform %} + </div> + <div class="modal-footer"> + <div class="form-section u-flex u-justify-space-between"> + <a href="#back" class="btn u-inline-block"> + {% translate "Cancel" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Submit & save" %} + </button> + </div> + </div> + </form> + </div> + </div> + {% endwith %} + </div> + {% endfor %} + + <div class="modal modal-large modal-animated--zoom-in" id="comment-create"> + <a href="#back" data-target="modals.closebtn" class="modal-overlay close-btn u-text-right" aria-label="{% translate 'Close' %}"> + <span class="icon mt-2 mr-3"> + <svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="times" class="svg-inline--fa fa-times fa-w-11 fa-wrapper" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="#fff" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg> + </span> + </a> + <div class="modal-content" role="document"> + <form + action="{% url 'web:comment-create' passage_pk %}" + data-controller="textareas" + data-action="input->textareas#expand" + method="post"> + <div class="modal-header"> + <div class="modal-title"> + {% translate "Add a new comment" %} + </div> + </div> + <div class="modal-body"> + {% form "web/formica/base_form.html" comment_create_form %} + {% fields %} + {% endform %} + </div> + <div class="modal-footer"> + <div class="form-section u-flex u-justify-space-between"> + <a href="#back" class="btn u-inline-block"> + {% translate "Cancel" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Submit & save" %} + </button> + </div> + </div> + </form> + </div> + </div> +</div> diff --git a/django/web/templates/web/passage/_descriptions.html b/django/web/templates/web/passage/_descriptions.html new file mode 100644 index 0000000000000000000000000000000000000000..cbff84165c11d65afbb8ec1c80ecdab7a0c615d5 --- /dev/null +++ b/django/web/templates/web/passage/_descriptions.html @@ -0,0 +1,156 @@ +{% load i18n static %} +{% load formica %} + +<div id="descriptions" class="content" data-controller="modals" data-action="keyup@window->modals#closeAll"> + <h2 class="tag-container-expand mt-4 mb-2 u-flex u-items-flex-end u-justify-space-between pb-1"> + {% blocktranslate count counter=description|length %} + Description + {% plural %} + Descriptions + {% endblocktranslate %} + <a class="text-light" href="#description-create"> + <span class="tag tag--halflarge tag--update tag--info bg-gray-700"> + <svg class="svgicon svgicon-plus"> + <use xlink:href="{% static 'web/img/symbol-defs.svg' %}#icon-plus"></use> + </svg> + </span><span class="tag tag--halflarge tag--hover tag--info bg-gray-700"> + {% translate 'Add' %} + </span> + </a> + </h2> + + {% for description in descriptions %} + <div id="description-{{ description.pk }}" class="p-4"> + <a href="#description-{{ description.pk }}" class="u-pull-right mr-2 lead">#{{ forloop.counter }}</a> + <blockquote class="pb-10"> + [{{ description.language.code }}] {{ description.description }} + <div class="mt-6 u-pull-right tag-container-expand"> + <a href="#description-update-{{ description.pk }}"> + <span class="tag tag--halflarge tag--update tag--info bg-gray-700"> + <svg class="svgicon svgicon-pencil"> + <use xlink:href="{% static 'web/img/symbol-defs.svg' %}#icon-pencil"></use> + </svg> + </span><span class="tag tag--halflarge tag--hover tag--info bg-gray-700"> + {% translate 'Edit' %} + </span> + </a> + <a href="#description-delete-{{ description.pk }}"> + <span class="tag tag--halflarge tag--delete tag--danger"> + <svg class="svgicon svgicon-cancel-circle"> + <use xlink:href="{% static 'web/img/symbol-defs.svg' %}#icon-cancel-circle"></use> + </svg> + </span><span class="tag tag--halflarge tag--hover tag--danger"> + {% translate 'Remove' %} + </span> + </a> + </div> + </blockquote> + + <div class="modal modal-large modal-animated--zoom-in" id="description-update-{{ description.pk }}"> + <a href="#back" data-target="modals.closebtn" class="modal-overlay close-btn u-text-right" aria-label="{% translate 'Close' %}"> + <span class="icon mt-2 mr-3"> + <svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="times" class="svg-inline--fa fa-times fa-w-11 fa-wrapper" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="#fff" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg> + </span> + </a> + <div class="modal-content" role="document"> + <form action="{% url 'web:description-update' passage_pk description.pk %}" method="post"> + <div class="modal-header"> + <div class="modal-title"> + {% translate "Update description" %} + </div> + </div> + <div class="modal-body"> + {% form "web/formica/base_form.html" description.update_form %} + {% fields %} + {% endform %} + </div> + <div class="modal-footer"> + <div class="form-section u-flex u-justify-space-between"> + <a href="#back" class="btn u-inline-block"> + {% translate "Cancel" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Submit & save" %} + </button> + </div> + </div> + </form> + </div> + </div> + + <div class="modal modal-large modal-animated--zoom-in" id="description-delete-{{ description.pk }}"> + <a href="#back" data-target="modals.closebtn" class="modal-overlay close-btn u-text-right" aria-label="{% translate 'Close' %}"> + <span class="icon mt-2 mr-3"> + <svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="times" class="svg-inline--fa fa-times fa-w-11 fa-wrapper" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="#fff" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg> + </span> + </a> + <div class="modal-content" role="document"> + <form action="{% url 'web:description-delete' passage_pk description.pk %}" method="post"> + <div class="modal-header"> + <div class="modal-title"> + {% translate "Delete description" %} + </div> + </div> + <div class="modal-body"> + <p> + {% translate "You are about to delete a description from this passage:" %} + </p> + <p> + {{ description.description }} + </p> + {% form "web/formica/base_form.html" description.delete_form %} + {% fields %} + {% endform %} + </div> + <div class="modal-footer"> + <div class="form-section u-flex u-justify-space-between"> + <a href="#back" class="btn u-inline-block"> + {% translate "Cancel" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Submit & save" %} + </button> + </div> + </div> + </form> + </div> + </div> + </div> + {% endfor %} + + <div class="modal modal-large modal-animated--zoom-in" id="description-create"> + <a href="#back" data-target="modals.closebtn" class="modal-overlay close-btn u-text-right" aria-label="{% translate 'Close' %}"> + <span class="icon mt-2 mr-3"> + <svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="times" class="svg-inline--fa fa-times fa-w-11 fa-wrapper" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="#fff" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg> + </span> + </a> + <div class="modal-content" role="document"> + <form + action="{% url 'web:description-create' passage_pk %}" + data-controller="textareas" + data-action="input->textareas#expand" + method="post"> + <div class="modal-header"> + <div class="modal-title"> + {% translate "Add a new description" %} + </div> + </div> + <div class="modal-body"> + {% form "web/formica/base_form.html" description_create_form %} + {% fields %} + {% endform %} + </div> + <div class="modal-footer"> + <div class="form-section u-flex u-justify-space-between"> + <a href="#back" class="btn u-inline-block"> + {% translate "Cancel" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Submit & save" %} + </button> + </div> + </div> + </form> + </div> + </div> +</div> diff --git a/django/web/templates/web/passage/_external_references.html b/django/web/templates/web/passage/_external_references.html new file mode 100644 index 0000000000000000000000000000000000000000..715782ffa6942afbab800c5912a1e3d9fb5be9e3 --- /dev/null +++ b/django/web/templates/web/passage/_external_references.html @@ -0,0 +1,21 @@ +{% load i18n %} + +{% if external_references %} +<div id="external_references" class="py-2"> + <div class="content"> + <h2 class="mt-4 mb-2 u-flex u-items-baseline u-justify-space-between"> + {% blocktranslate count counter=external_references|length %} + External reference + {% plural %} + External references + {% endblocktranslate %} + </h2> + <p> + {% for external_reference in external_references %} + <a href="{{ external_reference.url }}"> + {{ external_reference.title }}</a>{% if not forloop.last %}, {% endif %} + {% endfor %} + </p> + </div> +</div> +{% endif %} diff --git a/django/web/templates/web/passage/_internal_references.html b/django/web/templates/web/passage/_internal_references.html new file mode 100644 index 0000000000000000000000000000000000000000..be7c23cb386576189ca0bd9ae23546b6e1a429bf --- /dev/null +++ b/django/web/templates/web/passage/_internal_references.html @@ -0,0 +1,21 @@ +{% load i18n %} + +{% if internal_references %} +<div id="internal_references" class="py-2"> + <div class="content"> + <h2 class="mt-4 mb-2 u-flex u-items-baseline u-justify-space-between"> + {% blocktranslate count counter=internal_references|length %} + Internal reference + {% plural %} + Internal references + {% endblocktranslate %} + </h2> + <p> + {% for internal_reference in internal_references %} + <a href="{% url 'web:passage-detail' internal_reference.book.number internal_reference.fragment internal_reference.sub_fragment %}"> + {% translate "Passage" %} {{ internal_reference.book.number }}.{{ internal_reference.fragment }}{{ internal_reference.sub_fragment }}</a>{% if not forloop.last %}, {% endif %} + {% endfor %} + </p> + </div> +</div> +{% endif %} diff --git a/django/web/templates/web/passage/_keywords.html b/django/web/templates/web/passage/_keywords.html new file mode 100644 index 0000000000000000000000000000000000000000..3b28d1c1825db7a10095218f15c2faea1e8d0788 --- /dev/null +++ b/django/web/templates/web/passage/_keywords.html @@ -0,0 +1,138 @@ +{% load i18n static %} +{% load formica %} + +<div id="keywords" class="py-2"> + <div class="content"> + <h2 class="tag-container-expand mt-4 mb-2 u-flex u-items-flex-end u-justify-space-between pb-1"> + {% blocktranslate count counter=keywords|length %} + Keyword + {% plural %} + Keywords + {% endblocktranslate %} + <a class="text-light" href="#keyword-new"> + <span class="tag tag--halflarge tag--update tag--info bg-gray-700"> + <svg class="svgicon svgicon-plus"> + <use xlink:href="{% static 'web/img/symbol-defs.svg' %}#icon-plus"></use> + </svg> + </span><span class="tag tag--halflarge tag--hover tag--info bg-gray-700"> + {% translate 'Add' %} + </span> + </a> + </h2> + <div class="modal modal-large modal-animated--zoom-in" id="keyword-new"> + <a href="#back" data-target="modals.closebtn" class="modal-overlay close-btn u-text-right" aria-label="{% translate 'Close' %}"> + <span class="icon mt-2 mr-3"> + <svg class="svgicon svgicon-cross"> + <use xlink:href="{% static 'web/img/symbol-defs.svg' %}#icon-cross"></use> + </svg> + </span> + </a> + + <div class="modal-content" role="document"> + <form + action="{% url 'web:passage-add-keyword' passage_pk %}" + method="post"> + <div class="modal-header"> + <div class="modal-title"> + {% translate "Add a keyword" %} + </div> + </div> + <div class="modal-body"> + {% form "web/formica/base_form.html" add_keyword_to_passage_form %} + {% fields %} + {% endform %} + </div> + <div class="modal-footer"> + <div class="form-section u-flex u-justify-space-between"> + <a href="#back" class="btn u-inline-block"> + {% translate "Cancel" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Submit & save" %} + </button> + </div> + </div> + </form> + </div> + </div> + + {% for keyword_category in keywords_categories %} + {# Categories are only in French for now (first item). #} + <h5 class="font-light">{{ keyword_category.names.first.name }}</h5> + <div class="tag-container group-tags"> + {% for keyword in keywords %} + {% if keyword.category.pk == keyword_category.pk %} + <p class="i18n-item tag-container-expand px-1 pt-1"> + {% for name in keyword.names.all %} + <span class="mr-1"> + <span class="tag tag--halflarge tag--white bg-orange-300"> + {{ name.name }} + </span><span class="tag tag--halflarge tag--black bg-orange-800"> + {{ name.language.code }} + </span> + </span> + {% endfor %} + <a class="u-pull-right" href="#keyword-delete-{{ keyword.pk }}"> + <span class="tag tag--halflarge tag--delete tag--danger"> + <svg class="svgicon svgicon-cancel-circle"> + <use xlink:href="{% static 'web/img/symbol-defs.svg' %}#icon-cancel-circle"></use> + </svg> + </span><span class="tag tag--halflarge tag--hover tag--danger"> + {% translate 'Remove' %} + </span> + </a> + </p> + {% endif %} + {% endfor %} + {% for keyword in keywords %} + {% if keyword.category.pk == keyword_category.pk %} + <div class="modal modal-large modal-animated--zoom-in" id="keyword-delete-{{ keyword.pk }}"> + <a href="#back" data-target="modals.closebtn" class="modal-overlay close-btn u-text-right" aria-label="{% translate 'Close' %}"> + <span class="icon mt-2 mr-3"> + <svg class="svgicon svgicon-cross"> + <use xlink:href="{% static 'web/img/symbol-defs.svg' %}#icon-cross"></use> + </svg> + </span> + </a> + + <div class="modal-content" role="document"> + <form + action="{% url 'web:passage-remove-keyword' passage_pk %}" + method="post"> + <div class="modal-header"> + <div class="modal-title"> + {% translate "Remove a keyword" %} + </div> + </div> + <div class="modal-body"> + <p> + {% translate "You are about to remove a keyword from this passage:" %} + </p> + <ul> + {% for name in keyword.names.all %} + <li><strong>{{ name.name }}</strong> ({{ name.language.code }})</li> + {% endfor %} + </ul> + {% form "web/formica/base_form.html" keyword.remove_form %} + {% fields %} + {% endform %} + </div> + <div class="modal-footer"> + <div class="form-section u-flex u-justify-space-between"> + <a href="#back" class="btn u-inline-block"> + {% translate "Keep it" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Confirm & remove" %} + </button> + </div> + </div> + </form> + </div> + </div> + {% endif %} + {% endfor %} + </div> + {% endfor %} + </div> +</div> diff --git a/django/web/templates/web/_manuscripts.html b/django/web/templates/web/passage/_manuscripts.html similarity index 100% rename from django/web/templates/web/_manuscripts.html rename to django/web/templates/web/passage/_manuscripts.html diff --git a/django/web/templates/web/_metadata.html b/django/web/templates/web/passage/_metadata.html similarity index 50% rename from django/web/templates/web/_metadata.html rename to django/web/templates/web/passage/_metadata.html index aa6ed3b60e9c05e89f843136be9e1d5028fd49bb..494af588f2bac847e055cd4901f069954e0c4d1a 100644 --- a/django/web/templates/web/_metadata.html +++ b/django/web/templates/web/passage/_metadata.html @@ -1,19 +1,21 @@ {% load i18n %} <div class="tag-container group-tags u-center"> - <div> - <div class="tag">{% translate "URN" %}</div><div class="tag tag--link">{{ passage.urn_value }}</div> - </div> + {% if obj.urn_value %} + <div> + <div class="tag">{% translate "URN" %}</div><div class="tag tag--link">{{ obj.urn_value }}</div> + </div> + {% endif %} <div class="ml-2"> - <div class="tag">{% translate "Created on" %}</div><div class="tag tag--link">{{ passage.created_at|date:"F j, Y" }}</div> + <div class="tag">{% translate "Created on" %}</div><div class="tag tag--link">{{ obj.created_at|date:"F j, Y" }}</div> </div> <div class="ml-2"> - <div class="tag">{% translate "By" %}</div><div class="tag tag--link">{{ passage.creator }}</div> + <div class="tag">{% translate "By" %}</div><div class="tag tag--link">{{ obj.creator }}</div> </div> <div class="ml-2"> - <div class="tag">{% translate "Updated on" %}</div><div class="tag tag--link">{{ passage.updated_at|date:"F j, Y" }}</div> + <div class="tag">{% translate "Updated on" %}</div><div class="tag tag--link">{{ obj.updated_at|date:"F j, Y" }}</div> </div> </div> diff --git a/django/web/templates/web/_scholia.html b/django/web/templates/web/passage/_scholia.html similarity index 100% rename from django/web/templates/web/_scholia.html rename to django/web/templates/web/passage/_scholia.html diff --git a/django/web/templates/web/_texts.html b/django/web/templates/web/passage/_texts.html similarity index 80% rename from django/web/templates/web/_texts.html rename to django/web/templates/web/passage/_texts.html index 911af23a6dfcbe5afb6f793ee0154b622062bd60..f023289e030a4a3491ce9196e4eb49a2713761c5 100644 --- a/django/web/templates/web/_texts.html +++ b/django/web/templates/web/passage/_texts.html @@ -9,7 +9,7 @@ <ul role="tablist" data-action="keydown->tabs#keyboardA11Y" aria-label="{% translate 'Texts translations tabs (first)' %}"> - {% for text in texts reversed %} + {% for text in texts %} <li {% if forloop.first %}class="selected"{% endif %}> <a role="tab" id="text-tab-first-{{ forloop.counter }}-{{ text.language.code }}" @@ -22,11 +22,11 @@ >{{ text.language.code }}</a> </li> {% endfor %} - <li><a role="tab" href="#text-new">{% translate '+ add' %}</a></li> + <li><a class="bg-gray-700 text-light" role="tab" href="#text-new">{% translate '+ add' %}</a></li> </ul> </div> - {% for text in texts reversed %} + {% for text in texts %} <div role="tabpanel" id="text-content-first-{{ forloop.counter }}-{{ text.language.code }}" aria-labelledby="text-tab-first-{{ forloop.counter }}-{{ text.language.code }}" @@ -36,15 +36,11 @@ > <blockquote> {{ text.text|linebreaks }} - {% for author in authors %} - {% for name in author.names.all %} - {% if name.language.code == text.language.code %} - — <a href="{% url 'web:author-detail' author.pk %}"> - {{ name.name }} - </a> - {% endif %} - {% endfor %} - {% endfor %} + {% if text.author %} + — <a href="{% url 'web:author-detail' text.author.pk %}"> + {{ text.author_name.name }} + </a> + {% endif %} </blockquote> </div> {% endfor %} @@ -56,7 +52,7 @@ <ul role="tablist" data-action="keydown->tabs#keyboardA11Y" aria-label="{% translate 'Texts translations tabs (last)' %}"> - {% for text in texts %} + {% for text in texts reversed %} <li {% if forloop.first %}class="selected"{% endif %}> <a role="tab" id="text-tab-last-{{ forloop.counter }}-{{ text.language.code }}" @@ -69,11 +65,11 @@ >{{ text.language.code }}</a> </li> {% endfor %} - <li><a role="tab" href="#text-new">{% translate '+ add' %}</a></li> + <li><a class="bg-gray-700 text-light" role="tab" href="#text-new">{% translate '+ add' %}</a></li> </ul> </div> - {% for text in texts %} + {% for text in texts reversed %} <div role="tabpanel" id="text-content-last-{{ forloop.counter }}-{{ text.language.code }}" aria-labelledby="text-tab-last-{{ forloop.counter }}-{{ text.language.code }}" @@ -83,15 +79,11 @@ > <blockquote> {{ text.text|linebreaks }} - {% for author in authors %} - {% for name in author.names.all %} - {% if name.language.code == text.language.code %} - — <a href="{% url 'web:author-detail' author.pk %}"> - {{ name.name }} - </a> - {% endif %} - {% endfor %} - {% endfor %} + {% if text.author %} + — <a href="{% url 'web:author-detail' text.author.pk %}"> + {{ text.author_name.name }} + </a> + {% endif %} </blockquote> </div> {% endfor %} @@ -116,7 +108,7 @@ </div> </div> <div class="modal-body"> - {% form "web/formica/base_form.html" text_form %} + {% form "web/formica/base_form.html" add_text_to_passage_form %} {% fields %} {% endform %} </div> diff --git a/django/web/templates/web/passage/add_author_to_passage.html b/django/web/templates/web/passage/add_author_to_passage.html new file mode 100644 index 0000000000000000000000000000000000000000..86518ee63025568a9608af7bc1e50a3c4702b0e6 --- /dev/null +++ b/django/web/templates/web/passage/add_author_to_passage.html @@ -0,0 +1,28 @@ +{% extends "web/base.html" %} +{% load static i18n %} +{% load formica %} + +{% block content %} + <h1 class="mt-6 u-center headline-4 font-alt"> + {{ passage }} + </h1> + + <div class="content"> + <form + action="." + method="post"> + {% form "web/formica/base_form.html" add_author_to_passage_form %} + {% fields %} + {% endform %} + <div class="form-section u-flex u-justify-space-between"> + <a href="{{ passage.get_absolute_url }}" class="btn u-inline-block"> + {% translate "Cancel" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Submit & save" %} + </button> + </div> + </form> + </div> + +{% endblock %} diff --git a/django/web/templates/web/passage/add_city_to_passage.html b/django/web/templates/web/passage/add_city_to_passage.html new file mode 100644 index 0000000000000000000000000000000000000000..6ccafec01f7fb6553fbdd34cf3cea030e1ee4073 --- /dev/null +++ b/django/web/templates/web/passage/add_city_to_passage.html @@ -0,0 +1,29 @@ +{% extends "web/base.html" %} +{% load static i18n %} +{% load formica %} + +{% block content %} + <h1 class="mt-6 u-center headline-4 font-alt"> + {{ passage }} + </h1> + + <div class="content"> + <form + action="." + method="post"> + {% form "web/formica/base_form.html" add_city_to_passage_form %} + {% fields %} + {% endform %} + <div class="form-section u-flex u-justify-space-between"> + <a href="{{ passage.get_absolute_url }}" class="btn u-inline-block"> + {% translate "Cancel" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Submit & save" %} + </button> + </div> + </form> + </div> + +{% endblock %} + diff --git a/django/web/templates/web/passage/add_keyword_to_passage.html b/django/web/templates/web/passage/add_keyword_to_passage.html new file mode 100644 index 0000000000000000000000000000000000000000..de43ad05510f70f8cf71c6e378425e0a55b34b14 --- /dev/null +++ b/django/web/templates/web/passage/add_keyword_to_passage.html @@ -0,0 +1,29 @@ +{% extends "web/base.html" %} +{% load static i18n %} +{% load formica %} + +{% block content %} + <h1 class="mt-6 u-center headline-4 font-alt"> + {{ passage }} + </h1> + + <div class="content"> + <form + action="." + method="post"> + {% form "web/formica/base_form.html" add_keyword_to_passage_form %} + {% fields %} + {% endform %} + <div class="form-section u-flex u-justify-space-between"> + <a href="{{ passage.get_absolute_url }}" class="btn u-inline-block"> + {% translate "Cancel" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Submit & save" %} + </button> + </div> + </form> + </div> + +{% endblock %} + diff --git a/django/web/templates/web/add_text_from_passage.html b/django/web/templates/web/passage/add_text_to_passage.html similarity index 93% rename from django/web/templates/web/add_text_from_passage.html rename to django/web/templates/web/passage/add_text_to_passage.html index b195888b9a0022864ae6751088e5c7b0697ad247..8035894fa6d19df2df42f60caf9321286d7be4b7 100644 --- a/django/web/templates/web/add_text_from_passage.html +++ b/django/web/templates/web/passage/add_text_to_passage.html @@ -13,7 +13,7 @@ data-controller="textareas" data-action="input->textareas#expand" method="post"> - {% form "web/formica/base_form.html" text_form %} + {% form "web/formica/base_form.html" add_text_to_passage_form %} {% fields %} {% endform %} <div class="form-section u-flex u-justify-space-between"> diff --git a/django/web/templates/web/add_comment_from_passage.html b/django/web/templates/web/passage/comment_create.html similarity index 93% rename from django/web/templates/web/add_comment_from_passage.html rename to django/web/templates/web/passage/comment_create.html index 082f938dc4201f2bbccc0d1c860d3bb24e7552e0..8b82d8447fc038a297f747187c95adb845ae7914 100644 --- a/django/web/templates/web/add_comment_from_passage.html +++ b/django/web/templates/web/passage/comment_create.html @@ -14,7 +14,7 @@ data-action="input->textareas#expand" method="post"> {% csrf_token %} - {% form "web/formica/base_form.html" comment_description_form %} + {% form "web/formica/base_form.html" comment_create_form %} {% fields %} {% endform %} <div class="form-section u-flex u-justify-space-between"> diff --git a/django/web/templates/web/passage/comment_delete.html b/django/web/templates/web/passage/comment_delete.html new file mode 100644 index 0000000000000000000000000000000000000000..e597d3ffa9c8bebbee46ba6a921127376739fe63 --- /dev/null +++ b/django/web/templates/web/passage/comment_delete.html @@ -0,0 +1,40 @@ +{% extends "web/base.html" %} +{% load static i18n %} +{% load formica %} + +{% block content %} + <h1 class="mt-6 u-center headline-4 font-alt"> + {{ passage }} + </h1> + + <div class="content"> + <form + action="." + data-controller="textareas" + data-action="input->textareas#expand" + method="post"> + {% csrf_token %} + {% form "web/formica/base_form.html" comment_delete_form %} + {% fields %} + {% endform %} + <div class="form-section u-flex u-justify-space-between"> + <a href="{{ passage.get_absolute_url }}" class="btn u-inline-block"> + {% translate "Cancel" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Submit & save" %} + </button> + </div> + </form> + </div> + +{% endblock %} + +{% block extra_body %} + <script src="{% static 'web/js/textareas_controller.js' %}"></script> + <script type="text/javascript"> + ;(() => { + application.register('textareas', Textareas) + })() + </script> +{% endblock %} diff --git a/django/web/templates/web/passage/comment_update.html b/django/web/templates/web/passage/comment_update.html new file mode 100644 index 0000000000000000000000000000000000000000..d7de1ecaac31fea0b9c5e9d89d4cb3255c469fab --- /dev/null +++ b/django/web/templates/web/passage/comment_update.html @@ -0,0 +1,40 @@ +{% extends "web/base.html" %} +{% load static i18n %} +{% load formica %} + +{% block content %} + <h1 class="mt-6 u-center headline-4 font-alt"> + {{ passage }} + </h1> + + <div class="content"> + <form + action="." + data-controller="textareas" + data-action="input->textareas#expand" + method="post"> + {% csrf_token %} + {% form "web/formica/base_form.html" comment_update_form %} + {% fields %} + {% endform %} + <div class="form-section u-flex u-justify-space-between"> + <a href="{{ passage.get_absolute_url }}" class="btn u-inline-block"> + {% translate "Cancel" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Submit & save" %} + </button> + </div> + </form> + </div> + +{% endblock %} + +{% block extra_body %} + <script src="{% static 'web/js/textareas_controller.js' %}"></script> + <script type="text/javascript"> + ;(() => { + application.register('textareas', Textareas) + })() + </script> +{% endblock %} diff --git a/django/web/templates/web/passage/passage.html b/django/web/templates/web/passage/passage.html new file mode 100644 index 0000000000000000000000000000000000000000..433198e9211daffbd2208e6f5dddeba487586b7f --- /dev/null +++ b/django/web/templates/web/passage/passage.html @@ -0,0 +1,52 @@ +{% extends "web/base.html" %} +{% load static i18n %} + +{% block content %} + <h1 class="mt-6 u-center headline-4 font-alt"> + {% translate "Passage" %} {{ book.number }}.{{ passage.fragment }}{{ passage.sub_fragment }} + </h1> + + {% include "web/passage/_metadata.html" with obj=passage only %} + {% include "web/passage/_descriptions.html" with descriptions=descriptions description_create_form=description_create_form passage_pk=passage.pk csrf_token=csrf_token only %} + {% include "web/passage/_manuscripts.html" with manuscripts=passage.manuscripts.all only %} + {% include "web/passage/_texts.html" with texts=texts add_text_to_passage_form=add_text_to_passage_form passage_pk=passage.pk csrf_token=csrf_token only %} + {% include "web/passage/_authors.html" with authors=authors add_author_to_passage_form=add_author_to_passage_form passage_pk=passage.pk preferred_languages=preferred_languages csrf_token=csrf_token only %} + {% include "web/passage/_cities.html" with cities=cities add_city_to_passage_form=add_city_to_passage_form passage_pk=passage.pk preferred_languages=preferred_languages csrf_token=csrf_token only %} + {% include "web/passage/_keywords.html" with keywords=keywords keywords_categories=keywords_categories add_keyword_to_passage_form=add_keyword_to_passage_form passage_pk=passage.pk preferred_languages=preferred_languages csrf_token=csrf_token only %} + {% include "web/passage/_scholia.html" with scholia=passage.scholia.all passage=passage only %} + {% include "web/passage/_comments.html" with comments=comments comment_create_form=comment_create_form passage_pk=passage.pk csrf_token=csrf_token only %} + {% include "web/passage/_alignments.html" with texts=texts only %} + {% include "web/passage/_internal_references.html" with internal_references=passage.internal_references.all only %} + {% include "web/passage/_external_references.html" with external_references=passage.external_references.all only %} +{% endblock %} + +{% block footer %} + <div class="content"> + <div class="btn-group btn-group-fill"> + <a class="btn bg-orange-200 text-orange-700" href="{{ passage.get_previous_by_created_at.get_absolute_url }}"> + {% translate "↠Previous passage" %} + </a> + <a class="btn bg-orange-200 text-orange-700" href="{% url 'web:passage-update' passage.pk %}"> + {% translate "Edit this passage" %} + </a> + <a class="btn bg-orange-200 text-orange-700" href="{{ passage.get_next_by_created_at.get_absolute_url }}"> + {% translate "Next passage →" %} + </a> + </div> + </div> +{% endblock %} + +{% block extra_body %} + <script src="{% static 'web/js/tabs_controller.js' %}"></script> + <script src="{% static 'web/js/alignments_controller.js' %}"></script> + <script src="{% static 'web/js/modals_controller.js' %}"></script> + <script src="{% static 'web/js/textareas_controller.js' %}"></script> + <script type="text/javascript"> + ;(() => { + application.register('tabs', Tabs) + application.register('alignments', Alignments) + application.register('modals', Modals) + application.register('textareas', Textareas) + })() + </script> +{% endblock %} diff --git a/django/web/templates/web/passage_form.html b/django/web/templates/web/passage/passage_form.html similarity index 100% rename from django/web/templates/web/passage_form.html rename to django/web/templates/web/passage/passage_form.html diff --git a/django/web/templates/web/passage/remove_author_from_passage.html b/django/web/templates/web/passage/remove_author_from_passage.html new file mode 100644 index 0000000000000000000000000000000000000000..fa1d23b924b49630b5c7d57aa02b4c4bd3ba9c84 --- /dev/null +++ b/django/web/templates/web/passage/remove_author_from_passage.html @@ -0,0 +1,29 @@ +{% extends "web/base.html" %} +{% load static i18n %} +{% load formica %} + +{% block content %} + <h1 class="mt-6 u-center headline-4 font-alt"> + {{ passage }} + </h1> + + <div class="content"> + <form + action="." + method="post"> + {% form "web/formica/base_form.html" remove_author_from_passage_form %} + {% fields %} + {% endform %} + <div class="form-section u-flex u-justify-space-between"> + <a href="{{ passage.get_absolute_url }}" class="btn u-inline-block"> + {% translate "Cancel" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Submit & save" %} + </button> + </div> + </form> + </div> + +{% endblock %} + diff --git a/django/web/templates/web/passage/remove_city_from_passage.html b/django/web/templates/web/passage/remove_city_from_passage.html new file mode 100644 index 0000000000000000000000000000000000000000..011da35d5b7ef1b4d3846452f5b9f5e78d43f73b --- /dev/null +++ b/django/web/templates/web/passage/remove_city_from_passage.html @@ -0,0 +1,29 @@ +{% extends "web/base.html" %} +{% load static i18n %} +{% load formica %} + +{% block content %} + <h1 class="mt-6 u-center headline-4 font-alt"> + {{ passage }} + </h1> + + <div class="content"> + <form + action="." + method="post"> + {% form "web/formica/base_form.html" remove_city_from_passage_form %} + {% fields %} + {% endform %} + <div class="form-section u-flex u-justify-space-between"> + <a href="{{ passage.get_absolute_url }}" class="btn u-inline-block"> + {% translate "Cancel" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Submit & save" %} + </button> + </div> + </form> + </div> + +{% endblock %} + diff --git a/django/web/templates/web/passage/remove_keyword_from_passage.html b/django/web/templates/web/passage/remove_keyword_from_passage.html new file mode 100644 index 0000000000000000000000000000000000000000..8439e111941c487205f24185c17c5f38a1ea7284 --- /dev/null +++ b/django/web/templates/web/passage/remove_keyword_from_passage.html @@ -0,0 +1,29 @@ +{% extends "web/base.html" %} +{% load static i18n %} +{% load formica %} + +{% block content %} + <h1 class="mt-6 u-center headline-4 font-alt"> + {{ passage }} + </h1> + + <div class="content"> + <form + action="." + method="post"> + {% form "web/formica/base_form.html" remove_keyword_from_passage_form %} + {% fields %} + {% endform %} + <div class="form-section u-flex u-justify-space-between"> + <a href="{{ passage.get_absolute_url }}" class="btn u-inline-block"> + {% translate "Cancel" %} + </a> + <button class="btn-info u-inline-block" type="submit"> + {% translate "Submit & save" %} + </button> + </div> + </form> + </div> + +{% endblock %} + diff --git a/django/web/templates/web/scholium.html b/django/web/templates/web/scholium.html index aca069d688e19f831fe19c1ae22a2b2632897500..814d6cb3ddc4e3bf530087c6b853139ef3cd0cd9 100644 --- a/django/web/templates/web/scholium.html +++ b/django/web/templates/web/scholium.html @@ -1,26 +1,27 @@ -<html> +{% extends "web/base.html" %} +{% load static i18n %} -<body> +{% block content %} + <h1 class="mt-6 u-center headline-4 font-alt"> + {% translate "Scholium" %} {{ book.number }}.{{ passage.fragment }}{{ passage.sub_fragment }}.{{ scholium.number }} + </h1> + <h2 class="mt-2 mb-1 u-center headline-6 font-alt"> + <a href="{% url 'web:passage-detail' book.number passage.fragment passage.sub_fragment %}"> + {{ passage }} + </a> + </h2> - <h1>Scholium - {{scholium.passage.book.number}}.{{scholium.passage.fragment}}{{scholium.passage.sub_fragment}}.{{scholium.number}} - id: {{scholium.pk}}</h1> - {% for text in scholium.texts.all %} - <li>{{text.text}} {{text.language}} : {{text.status}} </li> + {% include "web/passage/_metadata.html" with obj=scholium only %} + {% include "web/passage/_manuscripts.html" with manuscripts=scholium.manuscripts.all only %} + {% include "web/passage/_texts.html" with texts=scholium.texts.all add_text_to_passage_form=add_text_to_passage_form passage_pk=passage.pk csrf_token=csrf_token only %} - {%endfor%} - <li> Passage's creator: {{scholium.creator}}</li> - <li> Validation status: {{scholium.validation}}</li> - <li> Keywords: {%for keyword in passage.keywords.all%} - {%for description in keyword.descriptions.all%}{{description.description}} {%endfor%}{%endfor%}</li> - <li> Manuscript images: - {% for manuscript in scholium.manuscripts.all %} - <img src="{{manuscript.url}}"> - {% endfor %} - {% for description in manuscript.descriptions.all%} - {{description.description}} - {%endfor%} - </li> -</body> - -</html> \ No newline at end of file + {# TODO: pass pertinent forms and links to these reusable templates. #} + {% comment %} + {% include "web/passage/_cities.html" with cities=cities add_city_to_passage_form=add_city_to_passage_form passage_pk=passage.pk preferred_languages=preferred_languages csrf_token=csrf_token only %} + {% include "web/passage/_keywords.html" with keywords=keywords keywords_categories=keywords_categories add_keyword_to_passage_form=add_keyword_to_passage_form passage_pk=passage.pk preferred_languages=preferred_languages csrf_token=csrf_token only %} + {% include "web/passage/_comments.html" with comments=comments comment_create_form=comment_create_form passage_pk=passage.pk csrf_token=csrf_token only %} + {% include "web/passage/_alignments.html" with texts=texts only %} + {% include "web/passage/_internal_references.html" with internal_references=passage.internal_references.all only %} + {% include "web/passage/_external_references.html" with external_references=passage.external_references.all only %} + {% endcomment %} +{% endblock %} diff --git a/django/web/tests/test_add_comment_passage.py b/django/web/tests/test_add_comment_passage.py index 73eda444ccce697189b6a174af4f5c6684c238f8..a1cb041225be3e0dd9eddcb5f223a443655af5f8 100644 --- a/django/web/tests/test_add_comment_passage.py +++ b/django/web/tests/test_add_comment_passage.py @@ -1,28 +1,80 @@ from django.test import TestCase from django.urls import reverse -from meleager.models import Book, Comment, Description, Passage, Work -from web.forms.comment import CommentForm -from web.forms.description import DescriptionForm +from meleager.models import Book, Comment, Description, Passage -class AddCommentFormPassage(TestCase): - fixtures = ['test_data.json'] - def setUp(self): +class CommentsForms(TestCase): + fixtures = ["test_data.json"] + + @classmethod + def setUpTestData(cls): book = Book.objects.first() - self.passage = Passage.objects.get(book=book, fragment=42) + cls.passage = Passage.objects.get(book=book, fragment=12) + + def test_create_comment(self): + self.assertEquals(self.passage.comments.count(), 0) - def test_form(self): - response = self.client.post( - reverse("web:comment-add", args=(self.passage.pk,)), + self.client.post( + reverse("web:comment-create", args=(self.passage.pk,)), data={ - "comment_title": "TEST COMMENT", - "description": "TEST DESCRIPTION", + "comment_title": "comment_title", + "description": "description", "language": "fra", }, ) - self.assertEquals( - self.passage.comments.first().descriptions.first().description, - "TEST DESCRIPTION", + first_description = self.passage.comments.first().descriptions.first() + self.assertEquals(first_description.description, "description") + self.assertEquals(first_description.language.code, "fra") + + def test_update_comment(self): + comment = Comment.objects.create( + comment_title="comment_title", + ) + self.passage.comments.add(comment) + + description = Description.objects.create( + description="old_content", language_id="eng" ) + comment.descriptions.add(description) + self.assertEquals(comment.descriptions.first().description, "old_content") + self.assertEquals(comment.descriptions.first().language.code, "eng") + + self.client.post( + reverse( + "web:comment-update", + kwargs={"comment_pk": comment.pk, "passage_pk": self.passage.pk}, + ), + data={ + "description": "new_content", + "language": "spa", + }, + ) + + self.assertEquals(comment.descriptions.first().description, "new_content") + self.assertEquals(comment.descriptions.first().language.code, "spa") + + def test_delete_comment(self): + comment = Comment.objects.create( + comment_title="comment_title", + ) + self.passage.comments.add(comment) + + description = Description.objects.create( + description="description", language_id="eng" + ) + comment.descriptions.add(description) + self.assertEquals(self.passage.comments.count(), 1) + + self.client.post( + reverse( + "web:comment-delete", + kwargs={"comment_pk": comment.pk, "passage_pk": self.passage.pk}, + ), + data={ + "pk": description.pk, + }, + ) + + self.assertEquals(self.passage.comments.count(), 0) diff --git a/django/web/tests/test_add_description_passage.py b/django/web/tests/test_add_description_passage.py new file mode 100644 index 0000000000000000000000000000000000000000..7c0c54402ac681059e32c3d7e1dde42003671a51 --- /dev/null +++ b/django/web/tests/test_add_description_passage.py @@ -0,0 +1,78 @@ +from django.test import TestCase +from django.urls import reverse + +from meleager.models import Book, Description, Passage + + +class DescriptionsForms(TestCase): + fixtures = ["test_data.json"] + + @classmethod + def setUpTestData(cls): + book = Book.objects.first() + cls.passage = Passage.objects.get(book=book, fragment=12) + cls.passage.descriptions.clear() + + def test_create_description(self): + self.assertEquals(self.passage.descriptions.count(), 0) + + self.client.post( + reverse("web:description-create", args=(self.passage.pk,)), + data={ + "description": "description", + "language": "fra", + }, + ) + + first_description = self.passage.descriptions.first() + self.assertEquals(first_description.description, "description") + self.assertEquals(first_description.language.code, "fra") + + def test_update_description(self): + description = Description.objects.create( + description="old_content", language_id="eng" + ) + self.passage.descriptions.add(description) + old_first_description = self.passage.descriptions.first() + self.assertEquals(old_first_description.description, "old_content") + self.assertEquals(old_first_description.language.code, "eng") + + self.client.post( + reverse( + "web:description-update", + kwargs={ + "description_pk": description.pk, + "passage_pk": self.passage.pk, + }, + ), + data={ + "description": "new_content", + "language": "spa", + }, + ) + + new_first_description = self.passage.descriptions.first() + self.assertEquals(new_first_description.description, "new_content") + self.assertEquals(new_first_description.language.code, "spa") + + def test_delete_description(self): + description = Description.objects.create( + description="description", language_id="eng" + ) + self.passage.descriptions.add(description) + self.assertEquals(self.passage.descriptions.count(), 1) + + self.client.post( + reverse( + "web:description-delete", + kwargs={ + "description_pk": description.pk, + "passage_pk": self.passage.pk, + }, + ), + data={ + "pk": description.pk, + }, + ) + + self.assertEquals(self.passage.descriptions.count(), 0) diff --git a/django/web/tests/test_passage.py b/django/web/tests/test_passage.py index b2b6bdc0aedf3f9b7405c3b49f160c94e085a3df..858c96a074ee9af5d8c40d45fac1216a2769fe6a 100644 --- a/django/web/tests/test_passage.py +++ b/django/web/tests/test_passage.py @@ -1,7 +1,4 @@ from django.test import TestCase -from meleager.models import Book, Work -from web.models import APPassage, APScholium - class TestURNPassage(TestCase): fixtures = ["test_data.json"] @@ -9,13 +6,13 @@ class TestURNPassage(TestCase): def test_homepage_ok(self): resp = self.client.get("/") self.assertEqual(resp.status_code, 200) - self.assertContains(resp, "12.42") - self.assertContains(resp, "12.12abc") + self.assertContains(resp, "42.12") + self.assertContains(resp, "42.69abc") def test_passage1_ok(self): - resp = self.client.get("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:12.42") + resp = self.client.get("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:42.12/") self.assertEqual(resp.status_code, 200) def test_passage2_ok(self): - resp = self.client.get("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:12.12abc") + resp = self.client.get("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:42.69abc/") self.assertEqual(resp.status_code, 200) diff --git a/django/web/urls.py b/django/web/urls.py deleted file mode 100644 index 90d3327ac790da09c0528a71cc446b5f3279fab2..0000000000000000000000000000000000000000 --- a/django/web/urls.py +++ /dev/null @@ -1,47 +0,0 @@ -from django.urls import path, re_path - -from .views import index_view -from .views.authors import AuthorCreate, AuthorDetail, AuthorList, AuthorUpdate -from .views.cities import CityCreate, CityDetail, CityList, CityUpdate -from .views.comments import add_comment_from_passage -from .views.passage import PassageDetail, PassageUpdate, AddAuthorToPassage, AddKeywordToPassage, passage_detail_pk -from .views.scholium import scholium_view -from .views.texts import AddTextFromPassage - -app_name = "web" - -urlpatterns = [ - path("", index_view, name="index"), - re_path( - "passages/urn:cts:greekLit:tlg7000.tlg001.ag:(?P<book>\d+).(?P<fragment>\d+)(?P<sub_fragment>[^/]*)/", - PassageDetail.as_view(), - name="passage-detail", - ), - path("passages/<int:passage_pk>/", passage_detail_pk, name="passage-detail-pk"), - path( - "passages/urn:cts:greekLit:tlg5011.tlg001.sag:<int:book>.<int:fragment>.<int:number>/", - scholium_view, - name="scholium", - ), - path("passages/<int:pk>/edit/", PassageUpdate.as_view(), name="passage-update"), - path("passages/<int:passage_pk>/authors/edit/", AddAuthorToPassage.as_view(), name="passage-add-author"), - path("passages/<int:passage_pk>/keywords/edit/", AddKeywordToPassage.as_view(), name="passage-add-keyword"), - path( - "comments/add/passage/<int:passage_pk>/", - add_comment_from_passage, - name="comment-add", - ), - path( - "texts/add/passage/<int:passage_pk>/", - AddTextFromPassage.as_view(), - name="text-add", - ), - path("authors/", AuthorList.as_view(), name="author-list"), - path("authors/new/", AuthorCreate.as_view(), name="author-update"), - path("authors/<int:pk>/", AuthorDetail.as_view(), name="author-detail"), - path("authors/<int:pk>/edit/", AuthorUpdate.as_view(), name="author-update"), - path("cities/", CityList.as_view(), name="city-list"), - path("cities/new/", CityCreate.as_view(), name="city-update"), - path("cities/<int:pk>/", CityDetail.as_view(), name="city-detail"), - path("cities/<int:pk>/edit/", CityUpdate.as_view(), name="city-update"), -] diff --git a/django/web/urls/__init__.py b/django/web/urls/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..116632385eaa5c7da3a379b189b832ad24853dd4 --- /dev/null +++ b/django/web/urls/__init__.py @@ -0,0 +1,24 @@ +from django.urls import path, include + +from ..views import index_view +from .cities import urlpatterns as cities_urls +from .passages import urlpatterns as passages_urls +from .descriptions import urlpatterns as descriptions_urls +from .authors import urlpatterns as authors_urls +from .comments import urlpatterns as comments_urls +from .keywords import urlpatterns as keywords_urls +from .texts import urlpatterns as texts_urls + + +app_name = "web" + +urlpatterns = [ + path("", index_view, name="index"), + path("cities/", include(cities_urls)), + path("passages/", include(passages_urls)), + path("descriptions/", include(descriptions_urls)), + path("authors/", include(authors_urls)), + path("comments/", include(comments_urls)), + path("keywords/", include(keywords_urls)), + path("texts/", include(texts_urls)), +] diff --git a/django/web/urls/authors.py b/django/web/urls/authors.py new file mode 100644 index 0000000000000000000000000000000000000000..247a2c210e38d70259004187ee3d943c6a012cf3 --- /dev/null +++ b/django/web/urls/authors.py @@ -0,0 +1,27 @@ +from django.urls import path + +from ..views.authors import ( + AuthorCreate, + AuthorDetail, + AuthorList, + AuthorUpdate, + add_author_to_passage, + remove_author_from_passage, +) + +urlpatterns = [ + path( + "add/passages/<int:passage_pk>/", + add_author_to_passage, + name="passage-add-author", + ), + path( + "remove/passages/<int:passage_pk>/", + remove_author_from_passage, + name="passage-remove-author", + ), + path("", AuthorList.as_view(), name="author-list"), + path("new/", AuthorCreate.as_view(), name="author-update"), + path("<int:pk>/", AuthorDetail.as_view(), name="author-detail"), + path("<int:pk>/edit/", AuthorUpdate.as_view(), name="author-update"), +] diff --git a/django/web/urls/cities.py b/django/web/urls/cities.py new file mode 100644 index 0000000000000000000000000000000000000000..4fef12f00084c5d1e23190a112df47ac4a3136ee --- /dev/null +++ b/django/web/urls/cities.py @@ -0,0 +1,27 @@ +from django.urls import path + +from ..views.cities import ( + CityCreate, + CityDetail, + CityList, + CityUpdate, + add_city_to_passage, + remove_city_from_passage, +) + +urlpatterns = [ + path( + "add/passage/<int:passage_pk>/", + add_city_to_passage, + name="passage-add-city", + ), + path( + "remove/passage/<int:passage_pk>/", + remove_city_from_passage, + name="passage-remove-city", + ), + path("", CityList.as_view(), name="city-list"), + path("new/", CityCreate.as_view(), name="city-update"), + path("<int:pk>/", CityDetail.as_view(), name="city-detail"), + path("<int:pk>/edit/", CityUpdate.as_view(), name="city-update"), +] diff --git a/django/web/urls/comments.py b/django/web/urls/comments.py new file mode 100644 index 0000000000000000000000000000000000000000..6f8c932fb31be601c171e5de1652c66de27a1460 --- /dev/null +++ b/django/web/urls/comments.py @@ -0,0 +1,26 @@ +from django.urls import path +from ..views.comments import ( + comment_create, + comment_delete, + comment_update, + CommentDetail, +) + +urlpatterns = [ + path( + "<int:passage_pk>/create/", + comment_create, + name="comment-create", + ), + path( + "<int:passage_pk>/<int:comment_pk>/update/", + comment_update, + name="comment-update", + ), + path( + "<int:passage_pk>/<int:comment_pk>/delete/", + comment_delete, + name="comment-delete", + ), + path("<int:pk>/", CommentDetail.as_view(), name="comment-detail"), +] diff --git a/django/web/urls/descriptions.py b/django/web/urls/descriptions.py new file mode 100644 index 0000000000000000000000000000000000000000..7e5a2898c0f2c2fe34408978bd63d776230dc59c --- /dev/null +++ b/django/web/urls/descriptions.py @@ -0,0 +1,25 @@ +from django.urls import path + +from ..views.descriptions import ( + description_create, + description_delete, + description_update, +) + +urlpatterns = [ + path( + "<int:passage_pk>/create/", + description_create, + name="description-create", + ), + path( + "<int:passage_pk>/<int:description_pk>/update/", + description_update, + name="description-update", + ), + path( + "<int:passage_pk>/<int:description_pk>/delete/", + description_delete, + name="description-delete", + ), +] diff --git a/django/web/urls/keywords.py b/django/web/urls/keywords.py new file mode 100644 index 0000000000000000000000000000000000000000..03de00b356693f189e934d795d8682d701d1fbe9 --- /dev/null +++ b/django/web/urls/keywords.py @@ -0,0 +1,16 @@ +from django.urls import path + +from ..views.keywords import add_keyword_to_passage, remove_keyword_from_passage + +urlpatterns = [ + path( + "add/<int:passage_pk>/", + add_keyword_to_passage, + name="passage-add-keyword", + ), + path( + "remove/<int:passage_pk>/", + remove_keyword_from_passage, + name="passage-remove-keyword", + ), +] diff --git a/django/web/urls/passages.py b/django/web/urls/passages.py new file mode 100644 index 0000000000000000000000000000000000000000..a3d12e88c5b93e41d705090565d2093a14aeaa8d --- /dev/null +++ b/django/web/urls/passages.py @@ -0,0 +1,22 @@ +from django.urls import path, re_path +from ..views.passage import ( + PassageDetail, + PassageUpdate, + passage_detail_pk, +) +from ..views.scholium import scholium_view + +urlpatterns = [ + re_path( + "urn:cts:greekLit:tlg7000.tlg001.ag:(?P<book>\d+).(?P<fragment>\d+)(?P<sub_fragment>[^/]*)/", + PassageDetail.as_view(), + name="passage-detail", + ), + path("<int:passage_pk>/", passage_detail_pk, name="passage-detail-pk"), + path( + "urn:cts:greekLit:tlg5011.tlg001.sag:<int:book>.<int:fragment>.<int:number>/", + scholium_view, + name="scholium", + ), + path("<int:pk>/edit/", PassageUpdate.as_view(), name="passage-update"), +] diff --git a/django/web/urls/texts.py b/django/web/urls/texts.py new file mode 100644 index 0000000000000000000000000000000000000000..7a437dc23c7649996dfa4bb502804b0f5d6dc1fd --- /dev/null +++ b/django/web/urls/texts.py @@ -0,0 +1,11 @@ +from django.urls import path + +from ..views.texts import AddTextToPassage + +urlpatterns = [ + path( + "add/passage/<int:passage_pk>/", + AddTextToPassage.as_view(), + name="text-add", + ), +] diff --git a/django/web/views/authors.py b/django/web/views/authors.py index da7f10c450ed225b33c9961c09ea02b224b49a3e..b6cacb1c0bf59b4b9ffd8a7d00939b0587cc7b16 100644 --- a/django/web/views/authors.py +++ b/django/web/views/authors.py @@ -1,8 +1,9 @@ -from django import forms -from django.shortcuts import get_object_or_404, render +from django.shortcuts import get_object_or_404, render, redirect from django.views import generic -from meleager.models import Author, Name + +from meleager.models import Author, Passage from web.forms.author import AuthorForm +from web.forms.passage import AddAuthorToPassageForm, RemoveFromPassageForm class AuthorCreate(generic.CreateView): @@ -28,3 +29,63 @@ class AuthorList(generic.ListView): model = Author template_name = "web/author/list.html" context_object_name = "authors" + + +def add_author_to_passage(request, passage_pk): + passage = get_object_or_404(Passage, pk=passage_pk) + if request.method == "GET": + return render( + request, + "web/passage/add_author_to_passage.html", + { + "passage": passage, + "add_author_to_passage_form": AddAuthorToPassageForm(), + }, + ) + + add_author_to_passage_form = AddAuthorToPassageForm(request.POST) + + if add_author_to_passage_form.is_valid(): + author_pk = add_author_to_passage_form.cleaned_data.get("author") + author = Author.objects.get(pk=author_pk) + passage.authors.add(author) + return redirect(f"{passage.get_absolute_url()}#authors") + else: + return render( + request, + "web/passage/add_author_to_passage.html", + { + "passage": passage, + "add_author_to_passage_form": add_author_to_passage_form, + }, + ) + + +def remove_author_from_passage(request, passage_pk): + passage = get_object_or_404(Passage, pk=passage_pk) + if request.method == "GET": + return render( + request, + "web/passage/remove_author_from_passage_form.html", + { + "passage": passage, + "remove_author_from_passage_form": RemoveFromPassageForm(), + }, + ) + + remove_author_from_passage_form = RemoveFromPassageForm(request.POST) + + if remove_author_from_passage_form.is_valid(): + author_pk = remove_author_from_passage_form.cleaned_data.get("pk") + author = Author.objects.get(pk=author_pk) + passage.authors.remove(author) + return redirect(f"{passage.get_absolute_url()}#authors") + else: + return render( + request, + "web/passage/remove_author_from_passage_form.html", + { + "passage": passage, + "remove_author_from_passage_form": remove_author_from_passage_form, + }, + ) diff --git a/django/web/views/cities.py b/django/web/views/cities.py index 4069672229aeeeb4702af57bc021ac206f2d4d52..305f103ce9aef7ef972ab65bfbdad6178c2f0019 100644 --- a/django/web/views/cities.py +++ b/django/web/views/cities.py @@ -1,6 +1,69 @@ from django.views import generic -from meleager.models import City +from django.shortcuts import get_object_or_404, redirect, render + +from meleager.models import City, Passage from web.forms.city import CityForm +from web.forms.passage import AddCityToPassageForm, RemoveFromPassageForm + + +def add_city_to_passage(request, passage_pk): + passage = get_object_or_404(Passage, pk=passage_pk) + if request.method == "GET": + return render( + request, + "web/passage/add_city_to_passage.html", + { + "passage": passage, + "add_city_to_passage_form": AddCityToPassageForm(), + }, + ) + + add_city_to_passage_form = AddCityToPassageForm(request.POST) + + if add_city_to_passage_form.is_valid(): + city_pk = add_city_to_passage_form.cleaned_data.get("city") + city = City.objects.get(pk=city_pk) + passage.cities.add(city) + return redirect(f"{passage.get_absolute_url()}#cities") + else: + return render( + request, + "web/passage/add_city_to_passage.html", + { + "passage": passage, + "add_city_to_passage_form": add_city_to_passage_form, + }, + ) + + +def remove_city_from_passage(request, passage_pk): + passage = get_object_or_404(Passage, pk=passage_pk) + if request.method == "GET": + return render( + request, + "web/passage/remove_city_from_passage.html", + { + "passage": passage, + "remove_city_from_passage_form": RemoveFromPassageForm(), + }, + ) + + remove_city_from_passage_form = RemoveFromPassageForm(request.POST) + + if remove_city_from_passage_form.is_valid(): + city_pk = remove_city_from_passage_form.cleaned_data.get("pk") + city = City.objects.get(pk=city_pk) + passage.cities.remove(city) + return redirect(f"{passage.get_absolute_url()}#cities") + else: + return render( + request, + "web/passage/remove_city_from_passage.html", + { + "passage": passage, + "remove_city_from_passage_form": remove_city_from_passage_form, + }, + ) class CityList(generic.ListView): diff --git a/django/web/views/comments.py b/django/web/views/comments.py index e81c518870ad3faf5a9113536d47487c67372212..9e8af55ac4223f0b1befe34247a8e90d9ebfa9ae 100644 --- a/django/web/views/comments.py +++ b/django/web/views/comments.py @@ -1,32 +1,114 @@ from django.shortcuts import get_object_or_404, redirect, render +from django.views import generic + +from meleager.models import Comment, Description, Passage from web.forms.comment import CommentForm -from web.forms.comment_description import CommentDescriptionForm +from web.forms.comment_description import ( + CommentCreateForm, + CommentDeleteForm, + CommentUpdateForm, +) from web.forms.description import DescriptionForm -from web.models import APPassage -def add_comment_from_passage(request, passage_pk): - passage = get_object_or_404(APPassage, pk=passage_pk) +def comment_create(request, passage_pk): + passage = get_object_or_404(Passage, pk=passage_pk) if request.method == "GET": return render( request, - "web/add_comment_from_passage.html", - {"passage": passage, "comment_description_form": CommentDescriptionForm()}, + "web/passage/comment_create.html", + { + "passage": passage, + "comment_create_form": CommentCreateForm(), + }, ) description_form = DescriptionForm(request.POST) comment_form = CommentForm(request.POST) - comment_description_form = CommentDescriptionForm(request.POST) + comment_create_form = CommentCreateForm(request.POST) if description_form.is_valid() and comment_form.is_valid(): description = description_form.save() comment = comment_form.save() comment.descriptions.add(description) - passage = APPassage.objects.get(pk=passage_pk) passage.comments.add(comment) return redirect(f"{passage.get_absolute_url()}#comment-{comment.pk}") else: return render( request, - "web/add_comment_from_passage.html", - {"passage": passage, "comment_description_form": comment_description_form}, + "web/comment_create.html", + { + "passage": passage, + "comment_create_form": comment_create_form, + }, + ) + + +def comment_delete(request, passage_pk, comment_pk): + passage = get_object_or_404(Passage, pk=passage_pk) + if request.method == "GET": + return render( + request, + "web/passage/comment_delete.html", + { + "passage": passage, + "comment_delete": CommentDeleteForm(), + }, + ) + + comment = get_object_or_404(Comment, pk=comment_pk) + comment_delete_form = CommentDeleteForm(request.POST) + + if comment_delete_form.is_valid(): + description_pk = comment_delete_form.cleaned_data.get("pk") + description = Description.objects.get(pk=description_pk) + description.delete() + comment.delete() + return redirect(f"{passage.get_absolute_url()}#comments") + else: + return render( + request, + "web/passage/comment_delete.html", + { + "passage": passage, + "comment_delete_form": comment_delete_form, + }, ) + + +def comment_update(request, passage_pk, comment_pk): + passage = get_object_or_404(Passage, pk=passage_pk) + if request.method == "GET": + return render( + request, + "web/passage/comment_update.html", + { + "passage": passage, + "comment_update_form": CommentUpdateForm(), + }, + ) + + comment_update_form = CommentUpdateForm(request.POST) + passage = get_object_or_404(Passage, pk=passage_pk) + description_form = DescriptionForm(request.POST) + if description_form.is_valid(): + comment_obj = get_object_or_404(Comment, pk=comment_pk) + # TODO: handle multiple descriptions? + description_obj = comment_obj.descriptions.first() + description_obj.description = description_form.cleaned_data["description"] + description_obj.language = description_form.cleaned_data["language"] + description_obj.save() + return redirect(f"{passage.get_absolute_url()}#comment-{comment_pk}") + else: + return render( + request, + "web/comment_update.html", + { + "passage": passage, + "comment_update_form": comment_update_form, + }, + ) + + +class CommentDetail(generic.DetailView): + model = Comment + template_name = "web/comment/detail.html" diff --git a/django/web/views/descriptions.py b/django/web/views/descriptions.py new file mode 100644 index 0000000000000000000000000000000000000000..12698f72fc5046f324a45fc51899abbecf4b3a77 --- /dev/null +++ b/django/web/views/descriptions.py @@ -0,0 +1,100 @@ +from django.shortcuts import get_object_or_404, redirect, render + +from meleager.models import Description, Passage +from web.forms.passage_description import ( + DescriptionCreateForm, + DescriptionDeleteForm, + DescriptionUpdateForm, +) +from web.forms.description import DescriptionForm + + +def description_create(request, passage_pk): + passage = get_object_or_404(Passage, pk=passage_pk) + if request.method == "GET": + return render( + request, + "web/passage/description_create.html", + { + "passage": passage, + "description_create_form": DescriptionCreateForm(), + }, + ) + + description_create_form = DescriptionForm(request.POST) + + if description_create_form.is_valid(): + description = description_create_form.save() + passage.descriptions.add(description) + return redirect(f"{passage.get_absolute_url()}#description-{description.pk}") + else: + return render( + request, + "web/description_create.html", + { + "passage": passage, + "description_create_form": description_create_form, + }, + ) + + +def description_delete(request, passage_pk, description_pk): + passage = get_object_or_404(Passage, pk=passage_pk) + if request.method == "GET": + return render( + request, + "web/passage/description_delete.html", + { + "passage": passage, + "description_delete": DescriptionDeleteForm(), + }, + ) + + description_delete_form = DescriptionDeleteForm(request.POST) + + if description_delete_form.is_valid(): + description_pk = description_delete_form.cleaned_data.get("pk") + description = Description.objects.get(pk=description_pk) + description.delete() + return redirect(f"{passage.get_absolute_url()}#descriptions") + else: + return render( + request, + "web/passage/description_delete.html", + { + "passage": passage, + "description_delete_form": description_delete_form, + }, + ) + + +def description_update(request, passage_pk, description_pk): + passage = get_object_or_404(Passage, pk=passage_pk) + if request.method == "GET": + return render( + request, + "web/passage/description_update.html", + { + "passage": passage, + "description_update_form": DescriptionUpdateForm(), + }, + ) + + description_update_form = DescriptionUpdateForm(request.POST) + passage = get_object_or_404(Passage, pk=passage_pk) + description_form = DescriptionForm(request.POST) + if description_form.is_valid(): + description = Description.objects.get(pk=description_pk) + description.description = description_form.cleaned_data["description"] + description.language = description_form.cleaned_data["language"] + description.save() + return redirect(f"{passage.get_absolute_url()}#description-{description.pk}") + else: + return render( + request, + "web/description_update.html", + { + "passage": passage, + "description_update_form": description_update_form, + }, + ) diff --git a/django/web/views/keywords.py b/django/web/views/keywords.py index 1c028a93674312697238cc556d06a865c0d59a38..b4b1fc02ce35bd9248559e2d04763f19946837c5 100644 --- a/django/web/views/keywords.py +++ b/django/web/views/keywords.py @@ -1,7 +1,69 @@ -from django.shortcuts import render -from meleager.models import Keyword +from django.shortcuts import get_object_or_404, render, redirect + +from meleager.models import Keyword, Passage +from web.forms.passage import AddKeywordToPassageForm, RemoveFromPassageForm def keywords(request): keywords = Keyword.objects.all() return render(request, "web/keywords.html", {"keywords": keywords}) + + +def add_keyword_to_passage(request, passage_pk): + passage = get_object_or_404(Passage, pk=passage_pk) + if request.method == "GET": + return render( + request, + "web/passage/add_keyword_to_passage.html", + { + "passage": passage, + "add_keyword_to_passage_form": AddKeywordToPassageForm(), + }, + ) + + add_keyword_to_passage_form = AddKeywordToPassageForm(request.POST) + + if add_keyword_to_passage_form.is_valid(): + keyword_pk = add_keyword_to_passage_form.cleaned_data.get("keyword") + keyword = Keyword.objects.get(pk=keyword_pk) + passage.keywords.add(keyword) + return redirect(f"{passage.get_absolute_url()}#keywords") + else: + return render( + request, + "web/passage/add_keyword_to_passage.html", + { + "passage": passage, + "add_keyword_to_passage_form": add_keyword_to_passage_form, + }, + ) + + +def remove_keyword_from_passage(request, passage_pk): + passage = get_object_or_404(Passage, pk=passage_pk) + if request.method == "GET": + return render( + request, + "web/passage/remove_keyword_from_passage.html", + { + "passage": passage, + "remove_keyword_from_passage_form": RemoveFromPassageForm(), + }, + ) + + remove_keyword_from_passage_form = RemoveFromPassageForm(request.POST) + + if remove_keyword_from_passage_form.is_valid(): + keyword_pk = remove_keyword_from_passage_form.cleaned_data.get("pk") + keyword = Keyword.objects.get(pk=keyword_pk) + passage.keywords.remove(keyword) + return redirect(f"{passage.get_absolute_url()}#keywords") + else: + return render( + request, + "web/passage/remove_keyword_from_passage.html", + { + "passage": passage, + "remove_keyword_from_passage_form": remove_keyword_from_passage_form, + }, + ) diff --git a/django/web/views/passage.py b/django/web/views/passage.py index 5aef94a8f561f227fd5fb7871ed2f509269cf849..1edcd5db4fd62d28bf91be66ac1a74bbc5d39b18 100644 --- a/django/web/views/passage.py +++ b/django/web/views/passage.py @@ -1,20 +1,35 @@ -from django.shortcuts import get_object_or_404, render, redirect, reverse +from django.shortcuts import get_object_or_404, redirect from django.views import generic -from web.forms.comment_description import CommentDescriptionForm -from web.forms.passage import PassageForm, AddAuthorToPassageForm, AddKeywordToPassageForm -from web.forms.text import TextForm -from web.models import APPassage -from meleager.models import Author, Keyword + +from meleager.models import Language, Passage +from web.forms.comment_description import ( + CommentCreateForm, + CommentUpdateForm, + CommentDeleteForm, +) +from web.forms.passage import ( + PassageForm, + AddAuthorToPassageForm, + RemoveFromPassageForm, + AddCityToPassageForm, + AddKeywordToPassageForm, +) +from web.forms.passage_description import ( + DescriptionCreateForm, + DescriptionUpdateForm, + DescriptionDeleteForm, +) +from web.forms.text import AddTextToPassageForm class PassageDetail(generic.DetailView): - model = APPassage - template_name = "web/passage.html" + model = Passage + template_name = "web/passage/passage.html" context_object_name = "passage" def get_object(self): obj = get_object_or_404( - APPassage, + Passage, book__number=self.kwargs.get("book"), fragment=self.kwargs.get("fragment"), sub_fragment=self.kwargs.get("sub_fragment", ""), @@ -24,80 +39,89 @@ class PassageDetail(generic.DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) passage = self.get_object() + context["book"] = passage.book + descriptions = passage.descriptions.all() + for description in descriptions: + description.update_form = DescriptionUpdateForm( + initial={ + "pk": description.pk, + "description": description.description, + "language": description.language, + } + ) + description.delete_form = DescriptionDeleteForm( + initial={ + "pk": description.pk, + } + ) + context["descriptions"] = descriptions + context["description_create_form"] = DescriptionCreateForm() + comments = passage.comments.all() for comment in comments: - # TODO: how to handle multiple descriptions? - for description in comment.descriptions.all(): - comment.form = CommentDescriptionForm( - initial={ - "comment_title": comment.comment_title, - "description": description.description, - "language": description.language, - } - ) + # TODO: handle multiple descriptions? + description = comment.descriptions.first() + comment.update_form = CommentUpdateForm( + initial={ + "pk": description.pk, + "description": description.description, + "language": description.language, + } + ) + comment.delete_form = CommentDeleteForm( + initial={ + "pk": description.pk, + } + ) context["comments"] = comments - context["comment_description_form"] = CommentDescriptionForm() - context["text_form"] = TextForm() - # Exclude texts without a text, otherwise forloop counters - # in templates would be inconsistent. - # TODISCUSS: maybe an import_ap issue? - context["texts"] = passage.texts.exclude(text="") + context["comment_create_form"] = CommentCreateForm() + + context["add_text_to_passage_form"] = AddTextToPassageForm() + + def _sort_grc_first(value): + return value.language.code != "grc" + + context["texts"] = sorted(passage.texts.all(), key=_sort_grc_first) + + authors = passage.authors.all() + for text in context["texts"]: + for author in authors: + for name in author.names.all(): + if name.language.code == text.language.code: + text.author = author + text.author_name = name + author.remove_form = RemoveFromPassageForm(initial={"pk": author.pk}) + context["authors"] = authors + context["add_author_to_passage_form"] = AddAuthorToPassageForm() + + cities = passage.cities.all() + for city in cities: + city.remove_form = RemoveFromPassageForm(initial={"pk": city.pk}) + context["cities"] = cities + context["add_city_to_passage_form"] = AddCityToPassageForm() + + keywords = passage.keywords.all() + for keyword in keywords: + keyword.remove_form = RemoveFromPassageForm(initial={"pk": keyword.pk}) + context["keywords"] = keywords + context["keywords_categories"] = {keyword.category for keyword in keywords} + context["add_keyword_to_passage_form"] = AddKeywordToPassageForm() + + context["preferred_languages"] = Language.objects.preferred() return context + def passage_detail_pk(request, passage_pk): - passage = get_object_or_404(APPassage, pk=passage_pk) - return redirect("web:passage-detail", passage.book.number, passage.fragment, passage.sub_fragment) + passage = get_object_or_404(Passage, pk=passage_pk) + return redirect( + "web:passage-detail", + passage.book.number, + passage.fragment, + passage.sub_fragment, + ) + class PassageUpdate(generic.UpdateView): - model = APPassage - template_name = "web/passage_form.html" + model = Passage + template_name = "web/passage/passage_form.html" form_class = PassageForm - -class AddRelationToPassage(generic.View): - related_model = None - related_model_form = None - relation_name = None - - def get(self, request, passage_pk): - passage = get_object_or_404(APPassage, pk=passage_pk) - - all_objects = self.related_model.objects.all() - objects_flat = all_objects.filter(names__language__preferred=True).values_list('pk', 'names__name', 'names__language__code') - - objects_nested = { - obj.pk: { - language: name - for language, name in obj.names.values_list('language__code', 'name') - } - for obj in all_objects - } - - return render( - request, - "web/passage/add_relation_to_passage.html", - {'passage': passage, 'objects_flat': objects_flat, 'objects_nested': objects_nested, 'form': self.related_model_form()} - ) - - def post(self, request, passage_pk): - form = self.related_model_form(request.POST) - passage = get_object_or_404(APPassage, pk=passage_pk) - - if form.is_valid(): - obj_pk = form.cleaned_data["entity_pk"] - getattr(passage, self.relation_name).add(obj_pk) - return redirect("web:passage-detail-pk", passage_pk) - else: - return render(request, "web/passage/add_relation_to_passage.html", - {'passage': passage, 'form_url': reverse(self.form_reverse_url, args=[passage.pk]), 'form': form}) - -class AddAuthorToPassage(AddRelationToPassage): - related_model = Author - related_model_form = AddAuthorToPassageForm - relation_name = "authors" - form_reverse_url = "web:passage-add-author" - -class AddKeywordToPassage(AddRelationToPassage): - related_model = Keyword - related_model_form = AddKeywordToPassageForm - relation_name = "keywords" - form_reverse_url = "web:passage-add-keyword" \ No newline at end of file diff --git a/django/web/views/scholium.py b/django/web/views/scholium.py index 129165703279fad42f4b9f2006bb41114455a1b8..eeeafc360a6c037eabaa329fbddb699e83cb39e8 100644 --- a/django/web/views/scholium.py +++ b/django/web/views/scholium.py @@ -1,13 +1,31 @@ from django.shortcuts import get_object_or_404, render -from web.models import APScholium + +from meleager.models import Language, Scholium +from web.forms.passage import AddKeywordToPassageForm def scholium_view(request, book, fragment, number): - obj = get_object_or_404( - APScholium, + scholium = get_object_or_404( + Scholium, passage__book__number=book, passage__fragment=fragment, number=number, ) + preferred_languages = Language.objects.preferred() + keywords = scholium.keywords.all() + keywords_categories = {keyword.category for keyword in keywords} + add_keyword_to_passage_form = AddKeywordToPassageForm() - return render(request, "web/scholium.html", {"scholium": obj}) + return render( + request, + "web/scholium.html", + { + "book": scholium.passage.book, + "passage": scholium.passage, + "scholium": scholium, + "preferred_languages": preferred_languages, + "keywords": keywords, + "keywords_categories": keywords_categories, + "add_keyword_to_passage_form": add_keyword_to_passage_form, + }, + ) diff --git a/django/web/views/texts.py b/django/web/views/texts.py index 813ac97110d4f94c4bbddde9f7a0528e24512a60..c15cd6cbcbc27ba1ec8f5bb4dd0b249c12484136 100644 --- a/django/web/views/texts.py +++ b/django/web/views/texts.py @@ -1,29 +1,33 @@ from django.shortcuts import get_object_or_404, redirect, render from django.views import View -from web.forms.text import TextForm -from web.models import APPassage +from web.forms.text import AddTextToPassageForm +from meleager.models import Passage -class AddTextFromPassage(View): + +class AddTextToPassage(View): def get(self, request, passage_pk): - passage = get_object_or_404(APPassage, pk=passage_pk) + passage = get_object_or_404(Passage, pk=passage_pk) return render( request, - "web/add_text_from_passage.html", - {"passage": passage, "text_form": TextForm()}, + "web/passage/add_text_to_passage.html", + {"passage": passage, "add_text_to_passage_form": AddTextToPassageForm()}, ) def post(self, request, passage_pk): - text_form = TextForm(request.POST) - passage = get_object_or_404(APPassage, pk=passage_pk) - if text_form.is_valid(): - text = text_form.save() + add_text_to_passage_form = AddTextToPassageForm(request.POST) + passage = get_object_or_404(Passage, pk=passage_pk) + if add_text_to_passage_form.is_valid(): + text = add_text_to_passage_form.save() passage.texts.add(text) return redirect(f"{passage.get_absolute_url()}#texts") else: return render( request, - "web/add_text_from_passage.html", - {"passage": passage, "text_form": text_form}, + "web/passage/add_text_to_passage.html", + { + "passage": passage, + "add_text_to_passage_form": add_text_to_passage_form, + }, ) diff --git a/integration/requirements.txt b/integration/requirements.txt index e1eafae51869c93c126b2c28ae45c890cc3e8e2c..5548ed0c76f1fdd388818a46656200a6df6eca6b 100644 --- a/integration/requirements.txt +++ b/integration/requirements.txt @@ -1,3 +1,3 @@ -playwright==0.162.1 +playwright==1.8.0a1 pytest==6.1.2 -pytest-playwright==0.0.8 +pytest-playwright==0.0.11 diff --git a/integration/test_alignments_hover.py b/integration/test_alignments_hover.py index ce733218a69a5d30f2f9f7b141502ba0f9b75c09..45417a93cfef973ae6e01d660e61c152eabe79bb 100644 --- a/integration/test_alignments_hover.py +++ b/integration/test_alignments_hover.py @@ -1,42 +1,42 @@ def test_hovering_original_highlight_translation(page): - page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70") - original = page.waitForSelector( - '#alignments-tabs #alignment-content-1-ENG #align-original-1 blockquote [data-id="align-1-[2]"]' + page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70/") + original = page.wait_for_selector( + '#alignments-tabs #alignment-content-1-eng #align-original-1 blockquote [data-id="align-1-[2]"]' ) - translation = page.waitForSelector( - '#alignments-tabs #alignment-content-1-ENG #align-translation-1 blockquote [data-id="align-1-[2]"]' + translation = page.wait_for_selector( + '#alignments-tabs #alignment-content-1-eng #align-translation-1 blockquote [data-id="align-1-[2]"]' ) - assert original.getAttribute("class") is None - assert translation.getAttribute("class") is None + assert original.get_attribute("class") is None + assert translation.get_attribute("class") is None original.hover() - assert original.getAttribute("class") == "tag tag--warning tag--normal" - assert translation.getAttribute("class") == "tag tag--warning tag--normal" + assert original.get_attribute("class") == "tag tag--warning tag--normal" + assert translation.get_attribute("class") == "tag tag--warning tag--normal" # Cannot find how to un-hover, so let’s hover another element… - page.waitForSelector( - '#alignments-tabs #alignment-content-1-ENG #align-original-1 blockquote [data-id="align-1-[3]"]' + page.wait_for_selector( + '#alignments-tabs #alignment-content-1-eng #align-original-1 blockquote [data-id="align-1-[3]"]' ).hover() - assert original.getAttribute("class") == "" - assert translation.getAttribute("class") == "" + assert original.get_attribute("class") == "" + assert translation.get_attribute("class") == "" def test_hovering_original_highlight_translation_from_another_tab(page): - page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70") - tab = page.waitForSelector('#alignments-tabs [href="#alignment-content-2-FRA"]') + page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70/") + tab = page.wait_for_selector('#alignments-tabs [href="#alignment-content-2-fra"]') tab.click() - original = page.waitForSelector( - '#alignments-tabs #alignment-content-2-FRA #align-original-2 blockquote [data-id="align-2-[2]"]' + original = page.wait_for_selector( + '#alignments-tabs #alignment-content-2-fra #align-original-2 blockquote [data-id="align-2-[2]"]' ) - translation = page.waitForSelector( - '#alignments-tabs #alignment-content-2-FRA #align-translation-2 blockquote [data-id="align-2-[2]"]' + translation = page.wait_for_selector( + '#alignments-tabs #alignment-content-2-fra #align-translation-2 blockquote [data-id="align-2-[2]"]' ) - assert original.getAttribute("class") is None - assert translation.getAttribute("class") is None + assert original.get_attribute("class") is None + assert translation.get_attribute("class") is None original.hover() - assert original.getAttribute("class") == "tag tag--warning tag--normal" - assert translation.getAttribute("class") == "tag tag--warning tag--normal" + assert original.get_attribute("class") == "tag tag--warning tag--normal" + assert translation.get_attribute("class") == "tag tag--warning tag--normal" # Cannot find how to un-hover, so let’s hover another element… - page.waitForSelector( - '#alignments-tabs #alignment-content-2-FRA #align-original-2 blockquote [data-id="align-2-[3]"]' + page.wait_for_selector( + '#alignments-tabs #alignment-content-2-fra #align-original-2 blockquote [data-id="align-2-[3]"]' ).hover() - assert original.getAttribute("class") == "" - assert translation.getAttribute("class") == "" + assert original.get_attribute("class") == "" + assert translation.get_attribute("class") == "" diff --git a/integration/test_alignments_tabs.py b/integration/test_alignments_tabs.py index f384d117c1345c6e43e97bf7e14c171c942b19c5..a667f8d365408d00a42acfb91bf887fc4c445412 100644 --- a/integration/test_alignments_tabs.py +++ b/integration/test_alignments_tabs.py @@ -1,15 +1,15 @@ def test_first_tab_is_selected(page): - page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70") + page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70/") assert ( - page.textContent("#alignments-tabs .tab-container .selected").strip() == "ENG" + page.text_content("#alignments-tabs .tab-container .selected").strip() == "eng" ) def test_first_content_is_visible(page): - page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70") + page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70/") assert ( - page.innerText( - "#alignments-tabs #alignment-content-1-ENG #align-original-1 blockquote" + page.inner_text( + "#alignments-tabs #alignment-content-1-eng #align-original-1 blockquote" ) .strip() .startswith("νῦν πλÎον á¼¢ τὸ πάÏοιθε") @@ -17,15 +17,15 @@ def test_first_content_is_visible(page): def test_click_on_french_tab(page): - page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70") - tab = page.waitForSelector('#alignments-tabs [href="#alignment-content-2-FRA"]') + page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70/") + tab = page.wait_for_selector('#alignments-tabs [href="#alignment-content-2-fra"]') tab.click() assert ( - page.textContent("#alignments-tabs .tab-container .selected").strip() == "FRA" + page.text_content("#alignments-tabs .tab-container .selected").strip() == "fra" ) assert ( - page.innerText( - "#alignments-tabs #alignment-content-2-FRA #align-translation-2 blockquote" + page.inner_text( + "#alignments-tabs #alignment-content-2-fra #align-translation-2 blockquote" ) .strip() .startswith("Maintenant plus qu ' auparavant , garde , triple chien") diff --git a/integration/test_authors.py b/integration/test_authors.py new file mode 100644 index 0000000000000000000000000000000000000000..b992f1b68748f3559a39ac0292fe82ca34e42cc6 --- /dev/null +++ b/integration/test_authors.py @@ -0,0 +1,10 @@ +def test_submitting_author_form(page): + page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.71/") + page.hover("#authors h2") + page.click("#authors h2 a >> text='Add'") + modal = page.wait_for_selector("#author-new") + author_select = modal.wait_for_selector("#id_author") + author_select.select_option(label="Adaeus, ᾿Αδαῖος, Addée ou Adaios de Macédoine") + submit_button = modal.wait_for_selector("button[type=submit]") + # submit_button.click() + # TODO: find the correct Playwright assertion in this context. diff --git a/integration/test_cities.py b/integration/test_cities.py new file mode 100644 index 0000000000000000000000000000000000000000..0c26fefb15788342a3648345eda6ba2554e7b9f7 --- /dev/null +++ b/integration/test_cities.py @@ -0,0 +1,10 @@ +def test_submitting_city_form(page): + page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.71/") + page.hover("#cities h2") + page.click("#cities h2 a >> text='Add'") + modal = page.wait_for_selector("#city-new") + keyword_select = modal.wait_for_selector("#id_city") + keyword_select.select_option(label="Myrina, Myrina, ΜυÏίνα") + submit_button = modal.wait_for_selector("button[type=submit]") + # submit_button.click() + # TODO: find the correct Playwright assertion in this context. diff --git a/integration/test_comments.py b/integration/test_comments.py index 4f11f06803ab0586b9bfbe2e74cd115c20ba82d9..097b70ad8fa3c7743c445e737478f779e47111ac 100644 --- a/integration/test_comments.py +++ b/integration/test_comments.py @@ -1,76 +1,79 @@ def test_open_close_comment_form_modal(page): - page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70") + page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70/") # The opacity of the modal should be 0 by default. - modal = page.waitForSelector("#comment-new") + modal = page.wait_for_selector("#comment-create") opacity = modal.evaluate("e => window.getComputedStyle(e).opacity") assert opacity == "0" - page.click("a >> text=/.*Add a new comment/") + page.hover("#comments h2") + page.click("#comments h2 a >> text='Add'") # Manually waiting for the .3s CSS transition. - page.waitForTimeout(350) + page.wait_for_timeout(350) # Once opened, the opacity is set to 1. - modal = page.waitForSelector("#comment-new") + modal = page.wait_for_selector("#comment-create") opacity = modal.evaluate("e => window.getComputedStyle(e).opacity") assert opacity == "1" assert ( - modal.waitForSelector(".modal-title").textContent().strip() + modal.wait_for_selector(".modal-title").text_content().strip() == "Add a new comment" ) # The Cancel link is closing the modal. - cancel_link = modal.waitForSelector("text=Cancel") + cancel_link = modal.wait_for_selector("text=Cancel") cancel_link.click() # Manually waiting for the .3s CSS transition. - page.waitForTimeout(350) + page.wait_for_timeout(350) - modal = page.waitForSelector("#comment-new") + modal = page.wait_for_selector("#comment-create") opacity = modal.evaluate("e => window.getComputedStyle(e).opacity") assert opacity == "0" def test_open_close_with_escape_key_comment_form_modal(page): - page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70") + page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70/") # The opacity of the modal should be 0 by default. - modal = page.waitForSelector("#comment-new") + modal = page.wait_for_selector("#comment-create") opacity = modal.evaluate("e => window.getComputedStyle(e).opacity") assert opacity == "0" - page.click("a >> text=/.*Add a new comment/") + page.hover("#comments h2") + page.click("#comments h2 a >> text='Add'") # Manually waiting for the .3s CSS transition. - page.waitForTimeout(350) + page.wait_for_timeout(350) # Once opened, the opacity is set to 1. - modal = page.waitForSelector("#comment-new") + modal = page.wait_for_selector("#comment-create") opacity = modal.evaluate("e => window.getComputedStyle(e).opacity") assert opacity == "1" assert ( - modal.waitForSelector(".modal-title").textContent().strip() + modal.wait_for_selector(".modal-title").text_content().strip() == "Add a new comment" ) # The escape key is closing the modal. page.keyboard.press("Escape") # Manually waiting for the .3s CSS transition. - page.waitForTimeout(350) + page.wait_for_timeout(350) - modal = page.waitForSelector("#comment-new") + modal = page.wait_for_selector("#comment-create") opacity = modal.evaluate("e => window.getComputedStyle(e).opacity") assert opacity == "0" def test_submitting_comment_form(page): - page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70") - page.click("a >> text=/.*Add a new comment/") - modal = page.waitForSelector("#comment-new") - title_input = modal.waitForSelector("#id_comment_title") + page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70/") + page.hover("#comments h2") + page.click("#comments h2 a >> text='Add'") + modal = page.wait_for_selector("#comment-create") + title_input = modal.wait_for_selector("#id_comment_title") title_input.fill("Title") - description_textarea = modal.waitForSelector("#id_description") + description_textarea = modal.wait_for_selector("#id_description") description_textarea.fill("Description") - language_input = modal.waitForSelector("#id_language") - language_input.fill("fra") - submit_button = modal.waitForSelector("button[type=submit]") + language_select = modal.wait_for_selector("#id_language") + language_select.select_option("fra") + submit_button = modal.wait_for_selector("button[type=submit]") # submit_button.click() # TODO: find the correct Playwright assertion in this context. diff --git a/integration/test_default_interface.py b/integration/test_default_interface.py index 4cc11cef75b22c65ff290fb2e3b4af856d0be451..2635e75117fe964faafdc3c8828166bd425a5c6c 100644 --- a/integration/test_default_interface.py +++ b/integration/test_default_interface.py @@ -1,8 +1,13 @@ def test_site_title(page): - page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70") - assert page.textContent("header h1").strip() == "Anthologia Graeca" + page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70/") + assert page.text_content("header h1").strip() == "Anthologia Graeca" -def test_page_title(page): - page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70") - assert page.textContent("section h1").strip() == "Passage 7.70" +def test_passage_title(page): + page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70/") + assert page.text_content("section h1").strip() == "Passage 7.70" + + +def test_scholium_title(page): + page.goto("/passages/urn:cts:greekLit:tlg5011.tlg001.sag:7.70.1/") + assert page.text_content("section h1").strip() == "Scholium 7.70.1" diff --git a/integration/test_descriptions.py b/integration/test_descriptions.py new file mode 100644 index 0000000000000000000000000000000000000000..bd5bd7a00b9784bcb4e225cae3a01e24da0839de --- /dev/null +++ b/integration/test_descriptions.py @@ -0,0 +1,12 @@ +def test_submitting_description_form(page): + page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70/") + page.hover("#descriptions h2") + page.click("#descriptions h2 a >> text='Add'") + modal = page.wait_for_selector("#description-create") + description_textarea = modal.wait_for_selector("#id_description") + description_textarea.fill("Description") + language_select = modal.wait_for_selector("#id_language") + language_select.select_option("fra") + submit_button = modal.wait_for_selector("button[type=submit]") + # submit_button.click() + # TODO: find the correct Playwright assertion in this context. diff --git a/integration/test_keywords.py b/integration/test_keywords.py new file mode 100644 index 0000000000000000000000000000000000000000..fed756964bab13862d1b321ca1b20efd91f8f532 --- /dev/null +++ b/integration/test_keywords.py @@ -0,0 +1,10 @@ +def test_submitting_keyword_form(page): + page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.71/") + page.hover("#keywords h2") + page.click("#keywords h2 a >> text='Add'") + modal = page.wait_for_selector("#keyword-new") + keyword_select = modal.wait_for_selector("#id_keyword") + keyword_select.select_option(label="Apollo, Apollon, Phébus / PhÅ“bus") + submit_button = modal.wait_for_selector("button[type=submit]") + # submit_button.click() + # TODO: find the correct Playwright assertion in this context. diff --git a/integration/test_texts.py b/integration/test_texts.py new file mode 100644 index 0000000000000000000000000000000000000000..d02ee93c0aba9af96685714b5ec7f38201781e6e --- /dev/null +++ b/integration/test_texts.py @@ -0,0 +1,11 @@ +def test_submitting_text_form(page): + page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70/") + page.click("#texts-tabs-first a >> text='+ add'") + modal = page.wait_for_selector("#text-new") + title_input = modal.wait_for_selector("#id_text") + title_input.fill("Text content") + language_select = modal.wait_for_selector("#id_language") + language_select.select_option("fra") + submit_button = modal.wait_for_selector("button[type=submit]") + # submit_button.click() + # TODO: find the correct Playwright assertion in this context. diff --git a/integration/test_texts_tabs.py b/integration/test_texts_tabs.py index 2ae8ce6be28a1002a94b2947e93accb34c2baac1..eb69ae2b145e32288cd162d8c1c1d68109a61163 100644 --- a/integration/test_texts_tabs.py +++ b/integration/test_texts_tabs.py @@ -1,28 +1,28 @@ def test_first_tab_is_selected(page): - page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70") + page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70/") assert ( - page.textContent("#texts-tabs-first .tab-container .selected").strip() == "GRC" + page.text_content("#texts-tabs-first .tab-container .selected").strip() == "grc" ) def test_first_content_is_visible(page): - page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70") + page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70/") assert ( - page.textContent("#texts-tabs-first #text-content-first-1-GRC") + page.text_content("#texts-tabs-first #text-content-first-1-grc") .strip() .startswith("νῦν πλÎον á¼¢ τὸ πάÏοιθε") ) def test_click_on_french_tab(page): - page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70") - tab = page.waitForSelector('#texts-tabs-first [href="#text-content-first-2-FRA"]') + page.goto("/passages/urn:cts:greekLit:tlg7000.tlg001.ag:7.70/") + tab = page.wait_for_selector('#texts-tabs-first [href="#text-content-first-4-fra"]') tab.click() assert ( - page.textContent("#texts-tabs-first .tab-container .selected").strip() == "FRA" + page.text_content("#texts-tabs-first .tab-container .selected").strip() == "fra" ) assert ( - page.textContent("#texts-tabs-first #text-content-first-2-FRA") + page.text_content("#texts-tabs-first #text-content-first-4-fra") .strip() .startswith("Maintenant plus qu'auparavant") ) diff --git a/requirements.txt b/requirements.txt index 9e087904df311ad33c02a437d289b2fe40a214fa..985591692fcf46087e0ac370eba83317e00fcbb4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,15 @@ Django==3.1.1 djangorestframework==3.11.1 django-extensions==3.0.9 +django-filter==2.4.0 django-guardian==2.3.0 psycopg2-binary==2.8.6 django-simple-history==2.12.0 django-select2==7.4.2 +graphene-django>=2.0.0 iso-639 requests Sphinx==3.3.0 sphinxcontrib-django==0.5.1 sphinx-rtd-theme==0.5.0 -autodocsumm==0.2.1 \ No newline at end of file +autodocsumm==0.2.1