diff --git a/app/apps/api/serializers.py b/app/apps/api/serializers.py index 61532a14ac5367a20821b0fc24bcf713866e73e5..e69add839008d6dc165a8575f89a6b53b472510a 100644 --- a/app/apps/api/serializers.py +++ b/app/apps/api/serializers.py @@ -102,11 +102,15 @@ class DocumentSerializer(serializers.ModelSerializer): transcriptions = TranscriptionSerializer(many=True, read_only=True) valid_block_types = BlockTypeSerializer(many=True, read_only=True) valid_line_types = LineTypeSerializer(many=True, read_only=True) + parts_count = serializers.SerializerMethodField() class Meta: model = Document fields = ('pk', 'name', 'transcriptions', - 'valid_block_types', 'valid_line_types') + 'valid_block_types', 'valid_line_types', 'parts_count') + + def get_parts_count(self, document): + return document.parts.count() class PartSerializer(serializers.ModelSerializer): @@ -127,6 +131,7 @@ class PartSerializer(serializers.ModelSerializer): 'image', 'bw_image', 'workflow', + 'order', 'recoverable', 'transcription_progress' ) diff --git a/app/apps/api/views.py b/app/apps/api/views.py index 19fc37659b438eebc26ae297a4cfada6ccc9d976..23722ea8683c7e712fc6e4761100ada5eac108f5 100644 --- a/app/apps/api/views.py +++ b/app/apps/api/views.py @@ -4,7 +4,9 @@ import logging from django.conf import settings from django.core.exceptions import PermissionDenied from django.db.models import Prefetch +from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 +from django.urls import reverse from rest_framework.decorators import action from rest_framework.response import Response @@ -193,6 +195,22 @@ class PartViewSet(DocumentPermissionMixin, ModelViewSet): else: # list & create return PartSerializer + @action(detail=False, methods=['get']) + def byorder(self, request, document_pk=None): + try: + order = int(request.GET.get('order')) + except ValueError: + return Response({'error': 'invalid order.'}) + if not order: + return Response({'error': 'pass order as an url parameter.'}) + try: + part = self.get_queryset().get(order=order) + except DocumentPart.DoesNotExist: + return Response({'error': 'Out of bounds.'}) + return HttpResponseRedirect(reverse('api:part-detail', + kwargs={'document_pk': self.kwargs.get('document_pk'), + 'pk': part.pk})) + @action(detail=True, methods=['post']) def move(self, request, document_pk=None, pk=None): part = DocumentPart.objects.get(document=document_pk, pk=pk) diff --git a/app/apps/core/migrations/0043_auto_20210324_1016.py b/app/apps/core/migrations/0043_auto_20210324_1016.py new file mode 100644 index 0000000000000000000000000000000000000000..f95b68dbe5e845947915d0a9ea60764dc7b002be --- /dev/null +++ b/app/apps/core/migrations/0043_auto_20210324_1016.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2.19 on 2021-03-24 10:16 + +from django.db import migrations, models +import django.db.models.deletion + + +def forward(apps, se): + # delete any unbound model + OcrModel = apps.get_model('core', 'OcrModel') + OcrModel.objects.filter(document__isnull=True).delete() + + +def backward(apps, se): + pass + + +class Migration(migrations.Migration): + dependencies = [ + ('core', '0042_auto_20201009_1015'), + ] + + operations = [ + migrations.RunPython(forward, backward), + migrations.AlterField( + model_name='ocrmodel', + name='document', + field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='ocr_models', to='core.Document'), + ), + ] diff --git a/app/apps/core/models.py b/app/apps/core/models.py index aad2b42151b11b7c417bfdcdafa843a1ab8731c5..680f26f313b5a88b18c3f5133f0737c4cf062fef 100644 --- a/app/apps/core/models.py +++ b/app/apps/core/models.py @@ -244,6 +244,13 @@ class Document(models.Model): else: return 'ltr' + @cached_property + def last_edited_part(self): + try: + return self.parts.order_by('-updated_at')[0] + except IndexError: + return None + @property def training_model(self): return self.ocr_models.filter(training=True).first() diff --git a/app/apps/users/admin.py b/app/apps/users/admin.py index d44bb4caff795746b50e2ce697a1c71584637f90..f67807b6cbe7c604385310d9d2176dd4d68e2328 100644 --- a/app/apps/users/admin.py +++ b/app/apps/users/admin.py @@ -5,7 +5,6 @@ from django.contrib.auth.forms import UserChangeForm, UserCreationForm from django.contrib import messages from django.utils.translation import ngettext - from users.models import User, ResearchField, Invitation, ContactUs, GroupOwner @@ -15,8 +14,11 @@ class MyUserChangeForm(UserChangeForm): class MyUserCreationForm(UserCreationForm): + email = forms.EmailField() + class Meta(UserCreationForm.Meta): model = User + fields = UserCreationForm.Meta.fields + ('email',) def clean_username(self): username = self.cleaned_data['username'] @@ -24,7 +26,7 @@ class MyUserCreationForm(UserCreationForm): User.objects.get(username=username) except User.DoesNotExist: return username - raise forms.ValidationError(self.error_messages['duplicate_username']) + raise forms.ValidationError('Duplicate username.') class MyUserAdmin(UserAdmin): @@ -32,9 +34,13 @@ class MyUserAdmin(UserAdmin): add_form = MyUserCreationForm list_display = UserAdmin.list_display + ('last_login',) fieldsets = UserAdmin.fieldsets + ( - (None, {'fields': ('fields','onboarding')}), # second fields refers to research fields + (None, {'fields': ('fields', 'onboarding')}), # second fields refers to research fields + ) + add_fieldsets = ( + (None, { + 'fields': ('username', 'email', 'password1', 'password2')} + ), ) - class InvitationAdmin(admin.ModelAdmin): date_hierarchy = 'created_at' diff --git a/app/apps/users/migrations/0014_auto_20210324_1007.py b/app/apps/users/migrations/0014_auto_20210324_1007.py new file mode 100644 index 0000000000000000000000000000000000000000..b30d5f894fbeefe077fc286d8e48c67f5b8f0f8a --- /dev/null +++ b/app/apps/users/migrations/0014_auto_20210324_1007.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.19 on 2021-03-24 10:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0013_auto_20210106_1305'), + ] + + operations = [ + migrations.AlterModelOptions( + name='contactus', + options={'verbose_name': 'Contact message', 'verbose_name_plural': 'Contact messages'}, + ), + ] diff --git a/app/escriptorium/context_processors.py b/app/escriptorium/context_processors.py index 5c226b31c36778f6b5d5cb7d7de0e460a24fa622..fb733ea58179f4a6571e0843cf85720fea1a97bf 100644 --- a/app/escriptorium/context_processors.py +++ b/app/escriptorium/context_processors.py @@ -5,3 +5,6 @@ def enable_cookie_consent(request): return {'ENABLE_COOKIE_CONSENT': getattr(settings, 'ENABLE_COOKIE_CONSENT', True)} + +def custom_homepage(request): + return {'CUSTOM_HOME': getattr(settings, 'CUSTOM_HOME', False)} diff --git a/app/escriptorium/local_settings.py.example b/app/escriptorium/local_settings.py.example index 56d974991e957b565b652834acc7908e2271b7c5..d7337561f992b14c43c74383d592ee9316429fee 100644 --- a/app/escriptorium/local_settings.py.example +++ b/app/escriptorium/local_settings.py.example @@ -39,3 +39,8 @@ DEBUG_TOOLBAR_PANELS = [ ] # USE_CELERY = False +# CELERY_TASK_ALWAYS_EAGER = True + +# LOGGING['loggers']['kraken']['level'] = 'DEBUG' + +# CUSTOM_HOME = True diff --git a/app/escriptorium/settings.py b/app/escriptorium/settings.py index a019a3270f3f6688d5b2fb59b8093d763b7b487a..4f6471c79ba21d39cea427be34c4c4a6f3a7c5f9 100644 --- a/app/escriptorium/settings.py +++ b/app/escriptorium/settings.py @@ -41,6 +41,8 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = os.getenv('DEBUG', False) +CUSTOM_HOME = os.getenv('CUSTOM_HOME', False) + ALLOWED_HOSTS = ['*'] ASGI_APPLICATION = "escriptorium.routing.application" @@ -87,7 +89,8 @@ ROOT_URLCONF = 'escriptorium.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(PROJECT_ROOT, 'templates')], + 'DIRS': [os.path.join(PROJECT_ROOT, 'templates'), + os.path.join(BASE_DIR, 'homepage')], # custom homepage dir (volume in docker) 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -95,7 +98,8 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', - 'escriptorium.context_processors.enable_cookie_consent' + 'escriptorium.context_processors.enable_cookie_consent', + 'escriptorium.context_processors.custom_homepage' ], }, }, @@ -219,6 +223,7 @@ STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'static') STATICFILES_DIRS = [ os.path.join(PROJECT_ROOT, 'static'), + os.path.join(BASE_DIR, 'homepage'), # custom homepage directory FRONTEND_DIR ] diff --git a/app/escriptorium/static/images/ephe.png b/app/escriptorium/static/images/ephe.png new file mode 100644 index 0000000000000000000000000000000000000000..6856c3805d904a95eb64c4611840b829eb974b61 Binary files /dev/null and b/app/escriptorium/static/images/ephe.png differ diff --git a/app/escriptorium/static/images/escriptorium_hd.png b/app/escriptorium/static/images/escriptorium_hd.png new file mode 100644 index 0000000000000000000000000000000000000000..20afca793e0282bab0f11c4a26b83bdfbf94a990 Binary files /dev/null and b/app/escriptorium/static/images/escriptorium_hd.png differ diff --git a/app/escriptorium/static/images/favicon.ico b/app/escriptorium/static/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7278052495125e8ceba83c4ddbc80dade813f193 Binary files /dev/null and b/app/escriptorium/static/images/favicon.ico differ diff --git a/app/escriptorium/static/images/h2020.jpg b/app/escriptorium/static/images/h2020.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9977dd90d6a92066fb847a2823d1f71fe0799ddb Binary files /dev/null and b/app/escriptorium/static/images/h2020.jpg differ diff --git a/app/escriptorium/static/images/horizon.jpg b/app/escriptorium/static/images/horizon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1ef9d3b92b72e271613c1c228d7a0502c6afd899 Binary files /dev/null and b/app/escriptorium/static/images/horizon.jpg differ diff --git a/app/escriptorium/static/images/inria.png b/app/escriptorium/static/images/inria.png new file mode 100644 index 0000000000000000000000000000000000000000..b2997fd5a37783cd23d6962dfa692ee9773cbe0a Binary files /dev/null and b/app/escriptorium/static/images/inria.png differ diff --git a/app/escriptorium/static/images/openiti.png b/app/escriptorium/static/images/openiti.png new file mode 100644 index 0000000000000000000000000000000000000000..0be04e49b604f0ea013c52a317e93d28a7db4055 Binary files /dev/null and b/app/escriptorium/static/images/openiti.png differ diff --git a/app/escriptorium/static/images/psl.png b/app/escriptorium/static/images/psl.png new file mode 100644 index 0000000000000000000000000000000000000000..a5df9793088b1b6c6e78321a80abfe5b7ac57076 Binary files /dev/null and b/app/escriptorium/static/images/psl.png differ diff --git a/app/escriptorium/static/images/pslscripta.png b/app/escriptorium/static/images/pslscripta.png new file mode 100644 index 0000000000000000000000000000000000000000..e5850eb2544e912f0a865216d3afeb8f54544aa7 Binary files /dev/null and b/app/escriptorium/static/images/pslscripta.png differ diff --git a/app/escriptorium/static/images/regionidf.jpg b/app/escriptorium/static/images/regionidf.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c8d08a09da84bbc3b8add3d8f14fc8bc45886b39 Binary files /dev/null and b/app/escriptorium/static/images/regionidf.jpg differ diff --git a/app/escriptorium/static/images/t1.png b/app/escriptorium/static/images/t1.png new file mode 100644 index 0000000000000000000000000000000000000000..926bf47050f74f8fd309498a40ca433d3578f109 Binary files /dev/null and b/app/escriptorium/static/images/t1.png differ diff --git a/app/escriptorium/static/images/t2.png b/app/escriptorium/static/images/t2.png new file mode 100644 index 0000000000000000000000000000000000000000..c51f244e1a7ed83c5b722951bd5bef1a96fa41b0 Binary files /dev/null and b/app/escriptorium/static/images/t2.png differ diff --git a/app/escriptorium/static/images/t3.png b/app/escriptorium/static/images/t3.png new file mode 100644 index 0000000000000000000000000000000000000000..17d0f6b526489d9e566eeeab5de9d18312875524 Binary files /dev/null and b/app/escriptorium/static/images/t3.png differ diff --git a/app/escriptorium/templates/base.html b/app/escriptorium/templates/base.html index 780cdb3ddb69f4975df0009f0690dde9bd3ccbf3..ac673a3fa27359b2a874bef5bd2dc37c0398a55a 100644 --- a/app/escriptorium/templates/base.html +++ b/app/escriptorium/templates/base.html @@ -10,8 +10,8 @@ {% comment %} <meta name="description" content=""> <meta name="author" content=""> - <link rel="icon" href="../../favicon.ico"> {% endcomment %} + <link rel="icon" href="{% static "images/favicon.ico" %}"> <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries --> <!--[if lt IE 9]> @@ -25,20 +25,24 @@ {% endblock styles %} </head> <body class="{% block bodyclass %}{% endblock %}"> - <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> - <a class="navbar-brand" href="{% url 'home' %}">eScriptorium</a> - <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> - <span class="navbar-toggler-icon"></span> - </button> + <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> + <a class="navbar-brand" href="{% url 'home' %}"> + <img src="{% static "images/escriptorium_hd.png" %}" width="30" height="30" class="mb-1"> + eScriptorium + </a> - <div class="collapse navbar-collapse" id="navbarSupportedContent"> - <ul class="navbar-nav mr-auto"> - <li class="nav-item {% block nav-home-class %}{% endblock %}"> - <a class="nav-link" href="{% url 'home'%}">Home</a> - </li> - <li class="nav-item"> - <a class="nav-link" href="#">{% trans 'About' %}</a> + <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> + <span class="navbar-toggler-icon"></span> + </button> + + <div class="collapse navbar-collapse" id="navbarSupportedContent"> + <ul class="navbar-nav mr-auto"> + <li class="nav-item {% block nav-home-class %}{% endblock %}"> + <a class="nav-link" href="{% url 'home'%}">Home</a> </li> + <!-- <li class="nav-item"> + <a class="nav-link" href="#">{% trans 'About' %}</a> + </li> --> <li class="nav-item"> <a class="nav-link" href="{% url 'contactus' %}">{% trans 'Contact' %}</a> </li> diff --git a/app/escriptorium/templates/core/document_images.html b/app/escriptorium/templates/core/document_images.html index 23043bad9b6f98c559b366a8c3c0791ed68fa678..7a0b01ed6cafdec2bcaa2fae639a9bd6504a0915 100644 --- a/app/escriptorium/templates/core/document_images.html +++ b/app/escriptorium/templates/core/document_images.html @@ -76,6 +76,7 @@ <div id="image-card-{pk}" class="card"> <div style="position: relative;"> <button class="js-card-select-hdl"><i class="fas fa-square"></i></button> + <span class="js-card-order"></span> <button title="{% trans 'Delete this image and all its data (segmentation, transcriptions)' %}" class="close mr-1 js-card-delete" aria-label="Close"><span aria-hidden="true">×</span></button> <img style="object-fit: cover;" width="180" height="180" class="card-img-top lazy"> <div class="card-btns"> diff --git a/app/escriptorium/templates/core/document_list.html b/app/escriptorium/templates/core/document_list.html index d37afee3430b928f9201e3fb0ee7b6b0ac4675df..4b881cf8e630f07076da04d33cc8e680ecabc498 100644 --- a/app/escriptorium/templates/core/document_list.html +++ b/app/escriptorium/templates/core/document_list.html @@ -13,11 +13,11 @@ <div class="col-md-12 col-md-offset-4"> <a href="{% url 'document-create' %}" class="btn btn-success float-sm-right">{% trans 'Create new' %}</a> <h2>{% trans "My Documents" %}</h2> - - <table class="table table-hover"> + + <table id="document-list" class="table table-hover"> <tbody> {% for document in object_list %} - <tr> + <tr onclick="document.location='{% url 'document-images' pk=document.pk %}'" role="button"> <td> {% with part=document.parts.first %} {% if part %} @@ -47,16 +47,26 @@ </td> {% endcomment %} <td class="text-right"> - {# <a href="{% url 'document-detail' pk=document.pk %}" class="btn btn-info disabled" title="{% trans 'View' %}"><i class="fas fa-eye"></i></a> #} - <a href="{% url 'document-update' pk=document.pk %}" class="btn btn-info" title="{% trans 'Edit' %}"><i class="fas fa-edit"></i></a> + {# <a href="{% url 'document-detail' pk=document.pk %}" class="btn btn-info disabled" title="{% trans 'View' %}"><i class="fas fa-eye"></i></a> #} + {# Note that doing one query per row is a lot faster than a subquery for some reason #} + {% if document.last_edited_part %} + <a href="{% url 'document-part-edit' pk=document.pk part_pk=document.last_edited_part.pk %}" + class="btn btn-info" + title="{% trans 'Edit last updated Element' %}"> + <i class="fas fa-edit"></i> + </a> + {% endif %} + + <form method="post" class="inline-form" action="{% url 'document-publish' pk=document.pk %}" onsubmit="return confirm('{% trans "Do you really want to delete the document?" %}');">{% csrf_token %} + <input type="hidden" name="workflow_state" value="{{ document.WORKFLOW_STATE_ARCHIVED }}"> + <button type="submit" value="" class="nav-item btn btn-danger" title="{% trans 'Delete' %}"><i class="fas fa-trash"></i></button> + </form> </td> </tr> {% endfor %} </tbody> </table> </div> - - </div> {% include 'includes/pagination.html' %} {% endblock %} diff --git a/app/escriptorium/templates/core/document_nav.html b/app/escriptorium/templates/core/document_nav.html index aef99ffd93fc6344d10c7de00b9b31669c8accf5..a0dea118aaca16d763f3d92ca547ad3d743206d3 100644 --- a/app/escriptorium/templates/core/document_nav.html +++ b/app/escriptorium/templates/core/document_nav.html @@ -14,7 +14,7 @@ <a href="{% if document and models_count %}{% url 'document-models' document_pk=document.pk %}{% else %}#{% endif %}" class="nav-item nav-link {% if not document or not models_count %}disabled{% endif %}" id="nav-models-tab" role="tab" aria-controls="nav-doc" aria-selected="true">{% trans "Models" %}</a> {% endwith %} {% endif %} - {% block extra_nav %}{% endblock %} + {% block extra_nav %}<div class="nav-div nav-item ml-5 ">{{document.name}}</div>{% endblock %} </div> </nav> diff --git a/app/escriptorium/templates/core/home.html b/app/escriptorium/templates/core/home.html index 188cf43eecb68c28dc8858724919bbc3e4d68d00..99ae44ea2977a2f3705058af1f80160ce15eab84 100644 --- a/app/escriptorium/templates/core/home.html +++ b/app/escriptorium/templates/core/home.html @@ -1,14 +1,87 @@ {% extends 'base.html' %} -{% load i18n %} +{% load i18n staticfiles %} {% block head_title %}{% trans "Homepage" %}{% endblock %} {% block nav-home-class %}active{% endblock %} {% block body %} -<div class="jumbotron"> - <h1>eScriptorium</h1> - <p>{% trans "A project providing digital recognition of handwritten documents using machine learning techniques." %}</p> - <p><small>Version {{ VERSION_DATE }} - <br/>{{ KRAKEN_VERSION }}</small></p> +<div class="jumbotron text-center bg-light"> + {% if CUSTOM_HOME %} + {% include "additional_heading.html" %} + {% else %} + <h1 class="jumbotron-heading">eScriptorium</h1> + <p class="lead text-muted">{% trans "A project providing digital recognition of handwritten documents using machine learning techniques." %}</p> + {% endif %} </div> + +<div class="container my-5"> + <div class="row"> + <div class="col-lg-4 text-center"> + <img class="rounded-circle" src="{% static "images/t1.png" %}" alt="" width="140" height="140"> + <h2>{% trans "Data Interchange" %}</h2> + <p class="text-muted">{% trans "Import or Export transcriptions with Alto or Page XML, Import images as zip or IIIF. Access data from any application through a full Rest API." %}</p> + </div> + <div class="col-lg-4 text-center"> + <img class="rounded-circle" src="{% static "images/t2.png" %}" alt="Generic placeholder image" width="140" height="140"> + <h2>{% trans "Manual Edition" %}</h2> + <p class="text-muted">{% trans "Make use of an ergonomic user interface leveraging modern browser technology to edit segmentations and transcriptions." %}</p> + </div> + <div class="col-lg-4 text-center"> + <img class="rounded-circle" src="{% static "images/t3.png" %}" alt="Generic placeholder image" width="140" height="140"> + <h2>{% trans "Automatic Transcription" %}</h2> + <p class="text-muted">{% trans "Train and apply new neural networks to vastly speed up the transcription process of large corpora." %}</p> + </div> + </div> + + <hr/> +</div> + + +<div class="row"> + <div class="my-5 container-fluid text-center"> + <a href="https://www.ephe.psl.eu" target="_blank"> + <img src="{% static "images/ephe.png" "%}" alt="EPHE logo" height="100"> + </a> + <a href="https://www.resilience-ri.eu/" class="pl-4" target="_blank"> + <img src="{% static "images/resilience.png" "%}" alt="Resilience logo" height="60"> + </a> + <a href="https://scripta.psl.eu/en/" class="pl-4" target="_blank"> + <img src="{% static "images/pslscripta.png" "%}" alt="Scripta PSL logo" height="50"> + </a> + <a href="https://ec.europa.eu/programmes/horizon2020/" class="pl-4" target="_blank"> + <img src="{% static "images/h2020.jpg" "%}" alt="Horizon 2020 logo" height="80"> + </a> + <a href="https://inria.fr/fr" class="pl-4" target="_blank"> + <img src="{% static "images/inria.png" "%}" alt="INRIA logo" height="60"> + </a> + <a href="https://www.openiti.org/" class="pl-4" target="_blank"> + <img src="{% static "images/openiti.png" "%}" alt="DIM Region ile de france logo" height="90"> + </a> + </div> + + {% if CUSTOM_HOME %} + {% include "additional_icons.html" %} + {% endif %} +</div> + +<footer class="bg-light mt-5 pt-1"> + <div class="container"> + <div class="row"> + <div class="col-6"> + <h5>{% trans "Resources" %}</h5> + <ul class="list-unstyled"> + <li><a href="https://lectaurep.hypotheses.org/documentation/escriptorium-tutorial-en" target="_blank">{% trans "Tutorial" %}</a></li> + <li><a href="https://gitlab.inria.fr/scripta/escriptorium" target="_blank">{% trans "eScriptorium open source code" %}</a> Version {{ VERSION_DATE }}</li> + <li><a href="https://escripta.hypotheses.org/" target="_blank">{% trans "Scripta Blog" %}</a></li> + <li><a href="http://kraken.re/" target="_blank">{{ KRAKEN_VERSION|capfirst }}</a></li> + <li><a href="https://gitlab.inria.fr/scripta/escriptorium/-/wikis/home" target="_blank">Wiki</a></li> + </ul> + </div> + + {% if CUSTOM_HOME %} + {% include "additional_footer.html" %} + {% endif %} + </div> + </div> +</footer> {% endblock %} diff --git a/app/homepage_example/additional_footer.html b/app/homepage_example/additional_footer.html new file mode 100644 index 0000000000000000000000000000000000000000..3b3cfba9bf0b49f68a2180447e47c3f869088f00 --- /dev/null +++ b/app/homepage_example/additional_footer.html @@ -0,0 +1,9 @@ +{% load i18n %} +<div class="col-6"> + <h5>{% trans "Powered by" %}</h5> + <ul class="list-unstyled"> + <li><a href="https://www.python.org" target="_blank">Python</a></li> + <li><a href="https://www.djangoproject.com" target="_blank">Django</a></li> + <li><a href="https://vuejs.org" target="_blank">Vue.js</a></li> + </ul> +</div> diff --git a/app/homepage_example/additional_heading.html b/app/homepage_example/additional_heading.html new file mode 100644 index 0000000000000000000000000000000000000000..7895815b24315554e859aca654f9dc4e61621a50 --- /dev/null +++ b/app/homepage_example/additional_heading.html @@ -0,0 +1,3 @@ +<h1 class="jumbotron-heading">eScriptorium</h1> +<p class="lead text-muted">A project providing digital recognition of handwritten documents using machine learning techniques.</p> +<p>eScriptorium accounts are created on invitation only for now. If your institution doesn't provide it, you can use the contact form to ask for access.</p> diff --git a/app/homepage_example/additional_icons.html b/app/homepage_example/additional_icons.html new file mode 100644 index 0000000000000000000000000000000000000000..1b6ece1af55126823c5e18d33b69f9d626352b21 --- /dev/null +++ b/app/homepage_example/additional_icons.html @@ -0,0 +1,11 @@ +{% load staticfiles %} +</div> <!-- example to create another row --> +<div class="row"> + <div class="container-fluid text-center"> + <a href="http://www.python.org" target="_blank"> + <img src="{% static "python.png" %}" alt="custom logo" height="50"> + </a> + <a href="http://www.djangoproject.com" target="_blank"> + <img src="{% static "django.png" %}" alt="custom logo" height="50"> + </a> + </div> diff --git a/app/homepage_example/django.png b/app/homepage_example/django.png new file mode 100644 index 0000000000000000000000000000000000000000..2a1d873eeb34af6bf0c69baec82fe45e1bf749b9 Binary files /dev/null and b/app/homepage_example/django.png differ diff --git a/app/homepage_example/python.png b/app/homepage_example/python.png new file mode 100644 index 0000000000000000000000000000000000000000..a215d687b662d4a105fc116492d0a3add6435cfb Binary files /dev/null and b/app/homepage_example/python.png differ diff --git a/app/requirements.txt b/app/requirements.txt index facd20bafb489790e35c68229a5cfd2b915e23f4..578411a956c52c1b8fe894d5f9f3482d30970226 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -12,7 +12,7 @@ django-redis==4.10.0 psycopg2-binary==2.7.6 django-ordered-model==3.1.1 easy-thumbnails==2.5 -git+https://github.com/mittagessen/kraken.git@3.0b21#egg=kraken +git+https://github.com/mittagessen/kraken.git@3.0b23#egg=kraken django-cleanup==5.1.0 djangorestframework==3.9.2 drf-nested-routers==0.91 diff --git a/docker-compose.override.yml_example b/docker-compose.override.yml_example index 519b21d512d6bd5e98a89c8eb850afe67e480eb1..5206c72563c9a520c9b913a09a72687c32044d1a 100644 --- a/docker-compose.override.yml_example +++ b/docker-compose.override.yml_example @@ -10,6 +10,12 @@ services: # constraints: # - node.hostname == frontend0 + ### to customize the homepage, uncomment this + # environment: + # - CUSTOM_HOME=True + # volumes: + # - $PWD/app/homepage + channelserver: restart: always # deploy: diff --git a/docker-compose.yml b/docker-compose.yml index b7bbb53b52f4ed4e023f75f5efd68eb81f49f84b..89b5980cd597a776019cb1c3370b13aae08a0a4d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,8 @@ services: web: <<: *app command: uwsgi --ini /usr/src/app/uwsgi.ini + volumes: + - logs:/user/src/app/escriptorium/logs expose: - 8000 @@ -116,3 +118,4 @@ volumes: media: postgres: esdata: + logs: diff --git a/front/css/escriptorium.css b/front/css/escriptorium.css index 2955b5d5dc5e5cde4c089f29277be0650e89f20d..85702e1ba33ef40bbb16495ec6e8c586dbf78dab 100644 --- a/front/css/escriptorium.css +++ b/front/css/escriptorium.css @@ -134,6 +134,7 @@ form.inline-form { #cards-container { overflow: auto; + counter-reset: card; } #cards-container .card { @@ -192,6 +193,16 @@ form.inline-form { border: 0; } +.js-card-order { + position: absolute; + left: 50%; +} + +.js-card-order:after { + counter-increment: card; + content: counter(card); +} + .process-part-form h5 { color: var(--secondary); text-align: center; @@ -381,6 +392,10 @@ form.inline-form { color: grey; } +#gotoModal .modal-dialog { + max-width: 355px; +} + .col-sides { width: 30px; margin: 0 15px; @@ -517,6 +532,8 @@ i.panel-icon { margin-top: 1rem; white-space: nowrap; padding: 0 2px; + box-sizing: content-box; + padding-bottom: 0.1em; transform-origin: top left; box-shadow: none; } diff --git a/front/css/ttb.css b/front/css/ttb.css new file mode 100644 index 0000000000000000000000000000000000000000..1dd3d09815820f0aa4aaf64bd201959ed95e4a74 --- /dev/null +++ b/front/css/ttb.css @@ -0,0 +1,57 @@ +/* vertical styles: */ + +#vertical_text_input{ + writing-mode: vertical-lr; + text-orientation: upright; + width:auto!important; + height: auto; + white-space: nowrap!important; + margin: .375rem 0; +} +#vertical_text_input:focus{ + outline: none; +} + +#textInputBorderWrapper{ + box-sizing: content-box; + padding: 0 .75rem; +} + +#diplomatic-lines.ttb{ + writing-mode: vertical-lr; + text-orientation: upright; +} + +#diplomatic-lines.ttb div{ + text-align: left; + min-width:25px; + padding: 0; + margin: 0; + padding-top: 30px; + + position: relative; + white-space: nowrap; +} + +#diplomatic-lines.ttb div::before{ + writing-mode: horizontal-tb !important; + position: absolute; + display: block; /*to set size properties*/ + white-space: nowrap; + text-align: left; + + width: 100%; /*adapt to #diplomatic-lines.ttb div size - set to position:relative*/ + height: auto; + + margin-left: 0; + margin-right: .5rem; + margin-top: -30px; + margin-bottom: 20px; + + padding-left: .5rem; + padding-right: 0; + + border-bottom: 1px solid #ddd; + border-right: none!important; + border-right-width:0!important; +} diff --git a/front/src/editor/api.js b/front/src/editor/api.js index 2cf9820be7bec04bffbd951eff34405cf5afd6c5..528517857dc97a59db9de4cca80851f17b194ab5 100644 --- a/front/src/editor/api.js +++ b/front/src/editor/api.js @@ -10,6 +10,8 @@ export const retrieveDocument = async document_id => (await axios.get(`/document export const retrieveDocumentPart = async (document_id, part_id) => (await axios.get(`/documents/${document_id}/parts/${part_id}/`)) +export const retrieveDocumentPartByOrder = async (document_id, order) => (await axios.get(`/documents/${document_id}/parts/byorder/?order=${order}`)) + export const retrievePage = async (document_id, part_id, transcription, page) => (await axios.get(`/documents/${document_id}/parts/${part_id}/transcriptions/?transcription=${transcription}&page=${page}`)) export const createContent = async (document_id, part_id, data) => (await axios.post(`/documents/${document_id}/parts/${part_id}/transcriptions/`, data)) @@ -40,4 +42,4 @@ export const bulkCreateLineTranscriptions = async (document_id, part_id, data) = export const bulkUpdateLineTranscriptions = async (document_id, part_id, data) => (await axios.put(`/documents/${document_id}/parts/${part_id}/transcriptions/bulk_update/`, data)) -export const moveLines = async (document_id, part_id, data) => (await axios.post(`/documents/${document_id}/parts/${part_id}/lines/move/`, data)) \ No newline at end of file +export const moveLines = async (document_id, part_id, data) => (await axios.post(`/documents/${document_id}/parts/${part_id}/lines/move/`, data)) diff --git a/front/src/editor/store/document.js b/front/src/editor/store/document.js index 88bf207411dee4dbf977714ab6b2ae4a6dd2dadc..ee3ffe0a4963cec7ce16f41ce070b43f7b240b5e 100644 --- a/front/src/editor/store/document.js +++ b/front/src/editor/store/document.js @@ -4,6 +4,7 @@ import * as api from '../api' export const initialState = () => ({ id: null, name: "", + partsCount: 0, defaultTextDirection: null, mainTextDirection: null, readDirection: null, @@ -39,6 +40,9 @@ export const mutations = { setTypes (state, types) { state.types = types }, + setPartsCount(state, count) { + state.partsCount = count + }, setBlockShortcuts(state, block) { state.blockShortcuts = block }, @@ -56,6 +60,7 @@ export const actions = { let data = resp.data commit('transcriptions/set', data.transcriptions, {root: true}) commit('setTypes', { 'regions': data.valid_block_types, 'lines': data.valid_line_types }) + commit('setPartsCount', data.parts_count) }, async togglePanel ({state, commit}, panel) { diff --git a/front/src/editor/store/parts.js b/front/src/editor/store/parts.js index ed1ce9649b90cabf951ad7623d281a7a1b9c4fb0..3d366ad30afe5ac43d6b608573922bd0a126a16b 100644 --- a/front/src/editor/store/parts.js +++ b/front/src/editor/store/parts.js @@ -31,12 +31,19 @@ export const mutations = { } export const actions = { - async fetchPart ({commit, dispatch, rootState}, pk) { - commit('setPartPk', pk) + async fetchPart ({commit, dispatch, rootState}, {pk, order}) { if (!rootState.transcriptions.all.length) { await dispatch('document/fetchDocument', rootState.document.id, {root: true}) } - const resp = await api.retrieveDocumentPart(rootState.document.id, pk) + var resp + if (pk) { + commit('setPartPk', pk) + resp = await api.retrieveDocumentPart(rootState.document.id, pk) + } else { + resp = await api.retrieveDocumentPartByOrder(rootState.document.id, order) + commit('setPartPk', resp.data.pk) + } + let data = resp.data data.lines.forEach(function(line) { @@ -63,7 +70,20 @@ export const actions = { commit('regions/reset', {}, {root: true}) commit('lines/reset', {}, {root: true}) commit('reset') - await dispatch('fetchPart', pk) + await dispatch('fetchPart', {pk: pk}) + }, + + async loadPartByOrder({state, commit, dispatch, rootState}, order) { + commit('regions/reset', {}, {root: true}) + commit('lines/reset', {}, {root: true}) + commit('reset') + try { + await dispatch('fetchPart', {order: order}) + await dispatch('transcriptions/getCurrentContent', rootState.transcriptions.selectedTranscription, {root: true}) + await dispatch('transcriptions/getComparisonContent', {}, {root: true}) + } catch (err) { + console.log('couldnt fetch part data!', err) + } }, async loadPart({state, commit, dispatch, rootState}, direction) { @@ -73,7 +93,7 @@ export const actions = { commit('lines/reset', {}, {root: true}) commit('reset') try { - await dispatch('fetchPart', part) + await dispatch('fetchPart', {pk: part}) await dispatch('transcriptions/getCurrentContent', rootState.transcriptions.selectedTranscription, {root: true}) await dispatch('transcriptions/getComparisonContent', {}, {root: true}) } catch (err) { diff --git a/front/src/image_cards.js b/front/src/image_cards.js index 8e4dc679c69f7bef6559afd15d2a76e8d0124ad3..15b904e4111105da27816b2375cb85de3838e1e3 100644 --- a/front/src/image_cards.js +++ b/front/src/image_cards.js @@ -36,6 +36,7 @@ function openWizard(proc) { class partCard { constructor(part) { this.pk = part.pk; + this.order = part.order; this.name = part.name; this.title = part.title; this.typology = part.typology; diff --git a/front/src/main.js b/front/src/main.js index 70a201056ccc4f1ddadd03a3b1fccaf56e94d086..6701affcac361dec89604a6ac195371a52bab37e 100644 --- a/front/src/main.js +++ b/front/src/main.js @@ -1,5 +1,6 @@ import '../css/escriptorium.css'; import '../css/rtl.css'; +import '../css/ttb.css'; import './ajax.js'; import { Alert, bootWebsocket, joinDocumentRoom } from './messages.js'; window.Alert = Alert; @@ -29,4 +30,4 @@ import { bootImageCards } from './image_cards.js'; window.bootImageCards = bootImageCards; import { bootModels } from './models.js'; -window.bootModels = bootModels; \ No newline at end of file +window.bootModels = bootModels; diff --git a/front/vue/components/DiploPanel.vue b/front/vue/components/DiploPanel.vue index 4da1265f94f744da00cf08334cadcac945091fd8..82409240c3d97e54fa341ce7953f0d8dbfc8d93d 100644 --- a/front/vue/components/DiploPanel.vue +++ b/front/vue/components/DiploPanel.vue @@ -18,7 +18,9 @@ v-bind:key="'DL' + line.pk"> </diploline> - <div id="diplomatic-lines" + <!--adding a class to get styles for ttb direction:--> + <div :class="$store.state.document.mainTextDirection" + id="diplomatic-lines" ref="diplomaticLines" contenteditable="true" autocomplete="off" @@ -65,6 +67,9 @@ export default Vue.extend({ }.bind(this), 10000); }, mounted() { + // fix the original width so that when content texts are loaded/page refreshed with diplo panel, the panel width wont be bigger than other, especially for ttb text: + document.querySelector('#diplo-panel').style.width = document.querySelector('#diplo-panel').clientWidth + 'px'; + Vue.nextTick(function() { var vm = this ; vm.sortable = Sortable.create(this.$refs.diplomaticLines, { diff --git a/front/vue/components/Editor.vue b/front/vue/components/Editor.vue index 64b66e6deda18712e213f01b81d6a1c821866ee9..ca62331237f1f28ff9645bbdae1730643718a26a 100644 --- a/front/vue/components/Editor.vue +++ b/front/vue/components/Editor.vue @@ -81,7 +81,7 @@ export default { this.$store.commit('document/setMainTextDirection', this.mainTextDirection); this.$store.commit('document/setReadDirection', this.readDirection); try { - await this.$store.dispatch('parts/fetchPart', this.partId); + await this.$store.dispatch('parts/fetchPart', {pk: this.partId}); let tr = userProfile.get('initialTranscriptions') && userProfile.get('initialTranscriptions')[this.$store.state.document.id] || this.$store.state.transcriptions.all[0].pk; diff --git a/front/vue/components/ExtraInfo.vue b/front/vue/components/ExtraInfo.vue index 218f0ae6cf2a6924731cc2ce26197765a288316d..9ddbf94d026984a843d8a899fd32057011a39bec 100644 --- a/front/vue/components/ExtraInfo.vue +++ b/front/vue/components/ExtraInfo.vue @@ -2,18 +2,59 @@ <div> <div class="nav-div nav-item ml-2"> <span v-if="$store.state.document.name" id="part-name">{{ $store.state.document.name }}</span> - <span id="part-title" v-if="$store.state.parts.loaded">{{ $store.state.parts.title }} - {{ $store.state.parts.filename }} - ({{ imageSize }})</span> + <span id="part-title" + v-if="$store.state.parts.loaded" + title="Click to go to another Element (Ctrl+Home)." + data-toggle="modal" + data-target="#gotoModal" + role="button">{{ $store.state.parts.title }} - {{ $store.state.parts.filename }} - ({{ imageSize }})</span> <span class="loading" v-if="!$store.state.parts.loaded">Loading…</span> </div> + + <div id="gotoModal" + class="modal ui-draggable show" + tabindex="-1" + role="dialog"> + <div class="modal-dialog modal-dialog-centered"> + <div class="modal-content"> + <div class="modal-body"> + Element # + <input type="number" + v-if="$store.state.parts.loaded" + min="1" + :max="$store.state.document.partsCount" + width="100%" + v-bind:value="$store.state.parts.order+1" + @change.lazy="goTo"/> + / {{$store.state.document.partsCount}} + </div> + </div> + </div> + </div> </div> </template> <script> export default { + async created() { + document.addEventListener('keyup', async function(event) { + if (event.ctrlKey && event.keyCode == 36) { // Home + $('#gotoModal').modal('show'); + } + }); + }, computed: { imageSize() { return this.$store.state.parts.image.size[0]+'x'+this.$store.state.parts.image.size[1]; }, + }, + methods: { + async goTo(ev) { + if (ev.target.value > 0 && ev.target.value <= parseInt(ev.target.attributes.max.value)) { + await this.$store.dispatch('parts/loadPartByOrder', ev.target.value-1); + $('#gotoModal').modal('hide'); + } + } } } </script> diff --git a/front/vue/components/TranscriptionModal.vue b/front/vue/components/TranscriptionModal.vue index 640ce673e574cbc89aa637747007f41d7c46f5bd..5cb968f7afa9f6580d7c78900bb27bcfc6a32970 100644 --- a/front/vue/components/TranscriptionModal.vue +++ b/front/vue/components/TranscriptionModal.vue @@ -70,7 +70,8 @@ </div> <div id="trans-input-container" ref="transInputContainer"> - <input v-on:keyup.down="$store.dispatch('lines/editLine', 'next')" + <input v-if="$store.state.document.mainTextDirection != 'ttb'" + v-on:keyup.down="$store.dispatch('lines/editLine', 'next')" v-on:keyup.up="$store.dispatch('lines/editLine', 'previous')" v-on:keyup.enter="$store.dispatch('lines/editLine', 'next')" id="trans-input" @@ -80,6 +81,30 @@ v-model.lazy="localTranscription" autocomplete="off" autofocus/> + <!--Hidden input for ttb text: --> + <input v-else + id="trans-input" + ref="transInput" + name="content" + type="hidden" + v-model.lazy="localTranscription" + autocomplete="off" /> + <!-- in this case, input field is replaced by: --> + <div v-if="$store.state.document.mainTextDirection == 'ttb'" + id="textInputWrapper"> + <div id="textInputBorderWrapper" class="form-control mb-2"> + <div v-on:blur="localTranscription = $event.target.textContent" + v-on:keyup="recomputeInputCharsScaleY()" + v-on:keyup.right="$store.dispatch('lines/editLine', 'next')" + v-on:keyup.left="$store.dispatch('lines/editLine', 'previous')" + v-on:keyup.enter="cleanHTMLTags();recomputeInputCharsScaleY();$store.dispatch('lines/editLine', 'next')" + v-html="localTranscription" + id="vertical_text_input" + contenteditable="true"> + </div> + </div> + </div> + <small v-if="line.currentTrans && line.currentTrans.version_updated_at" class="form-text text-muted"> <span>by {{line.currentTrans.version_author}} ({{line.currentTrans.version_source}})</span> <span>on {{momentDate}}</span> @@ -185,9 +210,27 @@ export default Vue.extend({ $(this.$refs.transModal).draggable({handle: '.modal-header'}); $(this.$refs.transModal).resizable(); this.computeStyles(); + let modele = this; let input = this.$refs.transInput; - input.focus(); + + // no need to make focus on hiden input with a ttb text + if(this.$store.state.document.mainTextDirection != 'ttb'){ + input.focus(); + }else{ // avoid some br or other html tag for a copied text on an editable input div (vertical_text_input): + // + document.getElementById("vertical_text_input").addEventListener("paste", function(e) { + + // cancel paste to treat its content before inserting it + e.preventDefault(); + + // get text representation of clipboard + var text = (e.originalEvent || e).clipboardData.getData('text/plain'); + this.innerHTML = text; + modele.recomputeInputCharsScaleY(); + + }, false); + } }, watch: { line(new_, old_) { @@ -240,7 +283,19 @@ export default Vue.extend({ close() { $(this.$refs.transModal).modal('hide'); }, - + cleanHTMLTags(){ + document.getElementById("vertical_text_input").innerHTML = document.getElementById("vertical_text_input").textContent; + }, + recomputeInputCharsScaleY(){ + + let inputHeight = document.getElementById("vertical_text_input").clientHeight; + let wrapperHeight = document.getElementById("textInputBorderWrapper").clientHeight; + let textScaleY = wrapperHeight / (inputHeight + 10); + + // to avoid input text outside the border box: + if(inputHeight > wrapperHeight) + document.getElementById("vertical_text_input").style.transform = "scaleY("+textScaleY+")"; + }, comparedContent(content) { if (!this.line.currentTrans) return; let diff = Diff.diffChars(this.line.currentTrans.content, content); @@ -286,6 +341,8 @@ export default Vue.extend({ // calculate rotation needed to get the line horizontal let target_angle = 0; // all lines should be topologically ltr + if(this.$store.state.document.mainTextDirection == 'ttb') // add a 90 angle for vertical texts + target_angle = 90; let angle = target_angle - this.getLineAngle(); // apply it to the polygon and get the resulting bbox @@ -309,8 +366,14 @@ export default Vue.extend({ let context = hContext*lineHeight; let visuHeight = lineHeight + 2*context; - modalImgContainer.style.height = visuHeight+'px'; + if(this.$store.state.document.mainTextDirection != 'ttb'){ + modalImgContainer.style.height = visuHeight+'px'; + }else{ + modalImgContainer.style.width = visuHeight+'px'; + modalImgContainer.style.display = 'inline-block'; + modalImgContainer.style.verticalAlign = 'top'; + } let top = -(bbox.top*ratio - context); let left = -(bbox.left*ratio - context); @@ -353,6 +416,8 @@ export default Vue.extend({ // Content input let container = this.$refs.transInputContainer; let input = container.querySelector('#trans-input'); + let verticalTextInput; + // note: input is not up to date yet let content = this.line.currentTrans && this.line.currentTrans.content || ''; let ruler = document.createElement('span'); @@ -360,13 +425,29 @@ export default Vue.extend({ ruler.style.visibility = 'hidden'; ruler.textContent = content; ruler.style.whiteSpace="nowrap" + + if(this.$store.state.document.mainTextDirection == 'ttb'){ + // put the container inline for vertical transcription: + container.style.display = 'inline-block'; + verticalTextInput = container.querySelector('#vertical_text_input'); + // apply vertical writing style to the ruler: + ruler.style.writingMode = 'vertical-lr'; + ruler.style.textOrientation = 'upright'; + } + container.appendChild(ruler); let context = hContext*lineHeight; let fontSize = Math.max(15, Math.round(lineHeight*0.7)); // Note could depend on the script ruler.style.fontSize = fontSize+'px'; - input.style.fontSize = fontSize+'px'; - input.style.height = Math.round(fontSize*1.1)+'px'; + + if(this.$store.state.document.mainTextDirection != 'ttb'){ + input.style.fontSize = fontSize+'px'; + input.style.height = Math.round(fontSize*1.1)+'px'; + }else{ + verticalTextInput.style.fontSize = fontSize+'px'; + verticalTextInput.style.width = Math.round(fontSize*1.1)+'px'; + } if (this.$store.state.document.readDirection == 'rtl') { container.style.marginRight = context+'px'; @@ -375,15 +456,40 @@ export default Vue.extend({ // TODO: deal with other directions container.style.marginLeft = context+'px'; } - if (content) { - let lineWidth = bbox.width*ratio; - var scaleX = Math.min(5, lineWidth / ruler.clientWidth); - scaleX = Math.max(0.2, scaleX); - input.style.transform = 'scaleX('+ scaleX +')'; - input.style.width = 100/scaleX + '%'; - } else { - input.style.transform = 'none'; - input.style.width = '100%'; //'calc(100% - '+context+'px)'; + if(this.$store.state.document.mainTextDirection != 'ttb'){ + if (content) { + let lineWidth = bbox.width*ratio; + var scaleX = Math.min(5, lineWidth / ruler.clientWidth); + scaleX = Math.max(0.2, scaleX); + input.style.transform = 'scaleX('+ scaleX +')'; + input.style.width = 100/scaleX + '%'; + } else { + input.style.transform = 'none'; + input.style.width = '100%'; //'calc(100% - '+context+'px)'; + } + }else{ + let modalImgContainer = this.$refs.modalImgContainer; + let textInputWrapper = container.querySelector('#textInputWrapper'); + let textInputBorderWrapper = container.querySelector('#textInputBorderWrapper'); + if (content) { + let lineWidth = bbox.height*ratio; + var scaleY = Math.min(5, lineWidth / ruler.clientHeight); + //var scaleY = Math.min(5, lineWidth / modalImgContainer.clientHeight); + //var scaleY = Math.min(5, modalImgContainer.clientHeight / ruler.clientHeight); + //var scaleY = Math.min(5, modalImgContainer.clientHeight / textInputWrapper.clientHeight) * 0.7; + scaleY = Math.max(0.2, scaleY); + verticalTextInput.style.transformOrigin = 'top'; + verticalTextInput.style.transform = 'scaleY('+ scaleY +')'; + //document.getElementById('vertical_text_input').style.height = 100/scaleY + '%'; // not needed here + } else { + verticalTextInput.style.transform = 'none'; + verticalTextInput.style.height = modalImgContainer.clientHeight + 'px'; + } + textInputWrapper.style.height = modalImgContainer.clientHeight + 'px'; + // simulate an input field border to fix it to the actual size of the image + textInputBorderWrapper.style.width = verticalTextInput.clientWidth+'px'; + //textInputBorderWrapper.style.width = verticalTextInput.offsetWidth+'px'; + textInputBorderWrapper.style.height = modalImgContainer.clientHeight+'px'; } container.removeChild(ruler); // done its job }, @@ -397,13 +503,33 @@ export default Vue.extend({ let bbox = this.getRotatedLineBBox(); let hContext = 0.3; // vertical context added around the line, in percentage - let ratio = modalImgContainer.clientWidth / (bbox.width + (2*bbox.height*hContext)); - let MAX_HEIGHT = Math.round(Math.max(25, (window.innerHeight-230) / 3)); - let lineHeight = Math.max(30, Math.round(bbox.height*ratio)); - if (lineHeight > MAX_HEIGHT) { - // change the ratio so that the image can not get too big - ratio = (MAX_HEIGHT/lineHeight)*ratio; - lineHeight = MAX_HEIGHT; + + // + let ratio = 1; + let lineHeight = 150; + + if(this.$store.state.document.mainTextDirection != 'ttb') + { + ratio = modalImgContainer.clientWidth / (bbox.width + (2*bbox.height*hContext)); + let MAX_HEIGHT = Math.round(Math.max(25, (window.innerHeight-230) / 3)); + lineHeight = Math.max(30, Math.round(bbox.height*ratio)); + if (lineHeight > MAX_HEIGHT) { + // change the ratio so that the image can not get too big + ratio = (MAX_HEIGHT/lineHeight)*ratio; + lineHeight = MAX_HEIGHT; + } + }else{ // permutation of sizes for ttb text + + modalImgContainer.style.height=String(window.innerHeight-230) + "px"; // needed to fix height or ratio is nulled + ratio = modalImgContainer.clientHeight / (bbox.height + (2*bbox.width*hContext)); + let MAX_WIDTH = 30; + lineHeight = Math.max(30, Math.round(bbox.width*ratio)); + + if (lineHeight > MAX_WIDTH) { + // change the ratio so that the image can not get too big + ratio = (MAX_WIDTH/lineHeight)*ratio; + lineHeight = MAX_WIDTH; + } } this.computeImgStyles(bbox, ratio, lineHeight, hContext); diff --git a/front/vue/components/VisuLine.vue b/front/vue/components/VisuLine.vue index 4e1182534f7fe52303056cda7cbcdb1d03caf8ce..8c98b1f0db3122337d0eb580bf75a6ce665aab55 100644 --- a/front/vue/components/VisuLine.vue +++ b/front/vue/components/VisuLine.vue @@ -10,14 +10,28 @@ fill="none" v-bind:stroke="pathStrokeColor" v-bind:d="baselinePoints"></path> + + <text :text-anchor="$store.state.document.defaultTextDirection == 'rtl' ? 'end' : ''" + ref="textElement" + lengthAdjust="spacingAndGlyphs" + v-if="$store.state.document.mainTextDirection != 'ttb'"> + <textPath v-bind:href="'#' + textPathId" + v-if="line.currentTrans"> + {{ line.currentTrans.content }} + </textPath> + </text> + <text :text-anchor="$store.state.document.defaultTextDirection == 'rtl' ? 'end' : ''" ref="textElement" - lengthAdjust="spacingAndGlyphs"> + rotate="-90" + font-size="1em" + v-else> <textPath v-bind:href="'#' + textPathId" v-if="line.currentTrans"> {{ line.currentTrans.content }} </textPath> </text> + </g> </template> @@ -26,6 +40,8 @@ import { LineBase } from '../../src/editor/mixins.js'; export default Vue.extend({ mixins: [LineBase], + mounted() { + }, watch: { 'line.currentTrans.content': function(n, o) { this.$nextTick(this.reset); @@ -41,18 +57,43 @@ export default Vue.extend({ let poly = this.line.mask.flat(1).map(pt => Math.round(pt)); var area = 0; // A = 1/2(x_1y_2-x_2y_1+x_2y_3-x_3y_2+...+x_(n-1)y_n-x_ny_(n-1)+x_ny_1-x_1y_n), - for (let i=0; i<poly.length; i++) { - let j = (i+1) % poly.length; // loop back to 1 - area += poly[i][0]*poly[j][1] - poly[j][0]*poly[i][1]; + + var liste = String(poly).split(","); + var indexCoordonnee = 0; + var arrayCoordonnees = new Array(); + var paire = []; + for(var i = 0; i < liste.length; i++){ + paire.push(liste[i]); + if(indexCoordonnee==0){ + indexCoordonnee = 1; + }else{ + indexCoordonnee = 0; + arrayCoordonnees.push(paire); + paire = new Array(); + } + } + + for (let i=0; i<arrayCoordonnees.length; i++) { + let j = (i+1) % arrayCoordonnees.length; // loop back to 1 + area += arrayCoordonnees[i][0]*arrayCoordonnees[j][1] - arrayCoordonnees[j][0]*arrayCoordonnees[i][1]; } + area = Math.abs(area*this.ratio); lineHeight = area / this.$refs.pathElement.getTotalLength(); + } else { lineHeight = 30; } + + lineHeight = Math.max(Math.round(lineHeight), 5) * 0.3; + + let ratio = 1/4; // more well suited for horizontal latin writings + if(this.$store.state.document.mainTextDirection == 'ttb') + ratio = 1/2; + + this.$refs.textElement.setAttribute("font-size", String(lineHeight * (ratio)) + 'px'); - lineHeight = Math.max(Math.min(Math.round(lineHeight), 100), 5); - this.$refs.textElement.style.fontSize = lineHeight * (1/2) + 'px'; + //return lineHeight+'px'; return 10+'px'; }, computeTextLength() { diff --git a/variables.env_example b/variables.env_example index a3dabbb55d4630114510d7c2dd6ceaab3ece3c35..69e9c245e24702af7af536471100947411926c2d 100644 --- a/variables.env_example +++ b/variables.env_example @@ -4,10 +4,10 @@ SECRET_KEY=changeme REDIS_HOST=redis # REDIS_PORT=6379 SQL_HOST=db -# SQL_PORT=5432 -# POSTGRES_USER=postgres -# POSTGRES_PASSWORD=postgres -# POSTGRES_DB=escriptorium +SQL_PORT=5432 +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DB=escriptorium DJANGO_SU_NAME=admin DJANGO_SU_EMAIL=admin@admin.com