forked from mirrors/bookwyrm
Merge pull request #1527 from bookwyrm-social/book-file-links
Book file links
This commit is contained in:
commit
0c2537e27a
53 changed files with 1681 additions and 206 deletions
|
@ -20,22 +20,6 @@ class ActivityEncoder(JSONEncoder):
|
|||
return o.__dict__
|
||||
|
||||
|
||||
@dataclass
|
||||
class Link:
|
||||
"""for tagging a book in a status"""
|
||||
|
||||
href: str
|
||||
name: str
|
||||
type: str = "Link"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Mention(Link):
|
||||
"""a subtype of Link for mentioning an actor"""
|
||||
|
||||
type: str = "Mention"
|
||||
|
||||
|
||||
@dataclass
|
||||
# pylint: disable=invalid-name
|
||||
class Signature:
|
||||
|
@ -198,8 +182,9 @@ class ActivityObject:
|
|||
)
|
||||
return instance
|
||||
|
||||
def serialize(self):
|
||||
def serialize(self, **kwargs):
|
||||
"""convert to dictionary with context attr"""
|
||||
omit = kwargs.get("omit", ())
|
||||
data = self.__dict__.copy()
|
||||
# recursively serialize
|
||||
for (k, v) in data.items():
|
||||
|
@ -208,7 +193,8 @@ class ActivityObject:
|
|||
data[k] = v.serialize()
|
||||
except TypeError:
|
||||
pass
|
||||
data = {k: v for (k, v) in data.items() if v is not None}
|
||||
data = {k: v for (k, v) in data.items() if v is not None and k not in omit}
|
||||
if "@context" not in omit:
|
||||
data["@context"] = "https://www.w3.org/ns/activitystreams"
|
||||
return data
|
||||
|
||||
|
@ -222,7 +208,6 @@ def set_related_field(
|
|||
model = apps.get_model(f"bookwyrm.{model_name}", require_ready=True)
|
||||
origin_model = apps.get_model(f"bookwyrm.{origin_model_name}", require_ready=True)
|
||||
|
||||
with transaction.atomic():
|
||||
if isinstance(data, str):
|
||||
existing = model.find_existing_by_remote_id(data)
|
||||
if existing:
|
||||
|
@ -241,9 +226,7 @@ def set_related_field(
|
|||
# edition.parentWork = instance, for example
|
||||
model_field = getattr(model, related_field_name)
|
||||
if hasattr(model_field, "activitypub_field"):
|
||||
setattr(
|
||||
activity, getattr(model_field, "activitypub_field"), instance.remote_id
|
||||
)
|
||||
setattr(activity, getattr(model_field, "activitypub_field"), instance.remote_id)
|
||||
item = activity.to_model()
|
||||
|
||||
# if the related field isn't serialized (attachments on Status), then
|
||||
|
@ -304,3 +287,27 @@ def resolve_remote_id(
|
|||
|
||||
# if we're refreshing, "result" will be set and we'll update it
|
||||
return item.to_model(model=model, instance=result, save=save)
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Link(ActivityObject):
|
||||
"""for tagging a book in a status"""
|
||||
|
||||
href: str
|
||||
name: str = None
|
||||
mediaType: str = None
|
||||
id: str = None
|
||||
attributedTo: str = None
|
||||
type: str = "Link"
|
||||
|
||||
def serialize(self, **kwargs):
|
||||
"""remove fields"""
|
||||
omit = ("id", "type", "@context")
|
||||
return super().serialize(omit=omit)
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Mention(Link):
|
||||
"""a subtype of Link for mentioning an actor"""
|
||||
|
||||
type: str = "Mention"
|
||||
|
|
|
@ -17,6 +17,8 @@ class BookData(ActivityObject):
|
|||
goodreadsKey: str = None
|
||||
bnfId: str = None
|
||||
lastEditedBy: str = None
|
||||
links: List[str] = field(default_factory=lambda: [])
|
||||
fileLinks: List[str] = field(default_factory=lambda: [])
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
|
|
@ -15,6 +15,11 @@ class PublicKey(ActivityObject):
|
|||
publicKeyPem: str
|
||||
type: str = "PublicKey"
|
||||
|
||||
def serialize(self, **kwargs):
|
||||
"""remove fields"""
|
||||
omit = ("type", "@context")
|
||||
return super().serialize(omit=omit)
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@dataclass(init=False)
|
||||
|
|
|
@ -216,6 +216,18 @@ class CoverForm(CustomForm):
|
|||
help_texts = {f: None for f in fields}
|
||||
|
||||
|
||||
class LinkDomainForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.LinkDomain
|
||||
fields = ["name"]
|
||||
|
||||
|
||||
class FileLinkForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.FileLink
|
||||
fields = ["url", "filetype", "book", "added_by"]
|
||||
|
||||
|
||||
class EditionForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Edition
|
||||
|
@ -230,6 +242,8 @@ class EditionForm(CustomForm):
|
|||
"shelves",
|
||||
"connector",
|
||||
"search_vector",
|
||||
"links",
|
||||
"file_links",
|
||||
]
|
||||
widgets = {
|
||||
"title": forms.TextInput(attrs={"aria-describedby": "desc_title"}),
|
||||
|
@ -439,7 +453,7 @@ class GroupForm(CustomForm):
|
|||
class ReportForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Report
|
||||
fields = ["user", "reporter", "statuses", "note"]
|
||||
fields = ["user", "reporter", "statuses", "links", "note"]
|
||||
|
||||
|
||||
class EmailBlocklistForm(CustomForm):
|
||||
|
|
|
@ -3,7 +3,7 @@ from django.core.management.base import BaseCommand
|
|||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from bookwyrm.models import Connector, FederatedServer, SiteSettings, User
|
||||
from bookwyrm import models
|
||||
|
||||
|
||||
def init_groups():
|
||||
|
@ -55,7 +55,7 @@ def init_permissions():
|
|||
},
|
||||
]
|
||||
|
||||
content_type = ContentType.objects.get_for_model(User)
|
||||
content_type = models.ContentType.objects.get_for_model(User)
|
||||
for permission in permissions:
|
||||
permission_obj = Permission.objects.create(
|
||||
codename=permission["codename"],
|
||||
|
@ -72,7 +72,7 @@ def init_permissions():
|
|||
|
||||
def init_connectors():
|
||||
"""access book data sources"""
|
||||
Connector.objects.create(
|
||||
models.Connector.objects.create(
|
||||
identifier="bookwyrm.social",
|
||||
name="BookWyrm dot Social",
|
||||
connector_file="bookwyrm_connector",
|
||||
|
@ -84,7 +84,7 @@ def init_connectors():
|
|||
priority=2,
|
||||
)
|
||||
|
||||
Connector.objects.create(
|
||||
models.Connector.objects.create(
|
||||
identifier="inventaire.io",
|
||||
name="Inventaire",
|
||||
connector_file="inventaire",
|
||||
|
@ -96,7 +96,7 @@ def init_connectors():
|
|||
priority=3,
|
||||
)
|
||||
|
||||
Connector.objects.create(
|
||||
models.Connector.objects.create(
|
||||
identifier="openlibrary.org",
|
||||
name="OpenLibrary",
|
||||
connector_file="openlibrary",
|
||||
|
@ -113,7 +113,7 @@ def init_federated_servers():
|
|||
"""big no to nazis"""
|
||||
built_in_blocks = ["gab.ai", "gab.com"]
|
||||
for server in built_in_blocks:
|
||||
FederatedServer.objects.create(
|
||||
models.FederatedServer.objects.create(
|
||||
server_name=server,
|
||||
status="blocked",
|
||||
)
|
||||
|
@ -121,18 +121,61 @@ def init_federated_servers():
|
|||
|
||||
def init_settings():
|
||||
"""info about the instance"""
|
||||
SiteSettings.objects.create(
|
||||
models.SiteSettings.objects.create(
|
||||
support_link="https://www.patreon.com/bookwyrm",
|
||||
support_title="Patreon",
|
||||
)
|
||||
|
||||
|
||||
def init_link_domains(*_):
|
||||
"""safe book links"""
|
||||
domains = [
|
||||
("standardebooks.org", "Standard EBooks"),
|
||||
("www.gutenberg.org", "Project Gutenberg"),
|
||||
("archive.org", "Internet Archive"),
|
||||
("openlibrary.org", "Open Library"),
|
||||
("theanarchistlibrary.org", "The Anarchist Library"),
|
||||
]
|
||||
for domain, name in domains:
|
||||
models.LinkDomain.objects.create(
|
||||
domain=domain,
|
||||
name=name,
|
||||
status="approved",
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Initializes the database with starter data"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--limit",
|
||||
default=None,
|
||||
help="Limit init to specific table",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
limit = options.get("limit")
|
||||
tables = [
|
||||
"group",
|
||||
"permission",
|
||||
"connector",
|
||||
"federatedserver",
|
||||
"settings",
|
||||
"linkdomain",
|
||||
]
|
||||
if limit not in tables:
|
||||
raise Exception("Invalid table limit:", limit)
|
||||
|
||||
if not limit or limit == "group":
|
||||
init_groups()
|
||||
if not limit or limit == "permission":
|
||||
init_permissions()
|
||||
if not limit or limit == "connector":
|
||||
init_connectors()
|
||||
if not limit or limit == "federatedserver":
|
||||
init_federated_servers()
|
||||
if not limit or limit == "settings":
|
||||
init_settings()
|
||||
if not limit or limit == "linkdomain":
|
||||
init_link_domains()
|
||||
|
|
|
@ -22,13 +22,6 @@ class Command(BaseCommand):
|
|||
|
||||
help = "Populate list streams for all users"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--stream",
|
||||
default=None,
|
||||
help="Specifies which time of stream to populate",
|
||||
)
|
||||
|
||||
# pylint: disable=no-self-use,unused-argument
|
||||
def handle(self, *args, **options):
|
||||
"""run feed builder"""
|
||||
|
|
144
bookwyrm/migrations/0126_filelink_link_linkdomain.py
Normal file
144
bookwyrm/migrations/0126_filelink_link_linkdomain.py
Normal file
|
@ -0,0 +1,144 @@
|
|||
# Generated by Django 3.2.10 on 2022-01-10 21:20
|
||||
|
||||
import bookwyrm.models.activitypub_mixin
|
||||
import bookwyrm.models.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0125_alter_user_preferred_language"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="LinkDomain",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_date", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"remote_id",
|
||||
bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
("domain", models.CharField(max_length=255, unique=True)),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("approved", "Approved"),
|
||||
("blocked", "Blocked"),
|
||||
("pending", "Pending"),
|
||||
],
|
||||
default="pending",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=100)),
|
||||
(
|
||||
"reported_by",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Link",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_date", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"remote_id",
|
||||
bookwyrm.models.fields.RemoteIdField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
||||
),
|
||||
),
|
||||
("url", bookwyrm.models.fields.URLField(max_length=255)),
|
||||
(
|
||||
"added_by",
|
||||
bookwyrm.models.fields.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"domain",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="links",
|
||||
to="bookwyrm.linkdomain",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
bases=(bookwyrm.models.activitypub_mixin.ActivitypubMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="FileLink",
|
||||
fields=[
|
||||
(
|
||||
"link_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="bookwyrm.link",
|
||||
),
|
||||
),
|
||||
("filetype", bookwyrm.models.fields.CharField(max_length=5)),
|
||||
(
|
||||
"book",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="file_links",
|
||||
to="bookwyrm.book",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
bases=("bookwyrm.link",),
|
||||
),
|
||||
]
|
22
bookwyrm/migrations/0127_auto_20220110_2211.py
Normal file
22
bookwyrm/migrations/0127_auto_20220110_2211.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 3.2.10 on 2022-01-10 22:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0126_filelink_link_linkdomain"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name="report",
|
||||
name="self_report",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="report",
|
||||
name="links",
|
||||
field=models.ManyToManyField(blank=True, to="bookwyrm.Link"),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,13 @@
|
|||
# Generated by Django 3.2.10 on 2022-01-13 01:14
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0126_auto_20220112_2315"),
|
||||
("bookwyrm", "0127_auto_20220110_2211"),
|
||||
]
|
||||
|
||||
operations = []
|
|
@ -4,6 +4,7 @@ import sys
|
|||
|
||||
from .book import Book, Work, Edition, BookDataModel
|
||||
from .author import Author
|
||||
from .link import Link, FileLink, LinkDomain
|
||||
from .connector import Connector
|
||||
|
||||
from .shelf import Shelf, ShelfBook
|
||||
|
|
|
@ -241,8 +241,11 @@ class Work(OrderedCollectionPageMixin, Book):
|
|||
)
|
||||
|
||||
activity_serializer = activitypub.Work
|
||||
serialize_reverse_fields = [("editions", "editions", "-edition_rank")]
|
||||
deserialize_reverse_fields = [("editions", "editions")]
|
||||
serialize_reverse_fields = [
|
||||
("editions", "editions", "-edition_rank"),
|
||||
("file_links", "fileLinks", "-created_date"),
|
||||
]
|
||||
deserialize_reverse_fields = [("editions", "editions"), ("file_links", "fileLinks")]
|
||||
|
||||
|
||||
# https://schema.org/BookFormatType
|
||||
|
@ -296,6 +299,8 @@ class Edition(Book):
|
|||
|
||||
activity_serializer = activitypub.Edition
|
||||
name_field = "title"
|
||||
serialize_reverse_fields = [("file_links", "fileLinks", "-created_date")]
|
||||
deserialize_reverse_fields = [("file_links", "fileLinks")]
|
||||
|
||||
def get_rank(self):
|
||||
"""calculate how complete the data is on this edition"""
|
||||
|
|
|
@ -517,6 +517,10 @@ class CharField(ActivitypubFieldMixin, models.CharField):
|
|||
"""activitypub-aware char field"""
|
||||
|
||||
|
||||
class URLField(ActivitypubFieldMixin, models.URLField):
|
||||
"""activitypub-aware url field"""
|
||||
|
||||
|
||||
class TextField(ActivitypubFieldMixin, models.TextField):
|
||||
"""activitypub-aware text field"""
|
||||
|
||||
|
|
85
bookwyrm/models/link.py
Normal file
85
bookwyrm/models/link.py
Normal file
|
@ -0,0 +1,85 @@
|
|||
""" outlink data """
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from .activitypub_mixin import ActivitypubMixin
|
||||
from .base_model import BookWyrmModel
|
||||
from . import fields
|
||||
|
||||
|
||||
class Link(ActivitypubMixin, BookWyrmModel):
|
||||
"""a link to a website"""
|
||||
|
||||
url = fields.URLField(max_length=255, activitypub_field="href")
|
||||
added_by = fields.ForeignKey(
|
||||
"User", on_delete=models.SET_NULL, null=True, activitypub_field="attributedTo"
|
||||
)
|
||||
domain = models.ForeignKey(
|
||||
"LinkDomain",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="links",
|
||||
)
|
||||
|
||||
activity_serializer = activitypub.Link
|
||||
reverse_unfurl = True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""link name via the assocaited domain"""
|
||||
return self.domain.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""create a link"""
|
||||
# get or create the associated domain
|
||||
if not self.domain:
|
||||
domain = urlparse(self.url).netloc
|
||||
self.domain, _ = LinkDomain.objects.get_or_create(domain=domain)
|
||||
|
||||
# this is never broadcast, the owning model broadcasts an update
|
||||
if "broadcast" in kwargs:
|
||||
del kwargs["broadcast"]
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
StatusChoices = [
|
||||
("approved", _("Approved")),
|
||||
("blocked", _("Blocked")),
|
||||
("pending", _("Pending")),
|
||||
]
|
||||
|
||||
|
||||
class LinkDomain(BookWyrmModel):
|
||||
"""List of domains used in links"""
|
||||
|
||||
domain = models.CharField(max_length=255, unique=True)
|
||||
status = models.CharField(max_length=50, choices=StatusChoices, default="pending")
|
||||
name = models.CharField(max_length=100)
|
||||
reported_by = models.ForeignKey(
|
||||
"User", blank=True, null=True, on_delete=models.SET_NULL
|
||||
)
|
||||
|
||||
def raise_not_editable(self, viewer):
|
||||
if viewer.has_perm("moderate_post"):
|
||||
return
|
||||
raise PermissionDenied()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""set a default name"""
|
||||
if not self.name:
|
||||
self.name = self.domain
|
||||
super().save(*args, **kwargs)
|
|
@ -1,6 +1,5 @@
|
|||
""" flagged for moderation """
|
||||
from django.db import models
|
||||
from django.db.models import F, Q
|
||||
from .base_model import BookWyrmModel
|
||||
|
||||
|
||||
|
@ -13,14 +12,12 @@ class Report(BookWyrmModel):
|
|||
note = models.TextField(null=True, blank=True)
|
||||
user = models.ForeignKey("User", on_delete=models.PROTECT)
|
||||
statuses = models.ManyToManyField("Status", blank=True)
|
||||
links = models.ManyToManyField("Link", blank=True)
|
||||
resolved = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
"""don't let users report themselves"""
|
||||
"""set order by default"""
|
||||
|
||||
constraints = [
|
||||
models.CheckConstraint(check=~Q(reporter=F("user")), name="self_report")
|
||||
]
|
||||
ordering = ("-created_date",)
|
||||
|
||||
|
||||
|
|
|
@ -346,6 +346,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""deactivate rather than delete a user"""
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.is_active = False
|
||||
# skip the logic in this class's save()
|
||||
super().save(*args, **kwargs)
|
||||
|
@ -406,14 +407,6 @@ class KeyPair(ActivitypubMixin, BookWyrmModel):
|
|||
self.private_key, self.public_key = create_key_pair()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def to_activity(self, **kwargs):
|
||||
"""override default AP serializer to add context object
|
||||
idk if this is the best way to go about this"""
|
||||
activity_object = super().to_activity(**kwargs)
|
||||
del activity_object["@context"]
|
||||
del activity_object["type"]
|
||||
return activity_object
|
||||
|
||||
|
||||
def get_current_year():
|
||||
"""sets default year for annual goal to this year"""
|
||||
|
|
|
@ -9,12 +9,12 @@ from django.utils.translation import gettext_lazy as _
|
|||
env = Env()
|
||||
env.read_env()
|
||||
DOMAIN = env("DOMAIN")
|
||||
VERSION = "0.1.1"
|
||||
VERSION = "0.2.0"
|
||||
|
||||
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
||||
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
||||
|
||||
JS_CACHE = "9b4cc1f7"
|
||||
JS_CACHE = "a47cc2ca"
|
||||
|
||||
# email
|
||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||
|
|
|
@ -720,6 +720,10 @@ ol.ordered-list li::before {
|
|||
}
|
||||
}
|
||||
|
||||
.overflow-wrap-anywhere {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* Threads
|
||||
******************************************************************************/
|
||||
|
||||
|
|
165
bookwyrm/static/js/autocomplete.js
Normal file
165
bookwyrm/static/js/autocomplete.js
Normal file
|
@ -0,0 +1,165 @@
|
|||
(function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Suggest a completion as a user types
|
||||
*
|
||||
* Use `data-autocomplete="<completions set identifier>"`on the input field.
|
||||
* specifying the trie to be used for autocomplete
|
||||
*
|
||||
* @example
|
||||
* <input
|
||||
* type="input"
|
||||
* data-autocomplete="mimetype"
|
||||
* >
|
||||
* @param {Event} event
|
||||
* @return {undefined}
|
||||
*/
|
||||
function autocomplete(event) {
|
||||
const input = event.target;
|
||||
|
||||
// Get suggestions
|
||||
let trie = tries[input.getAttribute("data-autocomplete")];
|
||||
|
||||
let suggestions = getSuggestions(input.value, trie);
|
||||
|
||||
const boxId = input.getAttribute("list");
|
||||
|
||||
// Create suggestion box, if needed
|
||||
let suggestionsBox = document.getElementById(boxId);
|
||||
|
||||
// Clear existing suggestions
|
||||
suggestionsBox.innerHTML = "";
|
||||
|
||||
// Populate suggestions box
|
||||
suggestions.forEach((suggestion) => {
|
||||
const suggestionItem = document.createElement("option");
|
||||
|
||||
suggestionItem.textContent = suggestion;
|
||||
suggestionsBox.appendChild(suggestionItem);
|
||||
});
|
||||
}
|
||||
|
||||
function getSuggestions(input, trie) {
|
||||
// Follow the trie through the provided input
|
||||
input.split("").forEach((letter) => {
|
||||
if (!trie) {
|
||||
return;
|
||||
}
|
||||
|
||||
trie = trie[letter];
|
||||
});
|
||||
|
||||
if (!trie) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return searchTrie(trie);
|
||||
}
|
||||
|
||||
function searchTrie(trie) {
|
||||
const options = Object.values(trie);
|
||||
|
||||
if (typeof trie == "string") {
|
||||
return [trie];
|
||||
}
|
||||
|
||||
return options
|
||||
.map((option) => {
|
||||
const newTrie = option;
|
||||
|
||||
if (typeof newTrie == "string") {
|
||||
return [newTrie];
|
||||
}
|
||||
|
||||
return searchTrie(newTrie);
|
||||
})
|
||||
.reduce((prev, next) => prev.concat(next));
|
||||
}
|
||||
|
||||
document.querySelectorAll("[data-autocomplete]").forEach((input) => {
|
||||
input.addEventListener("input", autocomplete);
|
||||
});
|
||||
})();
|
||||
|
||||
const tries = {
|
||||
mimetype: {
|
||||
a: {
|
||||
a: {
|
||||
c: "AAC",
|
||||
},
|
||||
z: {
|
||||
w: "AZW",
|
||||
},
|
||||
},
|
||||
d: {
|
||||
a: {
|
||||
i: {
|
||||
s: {
|
||||
y: "Daisy",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
e: {
|
||||
p: {
|
||||
u: {
|
||||
b: "ePub",
|
||||
},
|
||||
},
|
||||
},
|
||||
f: {
|
||||
l: {
|
||||
a: {
|
||||
c: "FLAC",
|
||||
},
|
||||
},
|
||||
},
|
||||
h: {
|
||||
t: {
|
||||
m: {
|
||||
l: "HTML",
|
||||
},
|
||||
},
|
||||
},
|
||||
m: {
|
||||
4: {
|
||||
a: "M4A",
|
||||
b: "M4B",
|
||||
},
|
||||
o: {
|
||||
b: {
|
||||
i: "MOBI",
|
||||
},
|
||||
},
|
||||
p: {
|
||||
3: "MP3",
|
||||
},
|
||||
},
|
||||
o: {
|
||||
g: {
|
||||
g: "OGG",
|
||||
},
|
||||
},
|
||||
p: {
|
||||
d: {
|
||||
f: "PDF",
|
||||
},
|
||||
l: {
|
||||
a: {
|
||||
i: {
|
||||
n: {
|
||||
t: {
|
||||
e: {
|
||||
x: {
|
||||
t: "Plaintext",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -35,7 +35,7 @@ let BookWyrm = new (class {
|
|||
.forEach((node) => node.addEventListener("change", this.disableIfTooLarge.bind(this)));
|
||||
|
||||
document
|
||||
.querySelectorAll("button[data-modal-open]")
|
||||
.querySelectorAll("[data-modal-open]")
|
||||
.forEach((node) => node.addEventListener("click", this.handleModalButton.bind(this)));
|
||||
|
||||
document
|
||||
|
|
|
@ -183,7 +183,7 @@
|
|||
{% include 'snippets/toggle/open_button.html' with text=button_text controls_text="add_description" controls_uid=book.id focus="id_description" hide_active=True id="hide_description" %}
|
||||
|
||||
<div class="box is-hidden" id="add_description_{{ book.id }}">
|
||||
<form name="add-description" method="POST" action="/add-description/{{ book.id }}">
|
||||
<form name="add-description" method="POST" action="{% url "add-description" book.id %}">
|
||||
{% csrf_token %}
|
||||
<p class="fields is-grouped">
|
||||
<label class="label" for="id_description_{{ book.id }}">{% trans "Description:" %}</label>
|
||||
|
@ -319,7 +319,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-one-fifth">
|
||||
<div class="column is-one-fifth is-clipped">
|
||||
{% if book.subjects %}
|
||||
<section class="content block">
|
||||
<h2 class="title is-5">{% trans "Subjects" %}</h2>
|
||||
|
@ -344,7 +344,7 @@
|
|||
{% endif %}
|
||||
|
||||
{% if lists.exists or request.user.list_set.exists %}
|
||||
<section class="content block">
|
||||
<section class="content block is-clipped">
|
||||
<h2 class="title is-5">{% trans "Lists" %}</h2>
|
||||
<ul>
|
||||
{% for list in lists %}
|
||||
|
@ -358,7 +358,7 @@
|
|||
<input type="hidden" name="book" value="{{ book.id }}">
|
||||
<label class="label" for="id_list">{% trans "Add to list" %}</label>
|
||||
<div class="field has-addons">
|
||||
<div class="select control">
|
||||
<div class="select control is-clipped">
|
||||
<select name="list" id="id_list">
|
||||
{% for list in user.list_set.all %}
|
||||
<option value="{{ list.id }}">{{ list.name }}</option>
|
||||
|
@ -373,6 +373,10 @@
|
|||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="content block">
|
||||
{% include "book/file_links/links.html" %}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -382,4 +386,5 @@
|
|||
|
||||
{% block scripts %}
|
||||
<script src="{% static "js/vendor/tabs.js" %}?v={{ js_cache }}"></script>
|
||||
<script src="{% static "js/autocomplete.js" %}?v={{ js_cache }}"></script>
|
||||
{% endblock %}
|
||||
|
|
56
bookwyrm/templates/book/file_links/add_link_modal.html
Normal file
56
bookwyrm/templates/book/file_links/add_link_modal.html
Normal file
|
@ -0,0 +1,56 @@
|
|||
{% extends 'components/modal.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block modal-title %}
|
||||
{% trans "Add file link" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-open %}
|
||||
<form name="add-link" method="POST" action="{% url 'file-link-add' book.id %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="book" value="{{ book.id }}">
|
||||
<input type="hidden" name="added_by" value="{{ request.user.id }}">
|
||||
|
||||
<p class="notification">
|
||||
{% trans "Links from unknown domains will need to be approved by a moderator before they are added." %}
|
||||
</p>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-four-fifths">
|
||||
<label class="label" for="id_url">{% trans "URL:" %}</label>
|
||||
<input type="url" name="url" maxlength="255" class="input" required="" id="id_url" value="{% firstof file_link_form.url.value "" %}" placeholder="https://..." aria-describedby="desc_name">
|
||||
{% include 'snippets/form_errors.html' with errors_list=file_link_form.url.errors id="desc_url" %}
|
||||
</div>
|
||||
<div class="column is-one-fifth">
|
||||
<label class="label" for="id_filetype">{% trans "File type:" %}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="filetype"
|
||||
maxlength="5"
|
||||
class="input"
|
||||
required=""
|
||||
id="id_filetype"
|
||||
value="{% firstof file_link_form.filetype.value '' %}"
|
||||
placeholder="ePub"
|
||||
list="mimetypes-list"
|
||||
data-autocomplete="mimetype"
|
||||
>
|
||||
<datalist id="mimetypes-list"></datalist>
|
||||
{% include 'snippets/form_errors.html' with errors_list=file_link_form.filetype.errors id="desc_filetype" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||
{% if not static %}
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
{% block modal-form-close %}</form>{% endblock %}
|
82
bookwyrm/templates/book/file_links/edit_links.html
Normal file
82
bookwyrm/templates/book/file_links/edit_links.html
Normal file
|
@ -0,0 +1,82 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block title %}{% trans "Edit links" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<header class="block content">
|
||||
<h1 class="title">
|
||||
{% blocktrans with title=book|book_title %}
|
||||
Links for "<em>{{ title }}</em>"
|
||||
{% endblocktrans %}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="{% url 'book' book.id %}">{{ book|book_title }}</a></li>
|
||||
<li class="is-active">
|
||||
<a href="#" aria-current="page">
|
||||
{% trans "Edit links" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<section class="block content">
|
||||
<div class="table-container">
|
||||
<table class="is-striped is-fullwidth">
|
||||
<tr>
|
||||
<th>{% trans "URL" %}</th>
|
||||
<th>{% trans "Added by" %}</th>
|
||||
<th>{% trans "Filetype" %}</th>
|
||||
<th>{% trans "Domain" %}</th>
|
||||
<th>{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
{% for link in book.file_links.all %}
|
||||
<tr>
|
||||
<td class="overflow-wrap-anywhere">
|
||||
<a href="{{ link.url }}" target="_blank" rel="noopener">{{ link.url }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'user-feed' link.added_by.id %}">{{ link.added_by.display_name }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ link.filelink.filetype }}
|
||||
</td>
|
||||
<td>
|
||||
{{ link.domain.name }} ({{ link.domain.get_status_display }})
|
||||
<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 %}">
|
||||
{% csrf_token %}
|
||||
<button class="button is-danger is-light" type="submit">Delete link</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not book.file_links.exists %}
|
||||
<tr>
|
||||
<td colspan="5"><em>{% trans "No links available for this book." %}</em></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% url 'file-link-add' book.id as fallback_url %}
|
||||
<form name="add-link" method="get" action="{{ fallback_url }}">
|
||||
<button class="button" type="submit" data-modal-open="add-links">
|
||||
<span class="icon icon-plus m-0-mobile" aria-hidden="true"></span>
|
||||
<span class="is-sr-only-mobile">
|
||||
{% trans "Add link to file" %}
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
15
bookwyrm/templates/book/file_links/file_link_page.html
Normal file
15
bookwyrm/templates/book/file_links/file_link_page.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}
|
||||
{% trans "File Links" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "book/file_links/add_link_modal.html" with book=book active=True static=True id="file-link" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{% static "js/autocomplete.js" %}?v={{ js_cache }}"></script>
|
||||
{% endblock %}
|
52
bookwyrm/templates/book/file_links/links.html
Normal file
52
bookwyrm/templates/book/file_links/links.html
Normal file
|
@ -0,0 +1,52 @@
|
|||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load utilities %}
|
||||
|
||||
{% get_book_file_links book as links %}
|
||||
{% if links.exists or request.user.is_authenticated %}
|
||||
<header class="columns is-mobile mb-0">
|
||||
<div class="column">
|
||||
<h2 class="title is-5">{% trans "Get a copy" %}</h2>
|
||||
</div>
|
||||
{% if can_edit_book %}
|
||||
<div class="column is-narrow">
|
||||
{% url 'file-link-add' book.id as fallback_url %}
|
||||
<form name="add-link" method="get" action="{{ fallback_url }}">
|
||||
<button class="button is-small" type="submit" data-modal-open="add-links">
|
||||
<span class="icon icon-plus">
|
||||
<span class="is-sr-only">
|
||||
{% trans "Add link to file" %}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</header>
|
||||
{% if links %}
|
||||
<ul class="mt-0">
|
||||
{% for link in links.all %}
|
||||
{% join "verify" link.id as verify_modal %}
|
||||
<li>
|
||||
<a href="{{ link.url }}" rel="noopener" target="_blank" title="{{ link.url }}" data-modal-open="{{ verify_modal }}">{{ link.name }}</a>
|
||||
({{ link.filetype }})
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% for link in links.all %}
|
||||
{% join "verify" link.id as verify_modal %}
|
||||
{% include "book/file_links/verification_modal.html" with id=verify_modal %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<em>{% trans "No links available" %}</em>
|
||||
{% endif %}
|
||||
|
||||
{% if can_edit_book and links.exists %}
|
||||
<a href="{% url 'file-link' book.id %}" class="is-pulled-right">
|
||||
<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 %}
|
||||
|
||||
{% endif %}
|
29
bookwyrm/templates/book/file_links/verification_modal.html
Normal file
29
bookwyrm/templates/book/file_links/verification_modal.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
{% extends 'components/modal.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal-title %}
|
||||
{% trans "Leaving BookWyrm" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block modal-body %}
|
||||
|
||||
{% blocktrans trimmed with link_url=link.url %}
|
||||
This link is taking you to: <code>{{ link_url }}</code>.<br>
|
||||
Is that where you'd like to go?
|
||||
{% endblocktrans %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block modal-footer %}
|
||||
<a href="{{ link.url }}" target="_blank" rel="noopener" class="button is-primary">{% trans "Continue" %}</a>
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
<div class="has-text-right is-flex-grow-1">
|
||||
<a href="{% url 'report-link' link.added_by.id link.id %}">{% trans "Report spam" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
10
bookwyrm/templates/report.html
Normal file
10
bookwyrm/templates/report.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
{% extends "layout.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}
|
||||
{% trans "Report" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "snippets/report_modal.html" with user=user active=True static=True %}
|
||||
{% endblock %}
|
|
@ -48,6 +48,17 @@
|
|||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if pending_domains %}
|
||||
<div class="column">
|
||||
<a href="{% url 'settings-link-domain' %}" class="notification is-primary is-block">
|
||||
{% blocktrans trimmed count counter=pending_domains with display_count=pending_domains|intcomma %}
|
||||
{{ display_count }} domain needs review
|
||||
{% plural %}
|
||||
{{ display_count }} domains need review
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not site.allow_registration and site.allow_invite_requests and invite_requests %}
|
||||
<div class="column">
|
||||
<a href="{% url 'settings-invite-requests' %}" class="notification is-block is-success is-light">
|
||||
|
|
|
@ -62,6 +62,10 @@
|
|||
{% url 'settings-ip-blocks' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "IP Address Blocklist" %}</a>
|
||||
</li>
|
||||
<li>
|
||||
{% url 'settings-link-domain' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Link Domains" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if perms.bookwyrm.edit_instance_settings %}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
{% extends 'components/modal.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal-title %}
|
||||
{% blocktrans with url=domain.domain %}Set display name for {{ url }}{% endblocktrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-open %}
|
||||
<form name="edit-domain-{{ domain.id }}" method="post" action="{% url 'settings-link-domain' status=status domain_id=domain.id %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
{% csrf_token %}
|
||||
<label class="label" for="id_name">{% trans "Name:" %}</label>
|
||||
<div class="control">
|
||||
<input type="text" id="id_name" class="input" name="name" value="{{ domain.name }}" required>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<button type="submit" class="button is-primary">{% trans "Set" %}</button>
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-close %}</form>{% endblock %}
|
106
bookwyrm/templates/settings/link_domains/link_domains.html
Normal file
106
bookwyrm/templates/settings/link_domains/link_domains.html
Normal file
|
@ -0,0 +1,106 @@
|
|||
{% extends 'settings/layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block title %}{% trans "Link Domains" %}{% endblock %}
|
||||
|
||||
{% block header %}{% trans "Link Domains" %}{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
<p class="notification block">
|
||||
{% trans "Link domains must be approved before they are shown on book pages. Please make sure that the domains are not hosting spam, malicious code, or deceptive links before approving." %}
|
||||
</p>
|
||||
|
||||
<div class="block">
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
{% url 'settings-link-domain' status='pending' as url %}
|
||||
<li {% if request.path in url %}class="is-active" aria-current="page"{% endif %}>
|
||||
<a href="{{ url }}">{% trans "Pending" %} ({{ counts.pending }})</a>
|
||||
</li>
|
||||
{% url 'settings-link-domain' status='approved' as url %}
|
||||
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
|
||||
<a href="{{ url }}">{% trans "Approved" %} ({{ counts.approved }})</a>
|
||||
</li>
|
||||
{% url 'settings-link-domain' status='blocked' as url %}
|
||||
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
|
||||
<a href="{{ url }}">{% trans "Blocked" %} ({{ counts.blocked }})</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% for domain in domains %}
|
||||
{% join "domain" domain.id as domain_modal %}
|
||||
<div class="box content" id="{{ domain.id }}">
|
||||
<div class="columns is-mobile">
|
||||
<header class="column">
|
||||
<h2 class="title is-5">
|
||||
{{ domain.name }}
|
||||
(<a href="http://{{ domain.domain }}" target="_blank" rel="noopener">{{ domain.domain }}</a>)
|
||||
</h2>
|
||||
</header>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
<details class="details-panel">
|
||||
<summary>
|
||||
<span role="heading" aria-level="3" class="title is-6 mb-0">
|
||||
{% trans "View links" %}
|
||||
({{ domain.links.count }})
|
||||
</span>
|
||||
<span class="details-close icon icon-x" aria-hidden></span>
|
||||
</summary>
|
||||
|
||||
<div class="table-container mt-4">
|
||||
{% include "settings/link_domains/link_table.html" with links=domain.links.all|slice:10 %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
{% include "settings/link_domains/edit_domain_modal.html" with domain=domain id=domain_modal %}
|
||||
|
||||
<div class="field has-addons">
|
||||
{% if status != "approved" %}
|
||||
<form
|
||||
name="domain-{{ domains.id }}-approve"
|
||||
class="control"
|
||||
method="post"
|
||||
action="{% url 'settings-link-domain-status' domain.id 'approved' %}"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="button is-success is-light">{% trans "Approve" %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if status != "blocked" %}
|
||||
<form
|
||||
name="domain-{{ domains.id }}-block"
|
||||
class="control"
|
||||
method="post"
|
||||
action="{% url 'settings-link-domain-status' domain.id 'blocked' %}"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="button is-danger is-light">{% trans "Block" %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if not domains.exists %}
|
||||
{% if status == "approved" %}
|
||||
<em>{% trans "No domains currently approved" %}</em>
|
||||
{% elif status == "pending" %}
|
||||
<em>{% trans "No domains currently pending" %}</em>
|
||||
{% else %}
|
||||
<em>{% trans "No domains currently blocked" %}</em>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
42
bookwyrm/templates/settings/link_domains/link_table.html
Normal file
42
bookwyrm/templates/settings/link_domains/link_table.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
<table class="is-striped is-fullwidth">
|
||||
<tr>
|
||||
<th>{% trans "URL" %}</th>
|
||||
<th>{% trans "Added by" %}</th>
|
||||
<th>{% trans "Filetype" %}</th>
|
||||
<th>{% trans "Book" %}</th>
|
||||
{% block additional_headers %}{% endblock %}
|
||||
</tr>
|
||||
{% for link in links %}
|
||||
<tr>
|
||||
<td class="overflow-wrap-anywhere">
|
||||
<a href="{{ link.url }}" target="_blank" rel="noopener">{{ link.url }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'settings-user' link.added_by.id %}">@{{ link.added_by|username }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if link.filelink.filetype %}
|
||||
{{ link.filelink.filetype }}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{% if link.filelink.book %}
|
||||
{% with book=link.filelink.book %}
|
||||
{% include "snippets/book_titleby.html" with book=book %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{% block additional_data %}{% endblock %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not links %}
|
||||
<tr>
|
||||
<td colspan="7"><em>{% trans "No links available for this domain." %}</em></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
|
@ -2,10 +2,12 @@
|
|||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block title %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %}
|
||||
{% block title %}
|
||||
{% include "settings/reports/report_header.html" with report=report %}
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}
|
||||
{% include "settings/reports/report_header.html" with report=report %}
|
||||
<a href="{% url 'settings-reports' %}" class="has-text-weight-normal help">{% trans "Back to reports" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -15,6 +17,36 @@
|
|||
{% include 'settings/reports/report_preview.html' with report=report %}
|
||||
</div>
|
||||
|
||||
{% if report.statuses.exists %}
|
||||
<div class="block">
|
||||
<h3 class="title is-4">{% trans "Reported statuses" %}</h3>
|
||||
<ul>
|
||||
{% for status in report.statuses.select_subclasses.all %}
|
||||
<li>
|
||||
{% if status.deleted %}
|
||||
<em>{% trans "Status has been deleted" %}</em>
|
||||
{% else %}
|
||||
{% include 'snippets/status/status.html' with status=status moderation_mode=True %}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if report.links.exists %}
|
||||
<div class="block">
|
||||
<h3 class="title is-4">{% trans "Reported links" %}</h3>
|
||||
<div class="card block">
|
||||
<div class="card-content content">
|
||||
<div class="table-container">
|
||||
{% include "settings/reports/report_links_table.html" with links=report.links.all %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% include 'settings/users/user_info.html' with user=report.user %}
|
||||
|
||||
{% include 'settings/users/user_moderation_actions.html' with user=report.user %}
|
||||
|
@ -41,23 +73,4 @@
|
|||
<button class="button">{% trans "Comment" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<h3 class="title is-4">{% trans "Reported statuses" %}</h3>
|
||||
{% if not report.statuses.exists %}
|
||||
<em>{% trans "No statuses reported" %}</em>
|
||||
{% else %}
|
||||
<ul>
|
||||
{% for status in report.statuses.select_subclasses.all %}
|
||||
<li>
|
||||
{% if status.deleted %}
|
||||
<em>{% trans "Status has been deleted" %}</em>
|
||||
{% else %}
|
||||
{% include 'snippets/status/status.html' with status=status moderation_mode=True %}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
22
bookwyrm/templates/settings/reports/report_header.html
Normal file
22
bookwyrm/templates/settings/reports/report_header.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% if report.statuses.exists %}
|
||||
|
||||
{% blocktrans trimmed with report_id=report.id username=report.user|username %}
|
||||
Report #{{ report_id }}: Status posted by @{{ username }}
|
||||
{% endblocktrans %}
|
||||
|
||||
{% elif report.links.exists %}
|
||||
|
||||
{% blocktrans trimmed with report_id=report.id username=report.user|username %}
|
||||
Report #{{ report_id }}: Link added by @{{ username }}
|
||||
{% endblocktrans %}
|
||||
|
||||
{% else %}
|
||||
|
||||
{% blocktrans trimmed with report_id=report.id username=report.user|username %}
|
||||
Report #{{ report_id }}: User @{{ username }}
|
||||
{% endblocktrans %}
|
||||
|
||||
{% endif %}
|
20
bookwyrm/templates/settings/reports/report_links_table.html
Normal file
20
bookwyrm/templates/settings/reports/report_links_table.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% extends "settings/link_domains/link_table.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block additional_headers %}
|
||||
<th>{% trans "Domain" %}</th>
|
||||
<th>{% trans "Actions" %}</th>
|
||||
{% endblock %}
|
||||
|
||||
{% block additional_data %}
|
||||
<td>
|
||||
<a href="{% url 'settings-link-domain' 'pending' %}#{{ link.domain.id }}">
|
||||
{{ link.domain.domain }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<form>
|
||||
<button type="submit" class="button is-danger is-light">{% trans "Block domain" %}</button>
|
||||
</form>
|
||||
</td>
|
||||
{% endblock %}
|
|
@ -1,9 +1,13 @@
|
|||
{% extends 'components/card.html' %}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block card-header %}
|
||||
<h2 class="card-header-title has-background-white-ter is-block">
|
||||
<a href="{% url 'settings-report' report.id %}">{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}</a>
|
||||
<a href="{% url 'settings-report' report.id %}">
|
||||
{% include "settings/reports/report_header.html" with report=report %}
|
||||
</a>
|
||||
</h2>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -17,7 +21,7 @@
|
|||
|
||||
{% block card-footer %}
|
||||
<div class="card-footer-item">
|
||||
<p>{% blocktrans with username=report.reporter.display_name path=report.reporter.local_path %}Reported by <a href="{{ path }}">{{ username }}</a>{% endblocktrans %}</p>
|
||||
<p>{% blocktrans with username=report.reporter|username path=report.reporter.local_path %}Reported by <a href="{{ path }}">@{{ username }}</a>{% endblocktrans %}</p>
|
||||
</div>
|
||||
<div class="card-footer-item">
|
||||
{{ report.created_date | naturaltime }}
|
||||
|
|
|
@ -5,8 +5,9 @@
|
|||
{% trans "Permanently deleted" %}
|
||||
</div>
|
||||
{% else %}
|
||||
<h3>{% trans "Actions" %}</h3>
|
||||
<h3>{% trans "User Actions" %}</h3>
|
||||
|
||||
<div class="box">
|
||||
<div class="is-flex">
|
||||
{% if user.is_active %}
|
||||
<p class="mr-1">
|
||||
|
@ -70,6 +71,7 @@
|
|||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -12,6 +12,6 @@
|
|||
>
|
||||
{% trans "Report" %}
|
||||
</button>
|
||||
{% include 'snippets/report_modal.html' with user=user reporter=request.user id=modal_id %}
|
||||
{% include 'snippets/report_modal.html' with user=user id=modal_id status=status.id %}
|
||||
|
||||
{% endwith %}
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
{% extends 'components/modal.html' %}
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block modal-title %}
|
||||
{% blocktrans with username=user.username %}Report @{{ username }}{% endblocktrans %}
|
||||
{% if status %}
|
||||
{% blocktrans with username=user|username %}Report @{{ username }}'s status{% endblocktrans %}
|
||||
{% elif link %}
|
||||
{% blocktrans with domain=link.domain.domain %}Report {{ domain }} link{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans with username=user|username %}Report @{{ username }}{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-open %}
|
||||
|
@ -13,14 +20,22 @@
|
|||
{% block modal-body %}
|
||||
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="reporter" value="{{ reporter.id }}">
|
||||
<input type="hidden" name="reporter" value="{{ request.user.id }}">
|
||||
<input type="hidden" name="user" value="{{ user.id }}">
|
||||
{% if status %}
|
||||
<input type="hidden" name="statuses" value="{{ status.id }}">
|
||||
<input type="hidden" name="statuses" value="{{ status_id }}">
|
||||
{% endif %}
|
||||
{% if link %}
|
||||
<input type="hidden" name="links" value="{{ link.id }}">
|
||||
{% endif %}
|
||||
|
||||
<section class="content">
|
||||
<p>{% blocktrans with site_name=site.name %}This report will be sent to {{ site_name }}'s moderators for review.{% endblocktrans %}</p>
|
||||
<p class="notification">
|
||||
{% blocktrans with site_name=site.name %}This report will be sent to {{ site_name }}'s moderators for review.{% endblocktrans %}
|
||||
{% if link %}
|
||||
{% trans "Links from this domain will be removed until your report has been reviewed." %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<div class="control">
|
||||
<label class="label" for="id_{{ controls_uid }}_report_note">
|
||||
{% trans "More info about this report:" %}
|
||||
|
@ -35,7 +50,9 @@
|
|||
{% block modal-footer %}
|
||||
|
||||
<button class="button is-success" type="submit">{% trans "Submit" %}</button>
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
{% if not static %}
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -198,3 +198,9 @@ def suggested_books(context):
|
|||
# this happens here instead of in the view so that the template snippet can
|
||||
# be cached in the template
|
||||
return get_suggested_books(context["request"].user)
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=False)
|
||||
def get_book_file_links(book):
|
||||
"""links for a book"""
|
||||
return book.file_links.filter(domain__status="approved")
|
||||
|
|
41
bookwyrm/tests/models/test_link.py
Normal file
41
bookwyrm/tests/models/test_link.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
""" testing models """
|
||||
from unittest.mock import patch
|
||||
from django.test import TestCase
|
||||
|
||||
from bookwyrm import models
|
||||
|
||||
|
||||
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
|
||||
class Link(TestCase):
|
||||
"""some activitypub oddness ahead"""
|
||||
|
||||
def setUp(self):
|
||||
"""look, a list"""
|
||||
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
|
||||
self.local_user = models.User.objects.create_user(
|
||||
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
|
||||
)
|
||||
work = models.Work.objects.create(title="hello")
|
||||
self.book = models.Edition.objects.create(title="hi", parent_work=work)
|
||||
|
||||
def test_create_domain(self, _):
|
||||
"""generated default name"""
|
||||
domain = models.LinkDomain.objects.create(domain="beep.com")
|
||||
self.assertEqual(domain.name, "beep.com")
|
||||
self.assertEqual(domain.status, "pending")
|
||||
|
||||
def test_create_link_new_domain(self, _):
|
||||
"""generates link and sets domain"""
|
||||
link = models.Link.objects.create(url="https://www.hello.com/hi-there")
|
||||
self.assertEqual(link.domain.domain, "www.hello.com")
|
||||
self.assertEqual(link.name, "www.hello.com")
|
||||
|
||||
def test_create_link_existing_domain(self, _):
|
||||
"""generate link with a known domain"""
|
||||
domain = models.LinkDomain.objects.create(domain="www.hello.com", name="Hi")
|
||||
|
||||
link = models.Link.objects.create(url="https://www.hello.com/hi-there")
|
||||
self.assertEqual(link.domain, domain)
|
||||
self.assertEqual(link.name, "Hi")
|
80
bookwyrm/tests/views/admin/test_link_domains.py
Normal file
80
bookwyrm/tests/views/admin/test_link_domains.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
class LinkDomainViews(TestCase):
|
||||
"""every response to a get request, html or json"""
|
||||
|
||||
def setUp(self):
|
||||
"""we need basic test data and mocks"""
|
||||
self.factory = RequestFactory()
|
||||
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
|
||||
self.local_user = models.User.objects.create_user(
|
||||
"mouse@local.com",
|
||||
"mouse@mouse.mouse",
|
||||
"password",
|
||||
local=True,
|
||||
localname="mouse",
|
||||
)
|
||||
self.book = models.Edition.objects.create(title="hello")
|
||||
models.FileLink.objects.create(
|
||||
book=self.book,
|
||||
url="https://beep.com/book/1",
|
||||
added_by=self.local_user,
|
||||
)
|
||||
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
def test_domain_page_get(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.LinkDomain.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
result = view(request, "pending")
|
||||
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_domain_page_post(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
domain = models.LinkDomain.objects.get(domain="beep.com")
|
||||
self.assertEqual(domain.name, "beep.com")
|
||||
|
||||
view = views.LinkDomain.as_view()
|
||||
request = self.factory.post("", {"name": "ugh"})
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
result = view(request, "pending", domain.id)
|
||||
self.assertEqual(result.status_code, 302)
|
||||
|
||||
domain.refresh_from_db()
|
||||
self.assertEqual(domain.name, "ugh")
|
||||
|
||||
def test_domain_page_set_status(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
domain = models.LinkDomain.objects.get(domain="beep.com")
|
||||
self.assertEqual(domain.status, "pending")
|
||||
|
||||
view = views.update_domain_status
|
||||
request = self.factory.post("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
||||
result = view(request, domain.id, "approved")
|
||||
self.assertEqual(result.status_code, 302)
|
||||
|
||||
domain.refresh_from_db()
|
||||
self.assertEqual(domain.status, "approved")
|
|
@ -37,7 +37,7 @@ class ReportViews(TestCase):
|
|||
|
||||
def test_reports_page(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Reports.as_view()
|
||||
view = views.ReportsAdmin.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
@ -49,7 +49,7 @@ class ReportViews(TestCase):
|
|||
|
||||
def test_reports_page_with_data(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Reports.as_view()
|
||||
view = views.ReportsAdmin.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
@ -62,7 +62,7 @@ class ReportViews(TestCase):
|
|||
|
||||
def test_report_page(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Report.as_view()
|
||||
view = views.ReportAdmin.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
@ -76,7 +76,7 @@ class ReportViews(TestCase):
|
|||
|
||||
def test_report_comment(self):
|
||||
"""comment on a report"""
|
||||
view = views.Report.as_view()
|
||||
view = views.ReportAdmin.as_view()
|
||||
request = self.factory.post("", {"note": "hi"})
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
@ -97,7 +97,7 @@ class ReportViews(TestCase):
|
|||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
|
||||
views.make_report(request)
|
||||
views.Report.as_view()(request)
|
||||
|
||||
report = models.Report.objects.get()
|
||||
self.assertEqual(report.reporter, self.local_user)
|
||||
|
|
89
bookwyrm/tests/views/books/test_links.py
Normal file
89
bookwyrm/tests/views/books/test_links.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
""" test for app action functionality """
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import forms, models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
class LinkViews(TestCase):
|
||||
"""books books books"""
|
||||
|
||||
def setUp(self):
|
||||
"""we need basic test data and mocks"""
|
||||
self.factory = RequestFactory()
|
||||
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||
):
|
||||
self.local_user = models.User.objects.create_user(
|
||||
"mouse@local.com",
|
||||
"mouse@mouse.com",
|
||||
"mouseword",
|
||||
local=True,
|
||||
localname="mouse",
|
||||
remote_id="https://example.com/users/mouse",
|
||||
)
|
||||
group = Group.objects.create(name="editor")
|
||||
group.permissions.add(
|
||||
Permission.objects.create(
|
||||
name="edit_book",
|
||||
codename="edit_book",
|
||||
content_type=ContentType.objects.get_for_model(models.User),
|
||||
).id
|
||||
)
|
||||
self.local_user.groups.add(group)
|
||||
|
||||
self.work = models.Work.objects.create(title="Test Work")
|
||||
self.book = models.Edition.objects.create(
|
||||
title="Example Edition",
|
||||
remote_id="https://example.com/book/1",
|
||||
parent_work=self.work,
|
||||
)
|
||||
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
def test_add_link_page(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.AddFileLink.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
result = view(request, self.book.id)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_add_link_post(self, *_):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.AddFileLink.as_view()
|
||||
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
|
||||
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
with patch(
|
||||
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
|
||||
) as mock:
|
||||
view(request, self.book.id)
|
||||
self.assertEqual(mock.call_count, 1)
|
||||
|
||||
activity = json.loads(mock.call_args[1]["args"][1])
|
||||
self.assertEqual(activity["type"], "Update")
|
||||
self.assertEqual(activity["object"]["type"], "Edition")
|
||||
|
||||
link = models.FileLink.objects.get()
|
||||
self.assertEqual(link.name, "www.example.com")
|
||||
self.assertEqual(link.url, "https://www.example.com")
|
||||
self.assertEqual(link.filetype, "HTML")
|
||||
|
||||
self.book.refresh_from_db()
|
||||
self.assertEqual(self.book.file_links.first(), link)
|
|
@ -6,6 +6,7 @@ from unittest.mock import patch
|
|||
from django.test import TestCase
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.activitypub.base_activity import set_related_field
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
@ -147,6 +148,48 @@ class InboxUpdate(TestCase):
|
|||
self.assertEqual(book.title, "Piranesi")
|
||||
self.assertEqual(book.last_edited_by, self.remote_user)
|
||||
|
||||
def test_update_edition_links(self):
|
||||
"""add links to edition"""
|
||||
datafile = pathlib.Path(__file__).parent.joinpath("../../data/bw_edition.json")
|
||||
bookdata = json.loads(datafile.read_bytes())
|
||||
del bookdata["authors"]
|
||||
# pylint: disable=line-too-long
|
||||
link_data = {
|
||||
"href": "https://openlibrary.org/books/OL11645413M/Queen_Victoria/daisy",
|
||||
"mediaType": "Daisy",
|
||||
"attributedTo": self.remote_user.remote_id,
|
||||
}
|
||||
bookdata["fileLinks"] = [link_data]
|
||||
|
||||
models.Work.objects.create(
|
||||
title="Test Work", remote_id="https://bookwyrm.social/book/5988"
|
||||
)
|
||||
book = models.Edition.objects.create(
|
||||
title="Test Book", remote_id="https://bookwyrm.social/book/5989"
|
||||
)
|
||||
self.assertFalse(book.file_links.exists())
|
||||
|
||||
with patch(
|
||||
"bookwyrm.activitypub.base_activity.set_related_field.delay"
|
||||
) as mock:
|
||||
views.inbox.activity_task(
|
||||
{
|
||||
"type": "Update",
|
||||
"to": [],
|
||||
"cc": [],
|
||||
"actor": "hi",
|
||||
"id": "sdkjf",
|
||||
"object": bookdata,
|
||||
}
|
||||
)
|
||||
args = mock.call_args[0]
|
||||
self.assertEqual(args[0], "FileLink")
|
||||
self.assertEqual(args[1], "Edition")
|
||||
self.assertEqual(args[2], "book")
|
||||
self.assertEqual(args[3], book.remote_id)
|
||||
self.assertEqual(args[4], link_data)
|
||||
# idk how to test that related name works, because of the transaction
|
||||
|
||||
def test_update_work(self):
|
||||
"""update an existing edition"""
|
||||
datafile = pathlib.Path(__file__).parent.joinpath("../../data/bw_work.json")
|
||||
|
|
|
@ -158,6 +158,27 @@ urlpatterns = [
|
|||
views.EmailBlocklist.as_view(),
|
||||
name="settings-email-blocks-delete",
|
||||
),
|
||||
re_path(
|
||||
r"^setting/link-domains/?$",
|
||||
views.LinkDomain.as_view(),
|
||||
name="settings-link-domain",
|
||||
),
|
||||
re_path(
|
||||
r"^setting/link-domains/(?P<status>(pending|approved|blocked))/?$",
|
||||
views.LinkDomain.as_view(),
|
||||
name="settings-link-domain",
|
||||
),
|
||||
# pylint: disable=line-too-long
|
||||
re_path(
|
||||
r"^setting/link-domains/(?P<status>(pending|approved|blocked))/(?P<domain_id>\d+)/?$",
|
||||
views.LinkDomain.as_view(),
|
||||
name="settings-link-domain",
|
||||
),
|
||||
re_path(
|
||||
r"^setting/link-domains/(?P<domain_id>\d+)/(?P<status>(pending|approved|blocked))/?$",
|
||||
views.update_domain_status,
|
||||
name="settings-link-domain-status",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/ip-blocklist/?$",
|
||||
views.IPBlocklist.as_view(),
|
||||
|
@ -169,10 +190,12 @@ urlpatterns = [
|
|||
name="settings-ip-blocks-delete",
|
||||
),
|
||||
# moderation
|
||||
re_path(r"^settings/reports/?$", views.Reports.as_view(), name="settings-reports"),
|
||||
re_path(
|
||||
r"^settings/reports/?$", views.ReportsAdmin.as_view(), name="settings-reports"
|
||||
),
|
||||
re_path(
|
||||
r"^settings/reports/(?P<report_id>\d+)/?$",
|
||||
views.Report.as_view(),
|
||||
views.ReportAdmin.as_view(),
|
||||
name="settings-report",
|
||||
),
|
||||
re_path(
|
||||
|
@ -195,7 +218,18 @@ urlpatterns = [
|
|||
views.resolve_report,
|
||||
name="settings-report-resolve",
|
||||
),
|
||||
re_path(r"^report/?$", views.make_report, name="report"),
|
||||
re_path(r"^report/?$", views.Report.as_view(), name="report"),
|
||||
re_path(r"^report/(?P<user_id>\d+)/?$", views.Report.as_view(), name="report"),
|
||||
re_path(
|
||||
r"^report/(?P<user_id>\d+)/status/(?P<status_id>\d+)?$",
|
||||
views.Report.as_view(),
|
||||
name="report-status",
|
||||
),
|
||||
re_path(
|
||||
r"^report/(?P<user_id>\d+)/link/(?P<link_id>\d+)?$",
|
||||
views.Report.as_view(),
|
||||
name="report-link",
|
||||
),
|
||||
# landing pages
|
||||
re_path(r"^about/?$", views.about, name="about"),
|
||||
re_path(r"^privacy/?$", views.privacy, name="privacy"),
|
||||
|
@ -430,7 +464,24 @@ urlpatterns = [
|
|||
re_path(
|
||||
r"^upload-cover/(?P<book_id>\d+)/?$", views.upload_cover, name="upload-cover"
|
||||
),
|
||||
re_path(r"^add-description/(?P<book_id>\d+)/?$", views.add_description),
|
||||
re_path(
|
||||
r"^add-description/(?P<book_id>\d+)/?$",
|
||||
views.add_description,
|
||||
name="add-description",
|
||||
),
|
||||
re_path(
|
||||
rf"{BOOK_PATH}/filelink/?$", views.BookFileLinks.as_view(), name="file-link"
|
||||
),
|
||||
re_path(
|
||||
rf"{BOOK_PATH}/filelink/(?P<link_id>\d+)/delete/?$",
|
||||
views.BookFileLinks.as_view(),
|
||||
name="file-link",
|
||||
),
|
||||
re_path(
|
||||
rf"{BOOK_PATH}/filelink/add/?$",
|
||||
views.AddFileLink.as_view(),
|
||||
name="file-link-add",
|
||||
),
|
||||
re_path(r"^resolve-book/?$", views.resolve_book, name="resolve-book"),
|
||||
re_path(r"^switch-edition/?$", views.switch_edition, name="switch-edition"),
|
||||
re_path(
|
||||
|
|
|
@ -9,10 +9,10 @@ from .admin.email_blocklist import EmailBlocklist
|
|||
from .admin.ip_blocklist import IPBlocklist
|
||||
from .admin.invite import ManageInvites, Invite, InviteRequest
|
||||
from .admin.invite import ManageInviteRequests, ignore_invite_request
|
||||
from .admin.link_domains import LinkDomain, update_domain_status
|
||||
from .admin.reports import (
|
||||
Report,
|
||||
Reports,
|
||||
make_report,
|
||||
ReportAdmin,
|
||||
ReportsAdmin,
|
||||
resolve_report,
|
||||
suspend_user,
|
||||
unsuspend_user,
|
||||
|
@ -28,10 +28,16 @@ from .preferences.delete_user import DeleteUser
|
|||
from .preferences.block import Block, unblock
|
||||
|
||||
# books
|
||||
from .books.books import Book, upload_cover, add_description, resolve_book
|
||||
from .books.books import (
|
||||
Book,
|
||||
upload_cover,
|
||||
add_description,
|
||||
resolve_book,
|
||||
)
|
||||
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
|
||||
|
||||
# landing
|
||||
from .landing.about import about, privacy, conduct
|
||||
|
@ -90,6 +96,7 @@ from .notifications import Notifications
|
|||
from .outbox import Outbox
|
||||
from .reading import ReadThrough, delete_readthrough, delete_progressupdate
|
||||
from .reading import ReadingStatus
|
||||
from .report import Report
|
||||
from .rss_feed import RssFeed
|
||||
from .search import Search
|
||||
from .status import CreateStatus, EditStatus, DeleteStatus, update_progress
|
||||
|
|
|
@ -96,6 +96,9 @@ class Dashboard(View):
|
|||
"statuses": status_queryset.count(),
|
||||
"works": models.Work.objects.count(),
|
||||
"reports": models.Report.objects.filter(resolved=False).count(),
|
||||
"pending_domains": models.LinkDomain.objects.filter(
|
||||
status="pending"
|
||||
).count(),
|
||||
"invite_requests": models.InviteRequest.objects.filter(
|
||||
ignored=False, invite_sent=False
|
||||
).count(),
|
||||
|
|
55
bookwyrm/views/admin/link_domains.py
Normal file
55
bookwyrm/views/admin/link_domains.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
""" Manage link domains"""
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
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.moderate_user", raise_exception=True),
|
||||
name="dispatch",
|
||||
)
|
||||
class LinkDomain(View):
|
||||
"""Moderate links"""
|
||||
|
||||
def get(self, request, status="pending"):
|
||||
"""view pending domains"""
|
||||
data = {
|
||||
"domains": models.LinkDomain.objects.filter(status=status).prefetch_related(
|
||||
"links"
|
||||
),
|
||||
"counts": {
|
||||
"pending": models.LinkDomain.objects.filter(status="pending").count(),
|
||||
"approved": models.LinkDomain.objects.filter(status="approved").count(),
|
||||
"blocked": models.LinkDomain.objects.filter(status="blocked").count(),
|
||||
},
|
||||
"form": forms.EmailBlocklistForm(),
|
||||
"status": status,
|
||||
}
|
||||
return TemplateResponse(
|
||||
request, "settings/link_domains/link_domains.html", data
|
||||
)
|
||||
|
||||
def post(self, request, status, domain_id):
|
||||
"""Set display name"""
|
||||
domain = get_object_or_404(models.LinkDomain, id=domain_id)
|
||||
form = forms.LinkDomainForm(request.POST, instance=domain)
|
||||
form.save()
|
||||
return redirect("settings-link-domain", status=status)
|
||||
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
def update_domain_status(request, domain_id, status):
|
||||
"""This domain seems fine"""
|
||||
domain = get_object_or_404(models.LinkDomain, id=domain_id)
|
||||
domain.raise_not_editable(request.user)
|
||||
|
||||
domain.status = status
|
||||
domain.save()
|
||||
return redirect("settings-link-domain", status="pending")
|
|
@ -5,9 +5,8 @@ from django.shortcuts import get_object_or_404, redirect
|
|||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import emailing, forms, models
|
||||
from bookwyrm import forms, models
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
|
@ -20,7 +19,7 @@ from bookwyrm import emailing, forms, models
|
|||
permission_required("bookwyrm.moderate_post", raise_exception=True),
|
||||
name="dispatch",
|
||||
)
|
||||
class Reports(View):
|
||||
class ReportsAdmin(View):
|
||||
"""list of reports"""
|
||||
|
||||
def get(self, request):
|
||||
|
@ -52,7 +51,7 @@ class Reports(View):
|
|||
permission_required("bookwyrm.moderate_post", raise_exception=True),
|
||||
name="dispatch",
|
||||
)
|
||||
class Report(View):
|
||||
class ReportAdmin(View):
|
||||
"""view a specific report"""
|
||||
|
||||
def get(self, request, report_id):
|
||||
|
@ -132,16 +131,3 @@ def resolve_report(_, report_id):
|
|||
if not report.resolved:
|
||||
return redirect("settings-report", report.id)
|
||||
return redirect("settings-reports")
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def make_report(request):
|
||||
"""a user reports something"""
|
||||
form = forms.ReportForm(request.POST)
|
||||
if not form.is_valid():
|
||||
raise ValueError(form.errors)
|
||||
|
||||
report = form.save()
|
||||
emailing.moderation_report_email(report)
|
||||
return redirect(request.headers.get("Referer", "/"))
|
||||
|
|
|
@ -40,7 +40,7 @@ class Book(View):
|
|||
.filter(Q(id=book_id) | Q(parent_work__id=book_id))
|
||||
.order_by("-edition_rank")
|
||||
.select_related("parent_work")
|
||||
.prefetch_related("authors")
|
||||
.prefetch_related("authors", "file_links")
|
||||
.first()
|
||||
)
|
||||
|
||||
|
@ -84,6 +84,7 @@ class Book(View):
|
|||
}
|
||||
|
||||
if request.user.is_authenticated:
|
||||
data["file_link_form"] = forms.FileLinkForm()
|
||||
readthroughs = models.ReadThrough.objects.filter(
|
||||
user=request.user,
|
||||
book=book,
|
||||
|
|
62
bookwyrm/views/books/links.py
Normal file
62
bookwyrm/views/books/links.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
""" the good stuff! the books! """
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.db import transaction
|
||||
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 bookwyrm import forms, models
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
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}
|
||||
)
|
||||
|
||||
def post(self, request, book_id, link_id):
|
||||
"""delete link"""
|
||||
link = get_object_or_404(models.FileLink, id=link_id, book=book_id)
|
||||
link.delete()
|
||||
return self.get(request, book_id)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
|
||||
)
|
||||
class AddFileLink(View):
|
||||
"""a book! this is the stuff"""
|
||||
|
||||
def get(self, request, book_id):
|
||||
"""Create link form"""
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
data = {
|
||||
"file_link_form": forms.FileLinkForm(),
|
||||
"book": book,
|
||||
}
|
||||
return TemplateResponse(request, "book/file_links/file_link_page.html", data)
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, request, book_id, link_id=None):
|
||||
"""Add a link to a copy of the book you can read"""
|
||||
book = get_object_or_404(models.Book.objects.select_subclasses(), id=book_id)
|
||||
link = get_object_or_404(models.FileLink, id=link_id) if link_id else None
|
||||
form = forms.FileLinkForm(request.POST, instance=link)
|
||||
if not form.is_valid():
|
||||
data = {"file_link_form": form, "book": book}
|
||||
return TemplateResponse(
|
||||
request, "book/file_links/file_link_page.html", data
|
||||
)
|
||||
|
||||
link = form.save()
|
||||
book.file_links.add(link)
|
||||
book.last_edited_by = request.user
|
||||
book.save()
|
||||
return redirect("book", book.id)
|
39
bookwyrm/views/report.py
Normal file
39
bookwyrm/views/report.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
""" moderation via flagged posts and users """
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
|
||||
from bookwyrm import emailing, forms, models
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class Report(View):
|
||||
"""Make reports"""
|
||||
|
||||
def get(self, request, user_id, status_id=None, link_id=None):
|
||||
"""static view of report modal"""
|
||||
data = {"user": get_object_or_404(models.User, id=user_id)}
|
||||
if status_id:
|
||||
data["status"] = status_id
|
||||
if link_id:
|
||||
data["link"] = get_object_or_404(models.Link, id=link_id)
|
||||
|
||||
return TemplateResponse(request, "report.html", data)
|
||||
|
||||
def post(self, request):
|
||||
"""a user reports something"""
|
||||
form = forms.ReportForm(request.POST)
|
||||
if not form.is_valid():
|
||||
raise ValueError(form.errors)
|
||||
|
||||
report = form.save()
|
||||
if report.links.exists():
|
||||
# revert the domain to pending
|
||||
domain = report.links.first().domain
|
||||
domain.status = "pending"
|
||||
domain.save()
|
||||
emailing.moderation_report_email(report)
|
||||
return redirect("/")
|
4
bw-dev
4
bw-dev
|
@ -31,7 +31,7 @@ function execweb {
|
|||
|
||||
function initdb {
|
||||
execweb python manage.py migrate
|
||||
execweb python manage.py initdb
|
||||
execweb python manage.py initdb "$@"
|
||||
}
|
||||
|
||||
function makeitblack {
|
||||
|
@ -65,7 +65,7 @@ case "$CMD" in
|
|||
docker-compose run --rm --service-ports web
|
||||
;;
|
||||
initdb)
|
||||
initdb
|
||||
initdb "$@"
|
||||
;;
|
||||
resetdb)
|
||||
clean
|
||||
|
|
Loading…
Reference in a new issue