mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-10-31 22:19:00 +00:00
Merge branch 'main' into production
This commit is contained in:
commit
56c6ee8879
15 changed files with 211 additions and 20 deletions
|
@ -225,7 +225,7 @@ class LinkDomainForm(CustomForm):
|
|||
class FileLinkForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.FileLink
|
||||
fields = ["url", "filetype", "book", "added_by"]
|
||||
fields = ["url", "filetype", "availability", "book", "added_by"]
|
||||
|
||||
|
||||
class EditionForm(CustomForm):
|
||||
|
|
32
bookwyrm/migrations/0129_auto_20220117_1716.py
Normal file
32
bookwyrm/migrations/0129_auto_20220117_1716.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Generated by Django 3.2.10 on 2022-01-17 17:16
|
||||
|
||||
import bookwyrm.models.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0128_merge_0126_auto_20220112_2315_0127_auto_20220110_2211"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="filelink",
|
||||
name="availability",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
choices=[
|
||||
("free", "Free"),
|
||||
("purchase", "Purchasable"),
|
||||
("loan", "Available for loan"),
|
||||
],
|
||||
default="free",
|
||||
max_length=100,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="filelink",
|
||||
name="filetype",
|
||||
field=bookwyrm.models.fields.CharField(max_length=50),
|
||||
),
|
||||
]
|
|
@ -47,13 +47,23 @@ class Link(ActivitypubMixin, BookWyrmModel):
|
|||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
AvailabilityChoices = [
|
||||
("free", _("Free")),
|
||||
("purchase", _("Purchasable")),
|
||||
("loan", _("Available for loan")),
|
||||
]
|
||||
|
||||
|
||||
class FileLink(Link):
|
||||
"""a link to a file"""
|
||||
|
||||
book = models.ForeignKey(
|
||||
"Book", on_delete=models.CASCADE, related_name="file_links", null=True
|
||||
)
|
||||
filetype = fields.CharField(max_length=5, activitypub_field="mediaType")
|
||||
filetype = fields.CharField(max_length=50, activitypub_field="mediaType")
|
||||
availability = fields.CharField(
|
||||
max_length=100, choices=AvailabilityChoices, default="free"
|
||||
)
|
||||
|
||||
|
||||
StatusChoices = [
|
||||
|
|
|
@ -14,7 +14,7 @@ VERSION = "0.2.0"
|
|||
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
||||
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
||||
|
||||
JS_CACHE = "a47cc2ca"
|
||||
JS_CACHE = "76c5ff1f"
|
||||
|
||||
# email
|
||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||
|
|
|
@ -722,6 +722,7 @@ ol.ordered-list li::before {
|
|||
|
||||
.overflow-wrap-anywhere {
|
||||
overflow-wrap: anywhere;
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
/* Threads
|
||||
|
|
|
@ -42,6 +42,8 @@
|
|||
|
||||
function getSuggestions(input, trie) {
|
||||
// Follow the trie through the provided input
|
||||
input = input.toLowerCase();
|
||||
|
||||
input.split("").forEach((letter) => {
|
||||
if (!trie) {
|
||||
return;
|
||||
|
@ -160,6 +162,23 @@ const tries = {
|
|||
},
|
||||
},
|
||||
},
|
||||
r: {
|
||||
i: {
|
||||
n: {
|
||||
t: {
|
||||
" ": {
|
||||
b: {
|
||||
o: {
|
||||
o: {
|
||||
k: "Print book",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<input
|
||||
type="text"
|
||||
name="filetype"
|
||||
maxlength="5"
|
||||
maxlength="50"
|
||||
class="input"
|
||||
required=""
|
||||
id="id_filetype"
|
||||
|
@ -43,6 +43,14 @@
|
|||
{% include 'snippets/form_errors.html' with errors_list=file_link_form.filetype.errors id="desc_filetype" %}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="id_availability">
|
||||
{% trans "Availability:" %}
|
||||
</label>
|
||||
<div class="select">
|
||||
{{ file_link_form.availability }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -33,9 +33,10 @@
|
|||
<th>{% trans "Added by" %}</th>
|
||||
<th>{% trans "Filetype" %}</th>
|
||||
<th>{% trans "Domain" %}</th>
|
||||
<th>{% trans "Actions" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th colspan="2">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
{% for link in book.file_links.all %}
|
||||
{% for link in links %}
|
||||
<tr>
|
||||
<td class="overflow-wrap-anywhere">
|
||||
<a href="{{ link.url }}" target="_blank" rel="noopener">{{ link.url }}</a>
|
||||
|
@ -47,13 +48,44 @@
|
|||
{{ link.filelink.filetype }}
|
||||
</td>
|
||||
<td>
|
||||
{{ link.domain.name }} ({{ link.domain.get_status_display }})
|
||||
{{ link.domain.name }}
|
||||
<p>
|
||||
<a href="{% url 'report-link' link.added_by.id link.id %}">{% trans "Report spam" %}</a>
|
||||
</p>
|
||||
</td>
|
||||
<td>
|
||||
<form name="delete-link-{{ link.id }}" class="control" method="post" action="{% url 'file-link' book.id link.id %}">
|
||||
{% with status=link.domain.status %}
|
||||
<span class="tag {% if status == 'blocked' %}has-background-danger{% elif status == 'approved' %}has-background-primary{% endif %}">
|
||||
<span class="icon" aria-hidden="true">
|
||||
<span class="icon-{% if status == 'blocked' %}x{% elif status == 'approved' %}check{% else %}lock{% endif %}"></span>
|
||||
</span>
|
||||
<span>
|
||||
{{ link.domain.get_status_display }}
|
||||
</span>
|
||||
</span>
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td>
|
||||
<form name="edit-link" class="control" method="post" action="{% url 'file-link' book.id link.id %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="url" value="{{ link.form.url.value }}">
|
||||
<input type="hidden" name="filetype" value="{{ link.form.filetype.value }}">
|
||||
<input type="hidden" name="added_by" value="{{ link.form.added_by.value }}">
|
||||
<input type="hidden" name="book" value="{{ link.form.book.value }}">
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
{{ link.form.availability }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form name="delete-link-{{ link.id }}" class="control" method="post" action="{% url 'file-link-delete' book.id link.id %}">
|
||||
{% csrf_token %}
|
||||
<button class="button is-danger is-light" type="submit">Delete link</button>
|
||||
</form>
|
||||
|
|
|
@ -30,6 +30,12 @@
|
|||
<li>
|
||||
<a href="{{ link.url }}" rel="noopener" target="_blank" title="{{ link.url }}" data-modal-open="{{ verify_modal }}">{{ link.name }}</a>
|
||||
({{ link.filetype }})
|
||||
|
||||
{% if link.availability != "free" %}
|
||||
<p class="help">
|
||||
{{ link.get_availability_display }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
@ -46,7 +52,7 @@
|
|||
<span class="icon icon-pencil" aria-hidden="true"></span>
|
||||
<span>{% trans "Edit links" %}</span>
|
||||
</a>
|
||||
{% include 'book/file_links/add_link_modal.html' with book=book id="add-links" %}
|
||||
{% endif %}
|
||||
{% include 'book/file_links/add_link_modal.html' with book=book id="add-links" %}
|
||||
|
||||
{% endif %}
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
<div class="column is-narrow">
|
||||
<button type="button" class="button" data-modal-open="{{ domain_modal }}">
|
||||
<span class="icon icon-pencil m-0-mobile" aria-hidden="treu"></span>
|
||||
<span>{% trans "Set display name" %}</span>
|
||||
<span class="is-sr-only-mobile">{% trans "Set display name" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -67,6 +67,7 @@ class LinkViews(TestCase):
|
|||
form.data["filetype"] = "HTML"
|
||||
form.data["book"] = self.book.id
|
||||
form.data["added_by"] = self.local_user.id
|
||||
form.data["availability"] = "loan"
|
||||
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
|
@ -87,3 +88,59 @@ class LinkViews(TestCase):
|
|||
|
||||
self.book.refresh_from_db()
|
||||
self.assertEqual(self.book.file_links.first(), link)
|
||||
|
||||
def test_book_links(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.BookFileLinks.as_view()
|
||||
models.FileLink.objects.create(
|
||||
book=self.book,
|
||||
added_by=self.local_user,
|
||||
url="https://www.hello.com",
|
||||
)
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
result = view(request, self.book.id)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
validate_html(result.render())
|
||||
|
||||
def test_book_links_post(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
link = models.FileLink.objects.create(
|
||||
book=self.book,
|
||||
added_by=self.local_user,
|
||||
url="https://www.hello.com",
|
||||
)
|
||||
view = views.BookFileLinks.as_view()
|
||||
form = forms.FileLinkForm()
|
||||
form.data["url"] = link.url
|
||||
form.data["filetype"] = "HTML"
|
||||
form.data["book"] = self.book.id
|
||||
form.data["added_by"] = self.local_user.id
|
||||
form.data["availability"] = "loan"
|
||||
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
view(request, self.book.id, link.id)
|
||||
|
||||
link.refresh_from_db()
|
||||
self.assertEqual(link.filetype, "HTML")
|
||||
self.assertEqual(link.availability, "loan")
|
||||
|
||||
def test_delete_link(self):
|
||||
"""remove a link"""
|
||||
link = models.FileLink.objects.create(
|
||||
book=self.book,
|
||||
added_by=self.local_user,
|
||||
url="https://www.hello.com",
|
||||
)
|
||||
form = forms.FileLinkForm()
|
||||
form.data["url"] = "https://www.example.com"
|
||||
form.data["filetype"] = "HTML"
|
||||
form.data["book"] = self.book.id
|
||||
form.data["added_by"] = self.local_user.id
|
||||
form.data["availability"] = "loan"
|
||||
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
views.delete_link(request, self.book.id, link.id)
|
||||
self.assertFalse(models.FileLink.objects.exists())
|
||||
|
|
|
@ -485,10 +485,15 @@ urlpatterns = [
|
|||
rf"{BOOK_PATH}/filelink/?$", views.BookFileLinks.as_view(), name="file-link"
|
||||
),
|
||||
re_path(
|
||||
rf"{BOOK_PATH}/filelink/(?P<link_id>\d+)/delete/?$",
|
||||
rf"{BOOK_PATH}/filelink/(?P<link_id>\d+)/?$",
|
||||
views.BookFileLinks.as_view(),
|
||||
name="file-link",
|
||||
),
|
||||
re_path(
|
||||
rf"{BOOK_PATH}/filelink/(?P<link_id>\d+)/delete/?$",
|
||||
views.delete_link,
|
||||
name="file-link-delete",
|
||||
),
|
||||
re_path(
|
||||
rf"{BOOK_PATH}/filelink/add/?$",
|
||||
views.AddFileLink.as_view(),
|
||||
|
|
|
@ -37,7 +37,7 @@ from .books.books import (
|
|||
from .books.books import update_book_from_remote
|
||||
from .books.edit_book import EditBook, ConfirmEditBook
|
||||
from .books.editions import Editions, switch_edition
|
||||
from .books.links import BookFileLinks, AddFileLink
|
||||
from .books.links import BookFileLinks, AddFileLink, delete_link
|
||||
|
||||
# landing
|
||||
from .landing.about import about, privacy, conduct
|
||||
|
|
|
@ -20,9 +20,9 @@ class LinkDomain(View):
|
|||
def get(self, request, status="pending"):
|
||||
"""view pending domains"""
|
||||
data = {
|
||||
"domains": models.LinkDomain.objects.filter(status=status).prefetch_related(
|
||||
"links"
|
||||
),
|
||||
"domains": models.LinkDomain.objects.filter(status=status)
|
||||
.prefetch_related("links")
|
||||
.order_by("-created_date"),
|
||||
"counts": {
|
||||
"pending": models.LinkDomain.objects.filter(status="pending").count(),
|
||||
"approved": models.LinkDomain.objects.filter(status="approved").count(),
|
||||
|
|
|
@ -5,28 +5,49 @@ from django.shortcuts import get_object_or_404, redirect
|
|||
from django.template.response import TemplateResponse
|
||||
from django.views import View
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import forms, models
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
|
||||
)
|
||||
class BookFileLinks(View):
|
||||
"""View all links"""
|
||||
|
||||
def get(self, request, book_id):
|
||||
"""view links"""
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
return TemplateResponse(
|
||||
request, "book/file_links/edit_links.html", {"book": book}
|
||||
)
|
||||
links = book.file_links.order_by("domain__status", "created_date")
|
||||
annotated_links = []
|
||||
for link in links.all():
|
||||
link.form = forms.FileLinkForm(instance=link)
|
||||
annotated_links.append(link)
|
||||
|
||||
data = {"book": book, "links": annotated_links}
|
||||
return TemplateResponse(request, "book/file_links/edit_links.html", data)
|
||||
|
||||
def post(self, request, book_id, link_id):
|
||||
"""delete link"""
|
||||
"""Edit a link"""
|
||||
link = get_object_or_404(models.FileLink, id=link_id, book=book_id)
|
||||
link.delete()
|
||||
form = forms.FileLinkForm(request.POST, instance=link)
|
||||
form.save()
|
||||
return self.get(request, book_id)
|
||||
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
# pylint: disable=unused-argument
|
||||
def delete_link(request, book_id, link_id):
|
||||
"""delete link"""
|
||||
link = get_object_or_404(models.FileLink, id=link_id, book=book_id)
|
||||
link.delete()
|
||||
return redirect("file-link", book_id)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
|
||||
|
|
Loading…
Reference in a new issue