diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index 24d383ac7..15ca5a938 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -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,8 +193,9 @@ class ActivityObject: data[k] = v.serialize() except TypeError: pass - data = {k: v for (k, v) in data.items() if v is not None} - data["@context"] = "https://www.w3.org/ns/activitystreams" + 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,35 +208,32 @@ 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: - data = existing.to_activity() - else: - data = get_data(data) - activity = model.activity_serializer(**data) + if isinstance(data, str): + existing = model.find_existing_by_remote_id(data) + if existing: + data = existing.to_activity() + else: + data = get_data(data) + activity = model.activity_serializer(**data) - # this must exist because it's the object that triggered this function - instance = origin_model.find_existing_by_remote_id(related_remote_id) - if not instance: - raise ValueError(f"Invalid related remote id: {related_remote_id}") + # this must exist because it's the object that triggered this function + instance = origin_model.find_existing_by_remote_id(related_remote_id) + if not instance: + raise ValueError(f"Invalid related remote id: {related_remote_id}") - # set the origin's remote id on the activity so it will be there when - # the model instance is created - # 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 - ) - item = activity.to_model() + # set the origin's remote id on the activity so it will be there when + # the model instance is created + # 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) + item = activity.to_model() - # if the related field isn't serialized (attachments on Status), then - # we have to set it post-creation - if not hasattr(model_field, "activitypub_field"): - setattr(item, related_field_name, instance) - item.save() + # if the related field isn't serialized (attachments on Status), then + # we have to set it post-creation + if not hasattr(model_field, "activitypub_field"): + setattr(item, related_field_name, instance) + item.save() def get_model_from_type(activity_type): @@ -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" diff --git a/bookwyrm/activitypub/book.py b/bookwyrm/activitypub/book.py index d8599c4b3..2238e3a87 100644 --- a/bookwyrm/activitypub/book.py +++ b/bookwyrm/activitypub/book.py @@ -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 diff --git a/bookwyrm/activitypub/person.py b/bookwyrm/activitypub/person.py index 174ead616..576e7f9a6 100644 --- a/bookwyrm/activitypub/person.py +++ b/bookwyrm/activitypub/person.py @@ -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) diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index b9ff59c2e..5af7c4553 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -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): diff --git a/bookwyrm/management/commands/initdb.py b/bookwyrm/management/commands/initdb.py index d0ab648e0..37dd66af4 100644 --- a/bookwyrm/management/commands/initdb.py +++ b/bookwyrm/management/commands/initdb.py @@ -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): - init_groups() - init_permissions() - init_connectors() - init_federated_servers() - init_settings() + 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() diff --git a/bookwyrm/management/commands/populate_lists_streams.py b/bookwyrm/management/commands/populate_lists_streams.py index e3c30baba..0a057401c 100644 --- a/bookwyrm/management/commands/populate_lists_streams.py +++ b/bookwyrm/management/commands/populate_lists_streams.py @@ -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""" diff --git a/bookwyrm/migrations/0126_filelink_link_linkdomain.py b/bookwyrm/migrations/0126_filelink_link_linkdomain.py new file mode 100644 index 000000000..7e5186b6d --- /dev/null +++ b/bookwyrm/migrations/0126_filelink_link_linkdomain.py @@ -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",), + ), + ] diff --git a/bookwyrm/migrations/0127_auto_20220110_2211.py b/bookwyrm/migrations/0127_auto_20220110_2211.py new file mode 100644 index 000000000..e18be29e6 --- /dev/null +++ b/bookwyrm/migrations/0127_auto_20220110_2211.py @@ -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"), + ), + ] diff --git a/bookwyrm/migrations/0128_merge_0126_auto_20220112_2315_0127_auto_20220110_2211.py b/bookwyrm/migrations/0128_merge_0126_auto_20220112_2315_0127_auto_20220110_2211.py new file mode 100644 index 000000000..dc700e99c --- /dev/null +++ b/bookwyrm/migrations/0128_merge_0126_auto_20220112_2315_0127_auto_20220110_2211.py @@ -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 = [] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index c5ea44e0a..4c6305f99 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -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 diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index a9dd9508d..8d1b70ae2 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -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""" diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index 0edcd8fda..e61f912e5 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -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""" diff --git a/bookwyrm/models/link.py b/bookwyrm/models/link.py new file mode 100644 index 000000000..be7c104f0 --- /dev/null +++ b/bookwyrm/models/link.py @@ -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) diff --git a/bookwyrm/models/report.py b/bookwyrm/models/report.py index 636817cb2..a9f5b3b1e 100644 --- a/bookwyrm/models/report.py +++ b/bookwyrm/models/report.py @@ -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",) diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index a63d3f155..6367dcaef 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -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""" diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index 3c8b16942..85be4ce53 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -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") diff --git a/bookwyrm/static/css/bookwyrm.css b/bookwyrm/static/css/bookwyrm.css index cc87e5b81..18c136713 100644 --- a/bookwyrm/static/css/bookwyrm.css +++ b/bookwyrm/static/css/bookwyrm.css @@ -720,6 +720,10 @@ ol.ordered-list li::before { } } +.overflow-wrap-anywhere { + overflow-wrap: anywhere; +} + /* Threads ******************************************************************************/ diff --git a/bookwyrm/static/js/autocomplete.js b/bookwyrm/static/js/autocomplete.js new file mode 100644 index 000000000..896100832 --- /dev/null +++ b/bookwyrm/static/js/autocomplete.js @@ -0,0 +1,165 @@ +(function () { + "use strict"; + + /** + * Suggest a completion as a user types + * + * Use `data-autocomplete=""`on the input field. + * specifying the trie to be used for autocomplete + * + * @example + * + * @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", + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index cf3944b35..94163787d 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -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 diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index e4d13007c..f6d9929dd 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -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" %} -
+
{% if book.subjects %}

{% trans "Subjects" %}

@@ -344,11 +344,11 @@ {% endif %} {% if lists.exists or request.user.list_set.exists %} -
+

{% trans "Lists" %}

@@ -358,7 +358,7 @@
-
+
+ + +

+ {% trans "Links from unknown domains will need to be approved by a moderator before they are added." %} +

+ +
+
+ + + {% include 'snippets/form_errors.html' with errors_list=file_link_form.url.errors id="desc_url" %} +
+
+ + + + {% include 'snippets/form_errors.html' with errors_list=file_link_form.filetype.errors id="desc_filetype" %} +
+
+ +{% endblock %} + +{% block modal-footer %} + +{% if not static %} + +{% endif %} + +{% endblock %} +{% block modal-form-close %}{% endblock %} diff --git a/bookwyrm/templates/book/file_links/edit_links.html b/bookwyrm/templates/book/file_links/edit_links.html new file mode 100644 index 000000000..c56f97a62 --- /dev/null +++ b/bookwyrm/templates/book/file_links/edit_links.html @@ -0,0 +1,82 @@ +{% extends 'layout.html' %} +{% load i18n %} +{% load utilities %} + +{% block title %}{% trans "Edit links" %}{% endblock %} + +{% block content %} + +
+

+ {% blocktrans with title=book|book_title %} + Links for "{{ title }}" + {% endblocktrans %} +

+
+ + + +
+
+ + + + + + + + + {% for link in book.file_links.all %} + + + + + + + + {% endfor %} + {% if not book.file_links.exists %} + + + + {% endif %} +
{% trans "URL" %}{% trans "Added by" %}{% trans "Filetype" %}{% trans "Domain" %}{% trans "Actions" %}
+ {{ link.url }} + + {{ link.added_by.display_name }} + + {{ link.filelink.filetype }} + + {{ link.domain.name }} ({{ link.domain.get_status_display }}) +

+ {% trans "Report spam" %} +

+
+
+ {% csrf_token %} + +
+
{% trans "No links available for this book." %}
+
+ + {% url 'file-link-add' book.id as fallback_url %} +
+ +
+
+ +{% endblock %} diff --git a/bookwyrm/templates/book/file_links/file_link_page.html b/bookwyrm/templates/book/file_links/file_link_page.html new file mode 100644 index 000000000..00efe5089 --- /dev/null +++ b/bookwyrm/templates/book/file_links/file_link_page.html @@ -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 %} + +{% endblock %} diff --git a/bookwyrm/templates/book/file_links/links.html b/bookwyrm/templates/book/file_links/links.html new file mode 100644 index 000000000..10a6da2f2 --- /dev/null +++ b/bookwyrm/templates/book/file_links/links.html @@ -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 %} +
+
+

{% trans "Get a copy" %}

+
+ {% if can_edit_book %} +
+ {% url 'file-link-add' book.id as fallback_url %} +
+ +
+
+ {% endif %} +
+{% if links %} +
    + {% for link in links.all %} + {% join "verify" link.id as verify_modal %} +
  • + {{ link.name }} + ({{ link.filetype }}) +
  • + {% endfor %} +
+{% 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 %} +{% trans "No links available" %} +{% endif %} + +{% if can_edit_book and links.exists %} + + + {% trans "Edit links" %} + +{% include 'book/file_links/add_link_modal.html' with book=book id="add-links" %} +{% endif %} + +{% endif %} diff --git a/bookwyrm/templates/book/file_links/verification_modal.html b/bookwyrm/templates/book/file_links/verification_modal.html new file mode 100644 index 000000000..1d53c1ef2 --- /dev/null +++ b/bookwyrm/templates/book/file_links/verification_modal.html @@ -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: {{ link_url }}.
+Is that where you'd like to go? +{% endblocktrans %} + +{% endblock %} + + +{% block modal-footer %} +{% trans "Continue" %} + + +{% if request.user.is_authenticated %} + +{% endif %} + +{% endblock %} diff --git a/bookwyrm/templates/report.html b/bookwyrm/templates/report.html new file mode 100644 index 000000000..686401ebe --- /dev/null +++ b/bookwyrm/templates/report.html @@ -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 %} diff --git a/bookwyrm/templates/settings/dashboard/dashboard.html b/bookwyrm/templates/settings/dashboard/dashboard.html index f320028b7..65c666a67 100644 --- a/bookwyrm/templates/settings/dashboard/dashboard.html +++ b/bookwyrm/templates/settings/dashboard/dashboard.html @@ -48,6 +48,17 @@
{% endif %} + {% if pending_domains %} + + {% endif %} {% if not site.allow_registration and site.allow_invite_requests and invite_requests %}
diff --git a/bookwyrm/templates/settings/layout.html b/bookwyrm/templates/settings/layout.html index d7a840af5..1addc8db7 100644 --- a/bookwyrm/templates/settings/layout.html +++ b/bookwyrm/templates/settings/layout.html @@ -62,6 +62,10 @@ {% url 'settings-ip-blocks' as url %} {% trans "IP Address Blocklist" %} +
  • + {% url 'settings-link-domain' as url %} + {% trans "Link Domains" %} +
  • {% endif %} {% if perms.bookwyrm.edit_instance_settings %} diff --git a/bookwyrm/templates/settings/link_domains/edit_domain_modal.html b/bookwyrm/templates/settings/link_domains/edit_domain_modal.html new file mode 100644 index 000000000..135783e78 --- /dev/null +++ b/bookwyrm/templates/settings/link_domains/edit_domain_modal.html @@ -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 %} +
    +{% endblock %} + +{% block modal-body %} +{% csrf_token %} + +
    + +
    +{% endblock %} + +{% block modal-footer %} + + +{% endblock %} + +{% block modal-form-close %}
    {% endblock %} diff --git a/bookwyrm/templates/settings/link_domains/link_domains.html b/bookwyrm/templates/settings/link_domains/link_domains.html new file mode 100644 index 000000000..3f2d60976 --- /dev/null +++ b/bookwyrm/templates/settings/link_domains/link_domains.html @@ -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 %} +

    + {% 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." %} +

    + +
    +
    + +
    + + {% for domain in domains %} + {% join "domain" domain.id as domain_modal %} +
    +
    +
    +

    + {{ domain.name }} + ({{ domain.domain }}) +

    +
    +
    + +
    +
    +
    +
    + + + {% trans "View links" %} + ({{ domain.links.count }}) + + + + +
    + {% include "settings/link_domains/link_table.html" with links=domain.links.all|slice:10 %} +
    +
    +
    + + {% include "settings/link_domains/edit_domain_modal.html" with domain=domain id=domain_modal %} + +
    + {% if status != "approved" %} +
    + {% csrf_token %} + +
    + {% endif %} + {% if status != "blocked" %} +
    + {% csrf_token %} + +
    + {% endif %} +
    +
    + {% endfor %} + + {% if not domains.exists %} + {% if status == "approved" %} + {% trans "No domains currently approved" %} + {% elif status == "pending" %} + {% trans "No domains currently pending" %} + {% else %} + {% trans "No domains currently blocked" %} + {% endif %} + {% endif %} +
    + +{% endblock %} + diff --git a/bookwyrm/templates/settings/link_domains/link_table.html b/bookwyrm/templates/settings/link_domains/link_table.html new file mode 100644 index 000000000..62fa17ee2 --- /dev/null +++ b/bookwyrm/templates/settings/link_domains/link_table.html @@ -0,0 +1,42 @@ +{% load i18n %} +{% load utilities %} + + + + + + + + {% block additional_headers %}{% endblock %} + + {% for link in links %} + + + + + + + + {% block additional_data %}{% endblock %} + + {% endfor %} + {% if not links %} + + + + {% endif %} +
    {% trans "URL" %}{% trans "Added by" %}{% trans "Filetype" %}{% trans "Book" %}
    + {{ link.url }} + + @{{ link.added_by|username }} + + {% if link.filelink.filetype %} + {{ link.filelink.filetype }} + {% endif %} + + {% if link.filelink.book %} + {% with book=link.filelink.book %} + {% include "snippets/book_titleby.html" with book=book %} + {% endwith %} + {% endif %} +
    {% trans "No links available for this domain." %}
    diff --git a/bookwyrm/templates/settings/reports/report.html b/bookwyrm/templates/settings/reports/report.html index 37593f3cc..84fafb143 100644 --- a/bookwyrm/templates/settings/reports/report.html +++ b/bookwyrm/templates/settings/reports/report.html @@ -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 %} {% trans "Back to reports" %} {% endblock %} @@ -15,6 +17,36 @@ {% include 'settings/reports/report_preview.html' with report=report %}
    +{% if report.statuses.exists %} +
    +

    {% trans "Reported statuses" %}

    +
      + {% for status in report.statuses.select_subclasses.all %} +
    • + {% if status.deleted %} + {% trans "Status has been deleted" %} + {% else %} + {% include 'snippets/status/status.html' with status=status moderation_mode=True %} + {% endif %} +
    • + {% endfor %} +
    +
    +{% endif %} + +{% if report.links.exists %} +
    +

    {% trans "Reported links" %}

    +
    +
    +
    + {% include "settings/reports/report_links_table.html" with links=report.links.all %} +
    +
    +
    +
    +{% 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 @@
    - -
    -

    {% trans "Reported statuses" %}

    - {% if not report.statuses.exists %} - {% trans "No statuses reported" %} - {% else %} -
      - {% for status in report.statuses.select_subclasses.all %} -
    • - {% if status.deleted %} - {% trans "Status has been deleted" %} - {% else %} - {% include 'snippets/status/status.html' with status=status moderation_mode=True %} - {% endif %} -
    • - {% endfor %} -
    - {% endif %} -
    {% endblock %} diff --git a/bookwyrm/templates/settings/reports/report_header.html b/bookwyrm/templates/settings/reports/report_header.html new file mode 100644 index 000000000..d76db1048 --- /dev/null +++ b/bookwyrm/templates/settings/reports/report_header.html @@ -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 %} diff --git a/bookwyrm/templates/settings/reports/report_links_table.html b/bookwyrm/templates/settings/reports/report_links_table.html new file mode 100644 index 000000000..b0ebf73a5 --- /dev/null +++ b/bookwyrm/templates/settings/reports/report_links_table.html @@ -0,0 +1,20 @@ +{% extends "settings/link_domains/link_table.html" %} +{% load i18n %} + +{% block additional_headers %} +{% trans "Domain" %} +{% trans "Actions" %} +{% endblock %} + +{% block additional_data %} + + + {{ link.domain.domain }} + + + +
    + +
    + +{% endblock %} diff --git a/bookwyrm/templates/settings/reports/report_preview.html b/bookwyrm/templates/settings/reports/report_preview.html index 363783d50..31bde0a1e 100644 --- a/bookwyrm/templates/settings/reports/report_preview.html +++ b/bookwyrm/templates/settings/reports/report_preview.html @@ -1,9 +1,13 @@ {% extends 'components/card.html' %} {% load i18n %} {% load humanize %} +{% load utilities %} + {% block card-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 %} +

    {% endblock %} @@ -17,7 +21,7 @@ {% block card-footer %} {% else %} -

    {% trans "Actions" %}

    +

    {% trans "User Actions" %}

    -
    - {% if user.is_active %} -

    - {% trans "Send direct message" %} -

    - {% endif %} +
    +
    + {% if user.is_active %} +

    + {% trans "Send direct message" %} +

    + {% endif %} - {% if user.is_active or user.deactivation_reason == "pending" %} -
    - {% csrf_token %} - -
    - {% else %} -
    - {% csrf_token %} - -
    + {% if user.is_active or user.deactivation_reason == "pending" %} +
    + {% csrf_token %} + +
    + {% else %} +
    + {% csrf_token %} + +
    + {% endif %} + + {% if user.local %} +
    + {% trans "Permanently delete user" as button_text %} + {% include "snippets/toggle/open_button.html" with controls_text="delete_user" text=button_text class="is-danger is-light" %} +
    + {% endif %} +
    + + {% if user.local %} +
    + {% include "settings/users/delete_user_form.html" with controls_text="delete_user" class="mt-2 mb-2" %} +
    {% endif %} {% if user.local %}
    - {% trans "Permanently delete user" as button_text %} - {% include "snippets/toggle/open_button.html" with controls_text="delete_user" text=button_text class="is-danger is-light" %} +
    + {% csrf_token %} + + {% if group_form.non_field_errors %} + {{ group_form.non_field_errors }} + {% endif %} + {% with group=user.groups.first %} +
    + +
    + + {% include 'snippets/form_errors.html' with errors_list=group_form.groups.errors id="desc_user_group" %} + {% endwith %} + +
    {% endif %}
    - {% if user.local %} -
    - {% include "settings/users/delete_user_form.html" with controls_text="delete_user" class="mt-2 mb-2" %} -
    - {% endif %} - - {% if user.local %} -
    -
    - {% csrf_token %} - - {% if group_form.non_field_errors %} - {{ group_form.non_field_errors }} - {% endif %} - {% with group=user.groups.first %} -
    - -
    - - {% include 'snippets/form_errors.html' with errors_list=group_form.groups.errors id="desc_user_group" %} - {% endwith %} - -
    -
    - {% endif %} - {% endif %}
    diff --git a/bookwyrm/templates/snippets/report_button.html b/bookwyrm/templates/snippets/report_button.html index 6b4a3f253..9d94d2af1 100644 --- a/bookwyrm/templates/snippets/report_button.html +++ b/bookwyrm/templates/snippets/report_button.html @@ -12,6 +12,6 @@ > {% trans "Report" %} -{% 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 %} diff --git a/bookwyrm/templates/snippets/report_modal.html b/bookwyrm/templates/snippets/report_modal.html index afd396448..7d2e52b64 100644 --- a/bookwyrm/templates/snippets/report_modal.html +++ b/bookwyrm/templates/snippets/report_modal.html @@ -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 %} - + {% if status %} - + +{% endif %} +{% if link %} + {% endif %}
    -

    {% blocktrans with site_name=site.name %}This report will be sent to {{ site_name }}'s moderators for review.{% endblocktrans %}

    +

    + {% 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 %} +