From 6493a8c5adda4fc2c24b9337355361aa066bf2a6 Mon Sep 17 00:00:00 2001 From: Robin Tissot <tissotrobin@gmail.com> Date: Thu, 29 Apr 2021 13:19:36 +0200 Subject: [PATCH] wip --- app/apps/core/admin.py | 4 +- app/apps/core/forms.py | 8 ++- .../migrations/0044_auto_20210427_0946.py | 39 ++++++++++++++ app/apps/core/migrations/0045_project_slug.py | 20 +++++++ app/apps/core/models.py | 54 +++++++++++++++++++ app/apps/core/urls.py | 11 +++- app/apps/core/views.py | 53 +++++++++++++++--- app/escriptorium/templates/base.html | 2 +- .../templates/core/document_list.html | 12 ++--- .../templates/core/project_form.html | 21 ++++++++ .../templates/core/project_list.html | 25 +++++++++ docker-compose.yml | 3 +- 12 files changed, 231 insertions(+), 21 deletions(-) create mode 100644 app/apps/core/migrations/0044_auto_20210427_0946.py create mode 100644 app/apps/core/migrations/0045_project_slug.py create mode 100644 app/escriptorium/templates/core/project_form.html create mode 100644 app/escriptorium/templates/core/project_list.html diff --git a/app/apps/core/admin.py b/app/apps/core/admin.py index bf33b7d0..0a978610 100644 --- a/app/apps/core/admin.py +++ b/app/apps/core/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin -from core.models import (Document, +from core.models import (Project, + Document, DocumentPart, Metadata, DocumentMetadata, @@ -44,6 +45,7 @@ class OcrModelAdmin(admin.ModelAdmin): list_display = ['name', 'job', 'owner', 'script', 'training'] +admin.site.register(Project) admin.site.register(Document, DocumentAdmin) admin.site.register(DocumentPart, DocumentPartAdmin) admin.site.register(LineTranscription, LineTranscriptionAdmin) diff --git a/app/apps/core/forms.py b/app/apps/core/forms.py index c591e7fd..6dc919d7 100644 --- a/app/apps/core/forms.py +++ b/app/apps/core/forms.py @@ -11,7 +11,7 @@ from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from bootstrap.forms import BootstrapFormMixin -from core.models import (Document, Metadata, DocumentMetadata, +from core.models import (Project, Document, Metadata, DocumentMetadata, DocumentPart, OcrModel, Transcription, BlockType, LineType, AlreadyProcessingException) from users.models import User @@ -19,6 +19,12 @@ from users.models import User logger = logging.getLogger(__name__) +class ProjectForm(BootstrapFormMixin, forms.ModelForm): + class Meta: + model = Project + fields = ['name'] + + class DocumentForm(BootstrapFormMixin, forms.ModelForm): def __init__(self, *args, **kwargs): self.request = kwargs.pop('request') diff --git a/app/apps/core/migrations/0044_auto_20210427_0946.py b/app/apps/core/migrations/0044_auto_20210427_0946.py new file mode 100644 index 00000000..d68d7b4f --- /dev/null +++ b/app/apps/core/migrations/0044_auto_20210427_0946.py @@ -0,0 +1,39 @@ +# Generated by Django 2.2.20 on 2021-04-27 09:46 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0011_update_proxy_permissions'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('core', '0043_auto_20210324_1016'), + ] + + operations = [ + migrations.AlterField( + model_name='document', + name='read_direction', + field=models.CharField(choices=[('ltr', 'Left to right'), ('rtl', 'Right to left')], default='ltr', help_text='The read direction describes the order of the elements in the document, in opposition with the text direction which describes the order of the words in a line and is set by the script.', max_length=3), + ), + migrations.CreateModel( + name='Project', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=512)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ('shared_with_groups', models.ManyToManyField(blank=True, related_name='shared_projects', to='auth.Group', verbose_name='Share with teams')), + ('shared_with_users', models.ManyToManyField(blank=True, related_name='shared_projects', to=settings.AUTH_USER_MODEL, verbose_name='Share with users')), + ], + ), + migrations.AddField( + model_name='document', + name='project', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.Project'), + ), + ] diff --git a/app/apps/core/migrations/0045_project_slug.py b/app/apps/core/migrations/0045_project_slug.py new file mode 100644 index 00000000..817618e7 --- /dev/null +++ b/app/apps/core/migrations/0045_project_slug.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.20 on 2021-04-27 10:22 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0044_auto_20210427_0946'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='slug', + field=models.SlugField(default=datetime.datetime(2021, 4, 27, 10, 22, 51, 880783)), + preserve_default=False, + ), + ] diff --git a/app/apps/core/models.py b/app/apps/core/models.py index 9299c1bc..06cd2ecc 100644 --- a/app/apps/core/models.py +++ b/app/apps/core/models.py @@ -5,6 +5,7 @@ import os import json import functools import subprocess +import time import uuid from PIL import Image from datetime import datetime @@ -21,6 +22,7 @@ from django.contrib.postgres.fields import JSONField from django.core.validators import FileExtensionValidator from django.dispatch import receiver from django.forms import ValidationError +from django.template.defaultfilters import slugify from django.utils.functional import cached_property from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ @@ -134,6 +136,54 @@ class DocumentMetadata(models.Model): return '%s:%s' % (self.document.name, self.key.name) +class ProjectManager(models.Manager): + def for_user(self, user): + # return the list of editable projects + # Note: Monitor this query + return (Project.objects + .filter(Q(owner=user) + | (Q(shared_with_users=user) + | Q(shared_with_groups__in=user.groups.all()))) + .prefetch_related('shared_with_groups') + .distinct()) + + +class Project(models.Model): + name = models.CharField(max_length=512) + slug = models.SlugField() + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) + + shared_with_users = models.ManyToManyField(User, blank=True, + verbose_name=_("Share with users"), + related_name='shared_projects') + shared_with_groups = models.ManyToManyField(Group, blank=True, + verbose_name=_("Share with teams"), + related_name='shared_projects') + + # strict_ontology = + + objects = ProjectManager() + + class Meta: + ordering = ('-updated_at',) + + def save(self, *args, **kwargs): + if not self.pk: + slug = slugify(self.name) + print(slug) + # check unicity + exists = Project.objects.filter(slug=slug).count() + if not exists: + self.slug = slug + else: + self.slug = slug[:40] + hex(int(time.time()))[2:] + super().save(*args, **kwargs) + + class DocumentManager(models.Manager): def get_queryset(self): return super().get_queryset().select_related('typology') @@ -201,6 +251,10 @@ class Document(models.Model): metadatas = models.ManyToManyField(Metadata, through=DocumentMetadata, blank=True) + project = models.ForeignKey(Project, null=True, blank=True, + on_delete=models.CASCADE, + related_name='documents') + objects = DocumentManager() class Meta: diff --git a/app/apps/core/urls.py b/app/apps/core/urls.py index 313b2e1e..84c04fd8 100644 --- a/app/apps/core/urls.py +++ b/app/apps/core/urls.py @@ -2,6 +2,9 @@ from django.urls import path from django.views.generic import TemplateView from core.views import (Home, + CreateProject, + ProjectList, + ProjectDetail, DocumentsList, DocumentDetail, CreateDocument, @@ -17,7 +20,13 @@ from core.views import (Home, urlpatterns = [ path('', Home.as_view(), name='home'), - path('documents/', DocumentsList.as_view(), name='documents-list'), + + path('projects/create/', CreateProject.as_view(), name='project-create'), + path('projects/', ProjectList.as_view(), name='projects-list'), + # path('project/<str:slug>/', ProjectDetail.as_view(), name='project-detail'), + path('project/<str:slug>/documents/', DocumentsList.as_view(), name='documents-list'), + + # path('documents/', DocumentsList.as_view(), name='documents-list'), path('document/<int:pk>/', DocumentDetail.as_view(), name='document-detail'), path('document/create/', CreateDocument.as_view(), name='document-create'), path('document/<int:pk>/edit/', UpdateDocument.as_view(), name='document-update'), diff --git a/app/apps/core/views.py b/app/apps/core/views.py index 1304370d..f6ae0424 100644 --- a/app/apps/core/views.py +++ b/app/apps/core/views.py @@ -13,9 +13,9 @@ from django.urls import reverse from django.views.generic import View, TemplateView, ListView, DetailView from django.views.generic import CreateView, UpdateView, DeleteView -from core.models import (Document, DocumentPart, Metadata, +from core.models import (Project, Document, DocumentPart, Metadata, OcrModel, AlreadyProcessingException) -from core.forms import (DocumentForm, MetadataFormSet, DocumentShareForm, +from core.forms import (ProjectForm, DocumentForm, MetadataFormSet, DocumentShareForm, UploadImageForm, DocumentProcessForm) from imports.forms import ImportForm, ExportForm @@ -33,16 +33,59 @@ class Home(TemplateView): return context +class ProjectList(LoginRequiredMixin, ListView): + model = Project + paginate_by = 10 + + def get_queryset(self): + return (Project.objects + .for_user(self.request.user) + .select_related('owner')) + + +class ProjectDetail(LoginRequiredMixin, DetailView): + model = Project + + def get_object(self): + obj = super().get_object() + try: + # we fetched the object already, now we check that the user has perms to edit it + Project.objects.for_user(self.request.user).get(pk=obj.pk) + except Document.DoesNotExist: + raise PermissionDenied + return obj + + +class CreateProject(LoginRequiredMixin, SuccessMessageMixin, CreateView): + model = Project + form_class = ProjectForm + success_message = _("Project created successfully!") + + def get_success_url(self): + return reverse('projects-list') + + def form_valid(self, form): + form.instance.owner = self.request.user + response = super().form_valid(form) + return response + + class DocumentsList(LoginRequiredMixin, ListView): model = Document paginate_by = 10 def get_queryset(self): - return (Document.objects - .for_user(self.request.user) + self.project = Project.objects.for_user(self.request.user).get(slug=self.kwargs['slug']) + return (Document.objects.filter(project=self.project) + # .for_user(self.request.user) .select_related('owner', 'main_script') .annotate(parts_updated_at=Max('parts__updated_at'))) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['project'] = self.project + return context + class DocumentMixin(): def get_success_url(self): @@ -90,8 +133,6 @@ class CreateDocument(LoginRequiredMixin, SuccessMessageMixin, DocumentMixin, Cre if form.is_valid() and metadata_form.is_valid(): return self.form_valid(form, metadata_form) else: - print(form.errors) - print(metadata_form.errors) return self.form_invalid(form) def form_valid(self, form, metadata_form): diff --git a/app/escriptorium/templates/base.html b/app/escriptorium/templates/base.html index ac673a3f..e88a8886 100644 --- a/app/escriptorium/templates/base.html +++ b/app/escriptorium/templates/base.html @@ -63,7 +63,7 @@ <ul class="navbar-nav mr-right"> {% if user.is_authenticated %} <li class="nav-item"> - <a class="nav-link {% block nav-doc-list-class %}{% endblock %}" href="{% url 'documents-list' %}">{% trans "My Documents" %}</a> + <a class="nav-link {% block nav-proj-list-class %}{% endblock %}" href="{% url 'projects-list' %}">{% trans "My Projects" %}</a> </li> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> diff --git a/app/escriptorium/templates/core/document_list.html b/app/escriptorium/templates/core/document_list.html index 3cd72eef..7a047a8d 100644 --- a/app/escriptorium/templates/core/document_list.html +++ b/app/escriptorium/templates/core/document_list.html @@ -2,17 +2,11 @@ {% load i18n staticfiles thumbnail %} {% block head_title %}{% trans "My Documents" %}{% endblock %} -{% block styles %} -{{ block.super }} -{% endblock %} - -{% block nav-doc-list-class %}active{% endblock %} - {% block body %} <div class="row"> - <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> + <div class="col-md-12 col-md-offset-4"> + <a href="{% url 'document-create' %}" class="btn btn-success float-sm-right mb-2">{% trans 'Create new Document' %}</a> + <h5><a href="{% url 'projects-list'%}">{% trans "Projects" %}</a> > {{ project.name }} > {% trans "Documents" %}</h5> <table id="document-list" class="table table-hover"> <tbody> diff --git a/app/escriptorium/templates/core/project_form.html b/app/escriptorium/templates/core/project_form.html new file mode 100644 index 00000000..36e10b24 --- /dev/null +++ b/app/escriptorium/templates/core/project_form.html @@ -0,0 +1,21 @@ +{% extends 'base.html' %} +{% load i18n staticfiles bootstrap json %} + +{% block head_title %}{% if object %}{% trans "Update a Project" %}{% else %}{% trans "Create a new Project" %}{% endif %}{% endblock %} + +{% block body %} + <form id="project-form" method="post"> + {% csrf_token %} + <fieldset> + <div class="form-group"> + {% for err in form.non_field_errors %} + <p class="error">{{ err }}</p> + {% endfor %} + </div> + + {% render_field form.name %} + + <input type="submit" value="{% if object %}{% trans 'Update' %}{% else %}{% trans 'Create' %}{% endif %}" class="btn btn-success btn-block my-3" {% if object %}autofocus{% endif %}> + </fieldset> + </form> +{% endblock %} diff --git a/app/escriptorium/templates/core/project_list.html b/app/escriptorium/templates/core/project_list.html new file mode 100644 index 00000000..a9751f98 --- /dev/null +++ b/app/escriptorium/templates/core/project_list.html @@ -0,0 +1,25 @@ +{% extends 'base.html' %} +{% load i18n staticfiles %} + +{% block head_title %}{% trans "My Projects" %}{% endblock %} + +{% block nav-proj-list-class %}active{% endblock %} + +{% block body %} +<div class="row"> + <div class="col-md-12 col-md-offset-4"> + <a href="{% url 'project-create' %}" class="btn btn-success float-sm-right">{% trans 'Create new Project' %}</a> + <h2>{% trans "My Projects" %}</h2> + + <table id="project-list" class="table table-hover"> + <tbody> + {% for project in project_list %} + <tr onclick="document.location='{% url 'documents-list' slug=project.slug %}'" role="button"> + <td><a href="{% url 'documents-list' slug=project.slug %}">{{project.name}}</a></td> + <td title="{% trans "Owner" %}">{{project.owner}}</td> + <td>{% blocktrans with count=project.documents.count %}{{ count }} document(s).{% endblocktrans %}</td> + </tr> + {% endfor %} + </tbody> + </table> +{% endblock %} diff --git a/docker-compose.yml b/docker-compose.yml index f8f155c8..f8eed4a9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,7 +32,7 @@ services: env_file: variables.env redis: - image: redis:3.0-alpine + image: sickp/alpine-redis:4.0.6 nginx: image: registry.gitlab.inria.fr/scripta/escriptorium/nginx @@ -120,4 +120,3 @@ volumes: media: postgres: esdata: - logs: -- GitLab