allow multiple URLs per document
This commit is contained in:
parent
6c8963531b
commit
9d8d422d69
9 changed files with 128 additions and 15 deletions
14
poetry.lock
generated
14
poetry.lock
generated
|
@ -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"},
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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() != ''
|
||||
|
|
46
sdbs_pile/pile/migrations/0013_auto_20200727_1533.py
Normal file
46
sdbs_pile/pile/migrations/0013_auto_20200727_1533.py
Normal 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',
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -37,7 +37,8 @@ INSTALLED_APPS = [
|
|||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'sdbs_pile.pile'
|
||||
'sdbs_pile.pile',
|
||||
'ordered_model'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
|
Loading…
Reference in a new issue