From 9d8d422d697f148cb5de6750412b2cd5fc74213d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Ml=C3=A1dek?= Date: Thu, 30 Jul 2020 23:43:39 +0200 Subject: [PATCH] allow multiple URLs per document --- poetry.lock | 14 +++++- pyproject.toml | 1 + sdbs_pile/pile/admin.py | 25 +++++++++- .../migrations/0013_auto_20200727_1533.py | 46 +++++++++++++++++++ sdbs_pile/pile/models.py | 26 +++++++---- sdbs_pile/pile/static/main.css | 6 ++- .../pile/templates/front_doc_detail.html | 18 ++++++++ sdbs_pile/pile/views.py | 4 +- sdbs_pile/settings.py | 3 +- 9 files changed, 128 insertions(+), 15 deletions(-) create mode 100644 sdbs_pile/pile/migrations/0013_auto_20200727_1533.py diff --git a/poetry.lock b/poetry.lock index 1f4e9ff..b592a50 100644 --- a/poetry.lock +++ b/poetry.lock @@ -155,6 +155,14 @@ version = "4.0.0" [package.dependencies] Django = ">=2.0.1" +[[package]] +category = "main" +description = "Allows Django models to be ordered and provides a simple admin interface for reordering them." +name = "django-ordered-model" +optional = false +python-versions = "*" +version = "3.4.1" + [[package]] category = "dev" description = "WSGI HTTP Server for UNIX" @@ -440,7 +448,7 @@ python-versions = "*" version = "0.5.1" [metadata] -content-hash = "aedaaacc2c26bf3618b35cd6ed67cbe4309526e18bb696cb37f81398e7cf9337" +content-hash = "c4c5d2ad677b97810bb47fc430207910d648ddc8ebc22de10715c58d8bb36f32" python-versions = "^3.8" [metadata.files] @@ -521,6 +529,10 @@ django-model-utils = [ {file = "django-model-utils-4.0.0.tar.gz", hash = "sha256:adf09e5be15122a7f4e372cb5a6dd512bbf8d78a23a90770ad0983ee9d909061"}, {file = "django_model_utils-4.0.0-py2.py3-none-any.whl", hash = "sha256:9cf882e5b604421b62dbe57ad2b18464dc9c8f963fc3f9831badccae66c1139c"}, ] +django-ordered-model = [ + {file = "django-ordered-model-3.4.1.tar.gz", hash = "sha256:d867166ed4dd12501139e119cbbc5b4d19798a3e72740aef0af4879ba97102cf"}, + {file = "django_ordered_model-3.4.1-py3-none-any.whl", hash = "sha256:29af6624cf3505daaf0df00e2df1d0726dd777b95e08f304d5ad0264092aa934"}, +] gunicorn = [ {file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"}, {file = "gunicorn-20.0.4.tar.gz", hash = "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626"}, diff --git a/pyproject.toml b/pyproject.toml index a48d32e..6db0a85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ weasyprint = "^51" pypdf2 = "^1.26.0" markdown2 = "^2.3.8" bleach = "^3.1.4" +django-ordered-model = "^3.4.1" [tool.poetry.dev-dependencies] ipython = "^7.13.0" diff --git a/sdbs_pile/pile/admin.py b/sdbs_pile/pile/admin.py index 92c484a..c2d58c0 100644 --- a/sdbs_pile/pile/admin.py +++ b/sdbs_pile/pile/admin.py @@ -1,7 +1,10 @@ from django import forms from django.contrib import admin +from django.core.exceptions import ValidationError +from django.forms import BaseInlineFormSet +from ordered_model.admin import OrderedInlineModelAdminMixin, OrderedTabularInline -from sdbs_pile.pile.models import Tag, Document +from sdbs_pile.pile.models import Tag, Document, DocumentLink class TagAdmin(admin.ModelAdmin): @@ -13,6 +16,23 @@ class TagAdmin(admin.ModelAdmin): return tag.documents.count() +class DocumentLinkFormset(BaseInlineFormSet): + def clean(self): + super(DocumentLinkFormset, self).clean() + has_url = any((form.cleaned_data.get('url') and not form.cleaned_data['DELETE']) for form in self.forms) + if not (self.instance.file or has_url): + raise ValidationError("An uploaded document or at least one external URL is required.") + + +class DocumentLinkAdmin(OrderedTabularInline): + model = DocumentLink + formset = DocumentLinkFormset + fields = ('description', 'url', 'move_up_down_links') + readonly_fields = ('move_up_down_links',) + extra = 1 + ordering = ('order',) + + class DocumentExternalListFilter(admin.SimpleListFilter): title = 'document location' parameter_name = 'external' @@ -44,13 +64,14 @@ class DocumentAdminForm(forms.ModelForm): self.fields['related'].queryset = Document.objects.exclude(pk=self.instance.pk) -class DocumentAdmin(admin.ModelAdmin): +class DocumentAdmin(OrderedInlineModelAdminMixin, admin.ModelAdmin): exclude = ('is_removed',) list_display = ('title', 'author', 'published', 'media_type', 'status', 'has_file', 'public', 'filed_under') list_filter = ('tags', 'media_type', 'status', DocumentExternalListFilter, 'public') search_fields = ('title', 'author', 'published') actions = ('make_published', 'make_hidden') form = DocumentAdminForm + inlines = (DocumentLinkAdmin,) def has_file(self, document: Document): return document.file is not None and str(document.file).strip() != '' diff --git a/sdbs_pile/pile/migrations/0013_auto_20200727_1533.py b/sdbs_pile/pile/migrations/0013_auto_20200727_1533.py new file mode 100644 index 0000000..66a5e41 --- /dev/null +++ b/sdbs_pile/pile/migrations/0013_auto_20200727_1533.py @@ -0,0 +1,46 @@ +# Generated by Django 3.0.4 on 2020-07-27 13:33 + +import django.db.models.deletion +from django.db import migrations, models + + +def copy_links_to_models(apps, _): + Document = apps.get_model("pile", "Document") + DocumentLink = apps.get_model("pile", "DocumentLink") + + for document in Document.objects.all(): + if document.external_url: + DocumentLink.objects.create( + document=document, + url=document.external_url, + order=1 + ) + + +class Migration(migrations.Migration): + dependencies = [ + ('pile', '0012_auto_20200610_1013'), + ] + + operations = [ + migrations.CreateModel( + name='DocumentLink', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.PositiveIntegerField(db_index=True, editable=False, verbose_name='order')), + ('url', models.URLField()), + ('description', models.CharField(blank=True, max_length=512, null=True)), + ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='urls', + to='pile.Document')), + ], + options={ + 'ordering': ('order',), + 'abstract': False, + }, + ), + migrations.RunPython(copy_links_to_models, reverse_code=lambda: None), + migrations.RemoveField( + model_name='document', + name='external_url', + ), + ] diff --git a/sdbs_pile/pile/models.py b/sdbs_pile/pile/models.py index 9907acd..430a855 100644 --- a/sdbs_pile/pile/models.py +++ b/sdbs_pile/pile/models.py @@ -6,6 +6,7 @@ from django.db import models from django.db.models import Count, Q from model_utils.managers import SoftDeletableManager, SoftDeletableQuerySet from model_utils.models import SoftDeletableModel +from ordered_model.models import OrderedModel class Tag(SoftDeletableModel): @@ -21,10 +22,10 @@ class DocumentQuerySet(SoftDeletableQuerySet): return super().annotate(tag_count=Count('tags')).filter(tag_count=0) def local(self): - return super().filter((Q(file__isnull=False) & ~Q(file='')) | Q(external_url__contains="pile.sdbs.cz")) + return super().filter((Q(file__isnull=False) & ~Q(file='')) | Q(urls__url__contains="pile.sdbs.cz")) def external(self): - return super().filter((Q(file__isnull=True) | Q(file='')) & ~Q(external_url__contains="pile.sdbs.cz")) + return super().filter((Q(file__isnull=True) | Q(file='')) & ~Q(urls__url__contains="pile.sdbs.cz")) class DocumentManager(SoftDeletableManager): @@ -48,7 +49,6 @@ class Document(SoftDeletableModel): author = models.CharField(max_length=512, null=False, blank=True) published = models.CharField(max_length=128, null=False, blank=True) description = models.TextField(max_length=2048, null=False, blank=True) - external_url = models.URLField(null=True, blank=True) file = models.FileField(null=True, blank=True, storage=FileSystemStorage(location='docs')) public = models.BooleanField(default=True, null=False, blank=False) media_type = models.CharField(null=False, blank=False, @@ -73,7 +73,7 @@ class Document(SoftDeletableModel): def url(self): if self.file: return f"/docs/{self.file.url}" - return self.external_url + return self.urls.first() @property def is_local_pdf(self): @@ -89,9 +89,19 @@ class Document(SoftDeletableModel): from django.urls import reverse return reverse('pile:document', args=[str(self.id)]) - def clean(self): - if not (self.file or self.external_url): - raise ValidationError("An uploaded document or an external URL is required.") - def __str__(self): return f"{self.title}{f' ({self.author})' if self.author else ''}" + + +class DocumentLink(OrderedModel): + document = models.ForeignKey(Document, related_name="urls", on_delete=models.CASCADE) + url = models.URLField(null=False, blank=False) + description = models.CharField(max_length=512, null=True, blank=True) + + order_with_respect_to = 'document' + + class Meta(OrderedModel.Meta): + pass + + def __str__(self): + return f"{self.description} - {self.url}" if self.description else self.url diff --git a/sdbs_pile/pile/static/main.css b/sdbs_pile/pile/static/main.css index 9e1bf26..a09dd94 100755 --- a/sdbs_pile/pile/static/main.css +++ b/sdbs_pile/pile/static/main.css @@ -189,7 +189,7 @@ ul > li:before { margin: .5em 0 0 0; } -.doc-link-intro:before { +.doc-link-intro:before, .doc-link-plus:before { content: "➜ "; } @@ -197,6 +197,10 @@ ul > li:before { text-decoration: underline; } +.doc-link-plus { + margin-left: 1em; +} + @media screen and (min-width: 64em ) { #sidebar { position: absolute; diff --git a/sdbs_pile/pile/templates/front_doc_detail.html b/sdbs_pile/pile/templates/front_doc_detail.html index f3504cf..e1550f9 100644 --- a/sdbs_pile/pile/templates/front_doc_detail.html +++ b/sdbs_pile/pile/templates/front_doc_detail.html @@ -45,6 +45,15 @@ Entry #{{ document.id }} of /-\ pile + {% if document.urls.count > 1 %} + + {% for link in document.urls.all|slice:"1:" %} + + {% endfor %} + {% endif %} + + {% if document.urls.count > 1 %} + + {% for link in document.urls.all|slice:"1:" %} + + {% endfor %} + {% endif %} +