allow multiple URLs per document

This commit is contained in:
Tomáš Mládek 2020-07-30 23:43:39 +02:00
parent 6c8963531b
commit 9d8d422d69
9 changed files with 128 additions and 15 deletions

14
poetry.lock generated
View file

@ -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"},

View file

@ -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"

View file

@ -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() != ''

View file

@ -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',
),
]

View file

@ -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

View file

@ -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;

View file

@ -45,6 +45,15 @@
<a href="{% url "pile:retrieve" document.id %}">Entry #{{ document.id }} of /-\ pile</a>
</div>
{% if document.urls.count > 1 %}
<div class="doc-link-intro">Also see:</div>
{% for link in document.urls.all|slice:"1:" %}
<div class="doc-link doc-link-plus">
<a href="{{ link.description|default:link.url }}">{{ link.url }}</a>
</div>
{% endfor %}
{% endif %}
<div class="doc-link">
<span class="doc-link-intro">Get original document at: </span>
<a href="{{ document.url }}">{{ document.url }}</a>
@ -55,6 +64,15 @@
<a href="{{ document.url }}">{{ document.url }}</a>
</div>
{% if document.urls.count > 1 %}
<div class="doc-link-intro">Also at:</div>
{% for link in document.urls.all|slice:"1:" %}
<div class="doc-link doc-link-plus">
<a href="{{ link.description|default:link.url }}">{{ link.url }}</a>
</div>
{% endfor %}
{% endif %}
<div class="doc-link">
<span class="doc-link-intro">Get label for file at:</span>
<a href="{% url "pile:label" document.id %}">{% url "pile:label" document.id %}</a>

View file

@ -17,7 +17,7 @@ from django.utils.text import slugify
from django.views import View
from django.views.generic import TemplateView
from sdbs_pile.pile.models import Tag, Document
from sdbs_pile.pile.models import Tag, Document, DocumentLink
class BasePileView(TemplateView):
@ -173,7 +173,7 @@ class RecentlyUploadedFeed(Feed):
def ExternalLinkView(request: HttpRequest):
external_links = Document.objects.all().external().values_list("external_url", flat=True)
external_links = DocumentLink.objects.order_by('order', '-document_id').values_list("url", flat=True)
external_links = [link for link in external_links if "pile.sdbs.cz" not in link]
return HttpResponse("\n".join(external_links), content_type='text/plain')

View file

@ -37,7 +37,8 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'sdbs_pile.pile'
'sdbs_pile.pile',
'ordered_model'
]
MIDDLEWARE = [