mirror of
https://git.sdbs.cz/sdbs/pile.git
synced 2025-05-10 00:12:18 +00:00
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]
|
[package.dependencies]
|
||||||
Django = ">=2.0.1"
|
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]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "WSGI HTTP Server for UNIX"
|
description = "WSGI HTTP Server for UNIX"
|
||||||
|
@ -440,7 +448,7 @@ python-versions = "*"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
content-hash = "aedaaacc2c26bf3618b35cd6ed67cbe4309526e18bb696cb37f81398e7cf9337"
|
content-hash = "c4c5d2ad677b97810bb47fc430207910d648ddc8ebc22de10715c58d8bb36f32"
|
||||||
python-versions = "^3.8"
|
python-versions = "^3.8"
|
||||||
|
|
||||||
[metadata.files]
|
[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.tar.gz", hash = "sha256:adf09e5be15122a7f4e372cb5a6dd512bbf8d78a23a90770ad0983ee9d909061"},
|
||||||
{file = "django_model_utils-4.0.0-py2.py3-none-any.whl", hash = "sha256:9cf882e5b604421b62dbe57ad2b18464dc9c8f963fc3f9831badccae66c1139c"},
|
{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 = [
|
gunicorn = [
|
||||||
{file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"},
|
{file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"},
|
||||||
{file = "gunicorn-20.0.4.tar.gz", hash = "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626"},
|
{file = "gunicorn-20.0.4.tar.gz", hash = "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626"},
|
||||||
|
|
|
@ -12,6 +12,7 @@ weasyprint = "^51"
|
||||||
pypdf2 = "^1.26.0"
|
pypdf2 = "^1.26.0"
|
||||||
markdown2 = "^2.3.8"
|
markdown2 = "^2.3.8"
|
||||||
bleach = "^3.1.4"
|
bleach = "^3.1.4"
|
||||||
|
django-ordered-model = "^3.4.1"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
ipython = "^7.13.0"
|
ipython = "^7.13.0"
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
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):
|
class TagAdmin(admin.ModelAdmin):
|
||||||
|
@ -13,6 +16,23 @@ class TagAdmin(admin.ModelAdmin):
|
||||||
return tag.documents.count()
|
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):
|
class DocumentExternalListFilter(admin.SimpleListFilter):
|
||||||
title = 'document location'
|
title = 'document location'
|
||||||
parameter_name = 'external'
|
parameter_name = 'external'
|
||||||
|
@ -44,13 +64,14 @@ class DocumentAdminForm(forms.ModelForm):
|
||||||
self.fields['related'].queryset = Document.objects.exclude(pk=self.instance.pk)
|
self.fields['related'].queryset = Document.objects.exclude(pk=self.instance.pk)
|
||||||
|
|
||||||
|
|
||||||
class DocumentAdmin(admin.ModelAdmin):
|
class DocumentAdmin(OrderedInlineModelAdminMixin, admin.ModelAdmin):
|
||||||
exclude = ('is_removed',)
|
exclude = ('is_removed',)
|
||||||
list_display = ('title', 'author', 'published', 'media_type', 'status', 'has_file', 'public', 'filed_under')
|
list_display = ('title', 'author', 'published', 'media_type', 'status', 'has_file', 'public', 'filed_under')
|
||||||
list_filter = ('tags', 'media_type', 'status', DocumentExternalListFilter, 'public')
|
list_filter = ('tags', 'media_type', 'status', DocumentExternalListFilter, 'public')
|
||||||
search_fields = ('title', 'author', 'published')
|
search_fields = ('title', 'author', 'published')
|
||||||
actions = ('make_published', 'make_hidden')
|
actions = ('make_published', 'make_hidden')
|
||||||
form = DocumentAdminForm
|
form = DocumentAdminForm
|
||||||
|
inlines = (DocumentLinkAdmin,)
|
||||||
|
|
||||||
def has_file(self, document: Document):
|
def has_file(self, document: Document):
|
||||||
return document.file is not None and str(document.file).strip() != ''
|
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 django.db.models import Count, Q
|
||||||
from model_utils.managers import SoftDeletableManager, SoftDeletableQuerySet
|
from model_utils.managers import SoftDeletableManager, SoftDeletableQuerySet
|
||||||
from model_utils.models import SoftDeletableModel
|
from model_utils.models import SoftDeletableModel
|
||||||
|
from ordered_model.models import OrderedModel
|
||||||
|
|
||||||
|
|
||||||
class Tag(SoftDeletableModel):
|
class Tag(SoftDeletableModel):
|
||||||
|
@ -21,10 +22,10 @@ class DocumentQuerySet(SoftDeletableQuerySet):
|
||||||
return super().annotate(tag_count=Count('tags')).filter(tag_count=0)
|
return super().annotate(tag_count=Count('tags')).filter(tag_count=0)
|
||||||
|
|
||||||
def local(self):
|
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):
|
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):
|
class DocumentManager(SoftDeletableManager):
|
||||||
|
@ -48,7 +49,6 @@ class Document(SoftDeletableModel):
|
||||||
author = models.CharField(max_length=512, null=False, blank=True)
|
author = models.CharField(max_length=512, null=False, blank=True)
|
||||||
published = models.CharField(max_length=128, null=False, blank=True)
|
published = models.CharField(max_length=128, null=False, blank=True)
|
||||||
description = models.TextField(max_length=2048, 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'))
|
file = models.FileField(null=True, blank=True, storage=FileSystemStorage(location='docs'))
|
||||||
public = models.BooleanField(default=True, null=False, blank=False)
|
public = models.BooleanField(default=True, null=False, blank=False)
|
||||||
media_type = models.CharField(null=False, blank=False,
|
media_type = models.CharField(null=False, blank=False,
|
||||||
|
@ -73,7 +73,7 @@ class Document(SoftDeletableModel):
|
||||||
def url(self):
|
def url(self):
|
||||||
if self.file:
|
if self.file:
|
||||||
return f"/docs/{self.file.url}"
|
return f"/docs/{self.file.url}"
|
||||||
return self.external_url
|
return self.urls.first()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_local_pdf(self):
|
def is_local_pdf(self):
|
||||||
|
@ -89,9 +89,19 @@ class Document(SoftDeletableModel):
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
return reverse('pile:document', args=[str(self.id)])
|
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):
|
def __str__(self):
|
||||||
return f"{self.title}{f' ({self.author})' if self.author else ''}"
|
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;
|
margin: .5em 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-link-intro:before {
|
.doc-link-intro:before, .doc-link-plus:before {
|
||||||
content: "➜ ";
|
content: "➜ ";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,6 +197,10 @@ ul > li:before {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.doc-link-plus {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 64em ) {
|
@media screen and (min-width: 64em ) {
|
||||||
#sidebar {
|
#sidebar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -45,6 +45,15 @@
|
||||||
<a href="{% url "pile:retrieve" document.id %}">Entry #{{ document.id }} of /-\ pile</a>
|
<a href="{% url "pile:retrieve" document.id %}">Entry #{{ document.id }} of /-\ pile</a>
|
||||||
</div>
|
</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">
|
<div class="doc-link">
|
||||||
<span class="doc-link-intro">Get original document at: </span>
|
<span class="doc-link-intro">Get original document at: </span>
|
||||||
<a href="{{ document.url }}">{{ document.url }}</a>
|
<a href="{{ document.url }}">{{ document.url }}</a>
|
||||||
|
@ -55,6 +64,15 @@
|
||||||
<a href="{{ document.url }}">{{ document.url }}</a>
|
<a href="{{ document.url }}">{{ document.url }}</a>
|
||||||
</div>
|
</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">
|
<div class="doc-link">
|
||||||
<span class="doc-link-intro">Get label for file at:</span>
|
<span class="doc-link-intro">Get label for file at:</span>
|
||||||
<a href="{% url "pile:label" document.id %}">{% url "pile:label" document.id %}</a>
|
<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 import View
|
||||||
from django.views.generic import TemplateView
|
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):
|
class BasePileView(TemplateView):
|
||||||
|
@ -173,7 +173,7 @@ class RecentlyUploadedFeed(Feed):
|
||||||
|
|
||||||
|
|
||||||
def ExternalLinkView(request: HttpRequest):
|
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]
|
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')
|
return HttpResponse("\n".join(external_links), content_type='text/plain')
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,8 @@ INSTALLED_APPS = [
|
||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'sdbs_pile.pile'
|
'sdbs_pile.pile',
|
||||||
|
'ordered_model'
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|
Loading…
Add table
Reference in a new issue