diff --git a/.gitignore b/.gitignore index 1384056f..4b5b7fef 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /venv *.pyc *.swp +**/__pycache__ # VSCode /.vscode @@ -15,4 +16,4 @@ /images/ # Testing -.coverage \ No newline at end of file +.coverage diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index 315ff58c..170bdfb9 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -258,10 +258,9 @@ def resolve_remote_id(remote_id, model=None, refresh=False, save=True): # load the data and create the object try: data = get_data(remote_id) - except (ConnectorException, ConnectionError): + except ConnectorException: raise ActivitySerializerError( - "Could not connect to host for remote_id in %s model: %s" - % (model.__name__, remote_id) + "Could not connect to host for remote_id in: %s" % (remote_id) ) # determine the model implicitly, if not provided if not model: diff --git a/bookwyrm/activitypub/verbs.py b/bookwyrm/activitypub/verbs.py index cd7a757b..883eb895 100644 --- a/bookwyrm/activitypub/verbs.py +++ b/bookwyrm/activitypub/verbs.py @@ -70,6 +70,9 @@ class Undo(Verb): if self.object.type == "Follow": model = apps.get_model("bookwyrm.UserFollows") obj = self.object.to_model(model=model, save=False, allow_create=False) + if not obj: + # if we don't have the object, we can't undo it. happens a lot with boosts + return obj.delete() @@ -137,7 +140,7 @@ class Add(Verb): def action(self): """ add obj to collection """ target = resolve_remote_id(self.target, refresh=False) - # we want to related field that isn't the book, this is janky af sorry + # we want to get the related field that isn't the book, this is janky af sorry model = [t for t in type(target)._meta.related_objects if t.name != "edition"][ 0 ].related_model @@ -153,7 +156,11 @@ class Remove(Verb): def action(self): """ find and remove the activity object """ - obj = self.object.to_model(save=False, allow_create=False) + target = resolve_remote_id(self.target, refresh=False) + model = [t for t in type(target)._meta.related_objects if t.name != "edition"][ + 0 + ].related_model + obj = self.to_model(model=model, save=False, allow_create=False) obj.delete() diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index 9f31b337..4b118d64 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -44,21 +44,10 @@ class AbstractMinimalConnector(ABC): if min_confidence: params["min_confidence"] = min_confidence - resp = requests.get( + data = get_data( "%s%s" % (self.search_url, query), params=params, - headers={ - "Accept": "application/json; charset=utf-8", - "User-Agent": settings.USER_AGENT, - }, ) - if not resp.ok: - resp.raise_for_status() - try: - data = resp.json() - except ValueError as e: - logger.exception(e) - raise ConnectorException("Unable to parse json response", e) results = [] for doc in self.parse_search_data(data)[:10]: @@ -68,24 +57,14 @@ class AbstractMinimalConnector(ABC): def isbn_search(self, query): """ isbn search """ params = {} - resp = requests.get( + data = get_data( "%s%s" % (self.isbn_search_url, query), params=params, - headers={ - "Accept": "application/json; charset=utf-8", - "User-Agent": settings.USER_AGENT, - }, ) - if not resp.ok: - resp.raise_for_status() - try: - data = resp.json() - except ValueError as e: - logger.exception(e) - raise ConnectorException("Unable to parse json response", e) results = [] - for doc in self.parse_isbn_search_data(data): + # this shouldn't be returning mutliple results, but just in case + for doc in self.parse_isbn_search_data(data)[:10]: results.append(self.format_isbn_search_result(doc)) return results @@ -234,17 +213,18 @@ def dict_from_mappings(data, mappings): return result -def get_data(url): +def get_data(url, params=None): """ wrapper for request.get """ try: resp = requests.get( url, + params=params, headers={ "Accept": "application/json; charset=utf-8", "User-Agent": settings.USER_AGENT, }, ) - except (RequestError, SSLError) as e: + except (RequestError, SSLError, ConnectionError) as e: logger.exception(e) raise ConnectorException() diff --git a/bookwyrm/connectors/connector_manager.py b/bookwyrm/connectors/connector_manager.py index 3891d02a..3ed25ceb 100644 --- a/bookwyrm/connectors/connector_manager.py +++ b/bookwyrm/connectors/connector_manager.py @@ -18,7 +18,7 @@ def search(query, min_confidence=0.1): results = [] # Have we got a ISBN ? - isbn = re.sub("[\W_]", "", query) + isbn = re.sub(r"[\W_]", "", query) maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13 dedup_slug = lambda r: "%s/%s/%s" % (r.title, r.author, r.year) @@ -36,7 +36,7 @@ def search(query, min_confidence=0.1): pass # if no isbn search or results, we fallback to generic search - if result_set == None or result_set == []: + if result_set in (None, []): try: result_set = connector.search(query, min_confidence=min_confidence) except (HTTPError, ConnectorException): diff --git a/bookwyrm/connectors/openlibrary.py b/bookwyrm/connectors/openlibrary.py index fb9a4e47..c83a65d6 100644 --- a/bookwyrm/connectors/openlibrary.py +++ b/bookwyrm/connectors/openlibrary.py @@ -161,21 +161,17 @@ def ignore_edition(edition_data): """ don't load a million editions that have no metadata """ # an isbn, we love to see it if edition_data.get("isbn_13") or edition_data.get("isbn_10"): - print(edition_data.get("isbn_10")) return False # grudgingly, oclc can stay if edition_data.get("oclc_numbers"): - print(edition_data.get("oclc_numbers")) return False # if it has a cover it can stay if edition_data.get("covers"): - print(edition_data.get("covers")) return False # keep non-english editions if edition_data.get("languages") and "languages/eng" not in str( edition_data.get("languages") ): - print(edition_data.get("languages")) return False return True diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index 380e701f..c6dfa9fe 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -143,7 +143,7 @@ class EditionForm(CustomForm): "created_date", "updated_date", "edition_rank", - "authors", # TODO + "authors", "parent_work", "shelves", "subjects", # TODO @@ -231,3 +231,9 @@ class ListForm(CustomForm): class Meta: model = models.List fields = ["user", "name", "description", "curation", "privacy"] + + +class ReportForm(CustomForm): + class Meta: + model = models.Report + fields = ["user", "reporter", "statuses", "note"] diff --git a/bookwyrm/migrations/0049_auto_20210309_0156.py b/bookwyrm/migrations/0049_auto_20210309_0156.py new file mode 100644 index 00000000..ae9d77a8 --- /dev/null +++ b/bookwyrm/migrations/0049_auto_20210309_0156.py @@ -0,0 +1,113 @@ +# Generated by Django 3.0.7 on 2021-03-09 01:56 + +import bookwyrm.models.fields +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.db.models.expressions + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0048_merge_20210308_1754"), + ] + + operations = [ + migrations.CreateModel( + name="Report", + 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], + ), + ), + ("note", models.TextField(blank=True, null=True)), + ("resolved", models.BooleanField(default=False)), + ( + "reporter", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="reporter", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "statuses", + models.ManyToManyField(blank=True, null=True, to="bookwyrm.Status"), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="ReportComment", + 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], + ), + ), + ("note", models.TextField()), + ( + "report", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="bookwyrm.Report", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddConstraint( + model_name="report", + constraint=models.CheckConstraint( + check=models.Q( + _negated=True, reporter=django.db.models.expressions.F("user") + ), + name="self_report", + ), + ), + ] diff --git a/bookwyrm/migrations/0050_auto_20210313_0030.py b/bookwyrm/migrations/0050_auto_20210313_0030.py new file mode 100644 index 00000000..8c81c452 --- /dev/null +++ b/bookwyrm/migrations/0050_auto_20210313_0030.py @@ -0,0 +1,26 @@ +# Generated by Django 3.0.7 on 2021-03-13 00:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0049_auto_20210309_0156"), + ] + + operations = [ + migrations.AlterModelOptions( + name="report", + options={"ordering": ("-created_date",)}, + ), + migrations.AlterModelOptions( + name="reportcomment", + options={"ordering": ("-created_date",)}, + ), + migrations.AlterField( + model_name="report", + name="statuses", + field=models.ManyToManyField(blank=True, to="bookwyrm.Status"), + ), + ] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index 67ee16d3..326a673e 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -21,6 +21,7 @@ from .tag import Tag, UserTag from .user import User, KeyPair, AnnualGoal from .relationship import UserFollows, UserFollowRequest, UserBlocks +from .report import Report, ReportComment from .federated_server import FederatedServer from .import_job import ImportJob, ImportItem diff --git a/bookwyrm/models/activitypub_mixin.py b/bookwyrm/models/activitypub_mixin.py index 4ced78c2..0a3c33a1 100644 --- a/bookwyrm/models/activitypub_mixin.py +++ b/bookwyrm/models/activitypub_mixin.py @@ -362,14 +362,15 @@ class CollectionItemMixin(ActivitypubMixin): """ broadcast a remove activity """ activity = self.to_remove_activity() super().delete(*args, **kwargs) - self.broadcast(activity, self.user) + if self.user.local: + self.broadcast(activity, self.user) def to_add_activity(self): """ AP for shelving a book""" object_field = getattr(self, self.object_field) collection_field = getattr(self, self.collection_field) return activitypub.Add( - id="%s#add" % self.remote_id, + id=self.remote_id, actor=self.user.remote_id, object=object_field, target=collection_field.remote_id, @@ -380,7 +381,7 @@ class CollectionItemMixin(ActivitypubMixin): object_field = getattr(self, self.object_field) collection_field = getattr(self, self.collection_field) return activitypub.Remove( - id="%s#remove" % self.remote_id, + id=self.remote_id, actor=self.user.remote_id, object=object_field, target=collection_field.remote_id, @@ -456,8 +457,8 @@ def broadcast_task(sender_id, activity, recipients): for recipient in recipients: try: sign_and_send(sender, activity, recipient) - except (HTTPError, SSLError, ConnectionError) as e: - logger.exception(e) + except (HTTPError, SSLError, ConnectionError): + pass def sign_and_send(sender, data, destination): diff --git a/bookwyrm/models/federated_server.py b/bookwyrm/models/federated_server.py index ce804310..8f7d903e 100644 --- a/bookwyrm/models/federated_server.py +++ b/bookwyrm/models/federated_server.py @@ -4,7 +4,7 @@ from .base_model import BookWyrmModel class FederatedServer(BookWyrmModel): - """ store which server's we federate with """ + """ store which servers we federate with """ server_name = models.CharField(max_length=255, unique=True) # federated, blocked, whatever else diff --git a/bookwyrm/models/report.py b/bookwyrm/models/report.py new file mode 100644 index 00000000..6a76d9fd --- /dev/null +++ b/bookwyrm/models/report.py @@ -0,0 +1,37 @@ +""" flagged for moderation """ +from django.db import models +from django.db.models import F, Q +from .base_model import BookWyrmModel + + +class Report(BookWyrmModel): + """ reported status or user """ + + reporter = models.ForeignKey( + "User", related_name="reporter", on_delete=models.PROTECT + ) + note = models.TextField(null=True, blank=True) + user = models.ForeignKey("User", on_delete=models.PROTECT) + statuses = models.ManyToManyField("Status", blank=True) + resolved = models.BooleanField(default=False) + + class Meta: + """ don't let users report themselves """ + + constraints = [ + models.CheckConstraint(check=~Q(reporter=F("user")), name="self_report") + ] + ordering = ("-created_date",) + + +class ReportComment(BookWyrmModel): + """ updates on a report """ + + user = models.ForeignKey("User", on_delete=models.PROTECT) + note = models.TextField() + report = models.ForeignKey(Report, on_delete=models.PROTECT) + + class Meta: + """ sort comments """ + + ordering = ("-created_date",) diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 80f2b593..c5e69936 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -115,13 +115,18 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): def ignore_activity(cls, activity): """ keep notes if they are replies to existing statuses """ if activity.type == "Announce": - # keep it if the booster or the boosted are local - boosted = activitypub.resolve_remote_id(activity.object, save=False) + try: + boosted = activitypub.resolve_remote_id(activity.object, save=False) + except activitypub.ActivitySerializerError: + # if we can't load the status, definitely ignore it + return True + # keep the boost if we would keep the status return cls.ignore_activity(boosted.to_activity_dataclass()) # keep if it if it's a custom type if activity.type != "Note": return False + # keep it if it's a reply to an existing status if cls.objects.filter(remote_id=activity.inReplyTo).exists(): return False diff --git a/bookwyrm/static/js/shared.js b/bookwyrm/static/js/shared.js index 65f0dd65..391efcc2 100644 --- a/bookwyrm/static/js/shared.js +++ b/bookwyrm/static/js/shared.js @@ -209,249 +209,249 @@ function removeClass(el, className) { */ class TabGroup { constructor(container) { - this.container = container; - - this.tablist = this.container.querySelector('[role="tablist"]'); - this.buttons = this.tablist.querySelectorAll('[role="tab"]'); - this.panels = this.container.querySelectorAll(':scope > [role="tabpanel"]'); - this.delay = this.determineDelay(); - - if(!this.tablist || !this.buttons.length || !this.panels.length) { - return; - } - - this.keys = this.keys(); - this.direction = this.direction(); - this.initButtons(); - this.initPanels(); + this.container = container; + + this.tablist = this.container.querySelector('[role="tablist"]'); + this.buttons = this.tablist.querySelectorAll('[role="tab"]'); + this.panels = this.container.querySelectorAll(':scope > [role="tabpanel"]'); + this.delay = this.determineDelay(); + + if(!this.tablist || !this.buttons.length || !this.panels.length) { + return; + } + + this.keys = this.keys(); + this.direction = this.direction(); + this.initButtons(); + this.initPanels(); } - + keys() { - return { - end: 35, - home: 36, - left: 37, - up: 38, - right: 39, - down: 40 - }; + return { + end: 35, + home: 36, + left: 37, + up: 38, + right: 39, + down: 40 + }; } - + // Add or substract depending on key pressed direction() { - return { - 37: -1, - 38: -1, - 39: 1, - 40: 1 - }; + return { + 37: -1, + 38: -1, + 39: 1, + 40: 1 + }; } - - initButtons() { - let count = 0; - for(let button of this.buttons) { - let isSelected = button.getAttribute("aria-selected") === "true"; - button.setAttribute("tabindex", isSelected ? "0" : "-1"); - - button.addEventListener('click', this.clickEventListener.bind(this)); - button.addEventListener('keydown', this.keydownEventListener.bind(this)); - button.addEventListener('keyup', this.keyupEventListener.bind(this)); - - button.index = count++; - } - } - - initPanels() { - let selectedPanelId = this.tablist.querySelector('[role="tab"][aria-selected="true"]').getAttribute("aria-controls"); - for(let panel of this.panels) { - if(panel.getAttribute("id") !== selectedPanelId) { - panel.setAttribute("hidden", ""); - } - panel.setAttribute("tabindex", "0"); - } - } - - clickEventListener(event) { - let button = event.target.closest('a'); - event.preventDefault(); - - this.activateTab(button, false); + initButtons() { + let count = 0; + for(let button of this.buttons) { + let isSelected = button.getAttribute("aria-selected") === "true"; + button.setAttribute("tabindex", isSelected ? "0" : "-1"); + + button.addEventListener('click', this.clickEventListener.bind(this)); + button.addEventListener('keydown', this.keydownEventListener.bind(this)); + button.addEventListener('keyup', this.keyupEventListener.bind(this)); + + button.index = count++; + } } - + + initPanels() { + let selectedPanelId = this.tablist.querySelector('[role="tab"][aria-selected="true"]').getAttribute("aria-controls"); + for(let panel of this.panels) { + if(panel.getAttribute("id") !== selectedPanelId) { + panel.setAttribute("hidden", ""); + } + panel.setAttribute("tabindex", "0"); + } + } + + clickEventListener(event) { + let button = event.target.closest('a'); + + event.preventDefault(); + + this.activateTab(button, false); + } + // Handle keydown on tabs keydownEventListener(event) { - var key = event.keyCode; - - switch (key) { - case this.keys.end: - event.preventDefault(); - // Activate last tab - this.activateTab(this.buttons[this.buttons.length - 1]); - break; - case this.keys.home: - event.preventDefault(); - // Activate first tab - this.activateTab(this.buttons[0]); - break; - - // Up and down are in keydown - // because we need to prevent page scroll >:) - case this.keys.up: - case this.keys.down: - this.determineOrientation(event); - break; - }; + var key = event.keyCode; + + switch (key) { + case this.keys.end: + event.preventDefault(); + // Activate last tab + this.activateTab(this.buttons[this.buttons.length - 1]); + break; + case this.keys.home: + event.preventDefault(); + // Activate first tab + this.activateTab(this.buttons[0]); + break; + + // Up and down are in keydown + // because we need to prevent page scroll >:) + case this.keys.up: + case this.keys.down: + this.determineOrientation(event); + break; + } } - + // Handle keyup on tabs keyupEventListener(event) { - var key = event.keyCode; - - switch (key) { - case this.keys.left: - case this.keys.right: - this.determineOrientation(event); - break; - }; + var key = event.keyCode; + + switch (key) { + case this.keys.left: + case this.keys.right: + this.determineOrientation(event); + break; + } } - + // When a tablist’s aria-orientation is set to vertical, // only up and down arrow should function. // In all other cases only left and right arrow function. determineOrientation(event) { - var key = event.keyCode; - var vertical = this.tablist.getAttribute('aria-orientation') == 'vertical'; - var proceed = false; - - if (vertical) { - if (key === this.keys.up || key === this.keys.down) { - event.preventDefault(); - proceed = true; - }; - } - else { - if (key === this.keys.left || key === this.keys.right) { - proceed = true; - }; - }; - - if (proceed) { - this.switchTabOnArrowPress(event); - }; + var key = event.keyCode; + var vertical = this.tablist.getAttribute('aria-orientation') == 'vertical'; + var proceed = false; + + if (vertical) { + if (key === this.keys.up || key === this.keys.down) { + event.preventDefault(); + proceed = true; + } + } + else { + if (key === this.keys.left || key === this.keys.right) { + proceed = true; + } + } + + if (proceed) { + this.switchTabOnArrowPress(event); + } } - + // Either focus the next, previous, first, or last tab // depending on key pressed switchTabOnArrowPress(event) { - var pressed = event.keyCode; - - for (let button of this.buttons) { - button.addEventListener('focus', this.focusEventHandler.bind(this)); - }; - - if (this.direction[pressed]) { - var target = event.target; - if (target.index !== undefined) { - if (this.buttons[target.index + this.direction[pressed]]) { - this.buttons[target.index + this.direction[pressed]].focus(); - } - else if (pressed === this.keys.left || pressed === this.keys.up) { - this.focusLastTab(); - } - else if (pressed === this.keys.right || pressed == this.keys.down) { - this.focusFirstTab(); - } + var pressed = event.keyCode; + + for (let button of this.buttons) { + button.addEventListener('focus', this.focusEventHandler.bind(this)); + } + + if (this.direction[pressed]) { + var target = event.target; + if (target.index !== undefined) { + if (this.buttons[target.index + this.direction[pressed]]) { + this.buttons[target.index + this.direction[pressed]].focus(); + } + else if (pressed === this.keys.left || pressed === this.keys.up) { + this.focusLastTab(); + } + else if (pressed === this.keys.right || pressed == this.keys.down) { + this.focusFirstTab(); + } + } } - } } - + // Activates any given tab panel activateTab (tab, setFocus) { - if(tab.getAttribute("role") !== "tab") { - tab = tab.closest('[role="tab"]'); - } - - setFocus = setFocus || true; - - // Deactivate all other tabs - this.deactivateTabs(); - - // Remove tabindex attribute - tab.removeAttribute('tabindex'); - - // Set the tab as selected - tab.setAttribute('aria-selected', 'true'); - - // Give the tab parent an is-active class - tab.parentNode.classList.add('is-active'); - - // Get the value of aria-controls (which is an ID) - var controls = tab.getAttribute('aria-controls'); - - // Remove hidden attribute from tab panel to make it visible - document.getElementById(controls).removeAttribute('hidden'); - - // Set focus when required - if (setFocus) { - tab.focus(); - } + if(tab.getAttribute("role") !== "tab") { + tab = tab.closest('[role="tab"]'); + } + + setFocus = setFocus || true; + + // Deactivate all other tabs + this.deactivateTabs(); + + // Remove tabindex attribute + tab.removeAttribute('tabindex'); + + // Set the tab as selected + tab.setAttribute('aria-selected', 'true'); + + // Give the tab parent an is-active class + tab.parentNode.classList.add('is-active'); + + // Get the value of aria-controls (which is an ID) + var controls = tab.getAttribute('aria-controls'); + + // Remove hidden attribute from tab panel to make it visible + document.getElementById(controls).removeAttribute('hidden'); + + // Set focus when required + if (setFocus) { + tab.focus(); + } } - + // Deactivate all tabs and tab panels deactivateTabs() { - for (let button of this.buttons) { - button.parentNode.classList.remove('is-active'); - button.setAttribute('tabindex', '-1'); - button.setAttribute('aria-selected', 'false'); - button.removeEventListener('focus', this.focusEventHandler.bind(this)); - } - - for (let panel of this.panels) { - panel.setAttribute('hidden', 'hidden'); - } + for (let button of this.buttons) { + button.parentNode.classList.remove('is-active'); + button.setAttribute('tabindex', '-1'); + button.setAttribute('aria-selected', 'false'); + button.removeEventListener('focus', this.focusEventHandler.bind(this)); + } + + for (let panel of this.panels) { + panel.setAttribute('hidden', 'hidden'); + } } - + focusFirstTab() { - this.buttons[0].focus(); + this.buttons[0].focus(); } - + focusLastTab() { - this.buttons[this.buttons.length - 1].focus(); + this.buttons[this.buttons.length - 1].focus(); } - + // Determine whether there should be a delay // when user navigates with the arrow keys determineDelay() { - var hasDelay = this.tablist.hasAttribute('data-delay'); - var delay = 0; - - if (hasDelay) { - var delayValue = this.tablist.getAttribute('data-delay'); - if (delayValue) { - delay = delayValue; + var hasDelay = this.tablist.hasAttribute('data-delay'); + var delay = 0; + + if (hasDelay) { + var delayValue = this.tablist.getAttribute('data-delay'); + if (delayValue) { + delay = delayValue; + } + else { + // If no value is specified, default to 300ms + delay = 300; + } } - else { - // If no value is specified, default to 300ms - delay = 300; - }; - }; - - return delay; + + return delay; } - + focusEventHandler(event) { - var target = event.target; - - setTimeout(this.checkTabFocus.bind(this), this.delay, target); - }; - + var target = event.target; + + setTimeout(this.checkTabFocus.bind(this), this.delay, target); + } + // Only activate tab on focus if it still has focus after the delay checkTabFocus(target) { - let focused = document.activeElement; - - if (target === focused) { - this.activateTab(target, false); - } + let focused = document.activeElement; + + if (target === focused) { + this.activateTab(target, false); + } } - } +} diff --git a/bookwyrm/templates/edit_book.html b/bookwyrm/templates/edit_book.html index 7374e6a0..7c660e9c 100644 --- a/bookwyrm/templates/edit_book.html +++ b/bookwyrm/templates/edit_book.html @@ -2,18 +2,24 @@ {% load i18n %} {% load humanize %} -{% block title %}{% trans "Edit Book" %}{% endblock %} +{% block title %}{% if book %}{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}{% else %}{% trans "Add Book" %}{% endif %}{% endblock %} {% block content %}

- Edit "{{ book.title }}" + {% if book %} + {% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %} + {% else %} + {% trans "Add Book" %} + {% endif %}

+ {% if book %}

{% trans "Added:" %} {{ book.created_date | naturaltime }}

{% trans "Updated:" %} {{ book.updated_date | naturaltime }}

{% trans "Last edited by:" %} {{ book.last_edited_by.display_name }}

+ {% endif %}
{% if form.non_field_errors %} @@ -22,40 +28,111 @@ {% endif %} -
+{% if book %} + +{% else %} + +{% endif %} + {% csrf_token %} + {% if confirm_mode %} +
+

{% trans "Confirm Book Info" %}

+
+ {% if author_matches %} +
+ {% for author in author_matches %} +
+ {% blocktrans with name=author.name %}Is "{{ name }}" an existing author?{% endblocktrans %} + {% with forloop.counter as counter %} + {% for match in author.matches %} + +

+ {% blocktrans with book_title=match.book_set.first.title %}Author of {{ book_title }}{% endblocktrans %} +

+ {% endfor %} + + {% endwith %} +
+ {% endfor %} +
+ {% else %} +

{% blocktrans with name=add_author %}Creating a new author: {{ name }}{% endblocktrans %}

+ {% endif %} + + {% if not book %} +
+
+ {% trans "Is this an edition of an existing work?" %} + {% for match in book_matches %} + + {% endfor %} + +
+
+ {% endif %} +
+ + + + {% trans "Back" %} + +
+ +
+ {% endif %} +
-

{% trans "Metadata" %}

-

{{ form.title }}

- {% for error in form.title.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.subtitle }}

- {% for error in form.subtitle.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.description }}

- {% for error in form.description.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.series }}

- {% for error in form.series.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.series_number }}

- {% for error in form.series_number.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.first_published_date }}

- {% for error in form.first_published_date.errors %} -

{{ error | escape }}

- {% endfor %} -

{{ form.published_date }}

- {% for error in form.published_date.errors %} -

{{ error | escape }}

- {% endfor %} +
+

{% trans "Metadata" %}

+

{{ form.title }}

+ {% for error in form.title.errors %} +

{{ error | escape }}

+ {% endfor %} +

{{ form.subtitle }}

+ {% for error in form.subtitle.errors %} +

{{ error | escape }}

+ {% endfor %} +

{{ form.description }}

+ {% for error in form.description.errors %} +

{{ error | escape }}

+ {% endfor %} +

{{ form.series }}

+ {% for error in form.series.errors %} +

{{ error | escape }}

+ {% endfor %} +

{{ form.series_number }}

+ {% for error in form.series_number.errors %} +

{{ error | escape }}

+ {% endfor %} +

{{ form.first_published_date }}

+ {% for error in form.first_published_date.errors %} +

{{ error | escape }}

+ {% endfor %} +

{{ form.published_date }}

+ {% for error in form.published_date.errors %} +

{{ error | escape }}

+ {% endfor %} +
+ +
+

{% trans "Authors" %}

+ {% if book.authors.exists %} +
+ {% for author in book.authors.all %} + + {% endfor %} +
+ {% endif %} + +

Separate multiple author names with commas.

+ +
@@ -116,10 +193,12 @@
+ {% if not confirm_mode %}
{% trans "Cancel" %}
+ {% endif %}
{% endblock %} diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index 901a12ff..fc2ebdb7 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -114,8 +114,8 @@ {% endif %} {% if perms.bookwyrm.edit_instance_settings %}
  • - - {% trans 'Site Configuration' %} + + {% trans 'Admin' %}
  • {% endif %} diff --git a/bookwyrm/templates/moderation/report.html b/bookwyrm/templates/moderation/report.html new file mode 100644 index 00000000..a231c41c --- /dev/null +++ b/bookwyrm/templates/moderation/report.html @@ -0,0 +1,76 @@ +{% extends 'settings/admin_layout.html' %} +{% load i18n %} +{% load humanize %} + +{% block title %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %} +{% block header %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %} + +{% block panel %} +
    + {% trans "Back to reports" %} +
    + +
    + {% include 'moderation/report_preview.html' with report=report %} +
    + +
    +

    {% trans "Actions" %}

    +

    {% trans "View user profile" %}

    +
    +

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

    +
    + {% csrf_token %} + {% if report.user.is_active %} + + {% else %} + + {% endif %} +
    +
    +
    + +
    +

    {% trans "Moderator Comments" %}

    + {% for comment in report.reportcomment_set.all %} +
    +

    {{ comment.note }}

    + +
    + {% endfor %} +
    + {% csrf_token %} + + + +
    +
    + +
    +

    {% trans "Reported statuses" %}

    + {% if not report.statuses.exists %} + {% trans "No statuses reported" %} + {% else %} + + {% endif %} +
    +{% endblock %} diff --git a/bookwyrm/templates/moderation/report_modal.html b/bookwyrm/templates/moderation/report_modal.html new file mode 100644 index 00000000..ce8408ee --- /dev/null +++ b/bookwyrm/templates/moderation/report_modal.html @@ -0,0 +1,37 @@ +{% extends 'components/modal.html' %} +{% load i18n %} +{% load humanize %} + +{% block modal-title %} +{% blocktrans with username=user.username %}Report @{{ username }}{% endblocktrans %} +{% endblock %} + +{% block modal-form-open %} +
    +{% endblock %} + +{% block modal-body %} + +{% csrf_token %} + + + + +
    +

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

    + + +
    + +{% endblock %} + + +{% block modal-footer %} + + +{% trans "Cancel" as button_text %} +{% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="report" controls_uid=report_uuid class="" %} + +{% endblock %} +{% block modal-form-close %}
    {% endblock %} + diff --git a/bookwyrm/templates/moderation/report_preview.html b/bookwyrm/templates/moderation/report_preview.html new file mode 100644 index 00000000..363783d5 --- /dev/null +++ b/bookwyrm/templates/moderation/report_preview.html @@ -0,0 +1,37 @@ +{% extends 'components/card.html' %} +{% load i18n %} +{% load humanize %} +{% block card-header %} +

    + {% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %} +

    +{% endblock %} + +{% block card-content %} +
    +

    + {% if report.note %}{{ report.note }}{% else %}{% trans "No notes provided" %}{% endif %} +

    +
    +{% endblock %} + +{% block card-footer %} + + + +{% endblock %} diff --git a/bookwyrm/templates/moderation/reports.html b/bookwyrm/templates/moderation/reports.html new file mode 100644 index 00000000..ebf29a7a --- /dev/null +++ b/bookwyrm/templates/moderation/reports.html @@ -0,0 +1,28 @@ +{% extends 'settings/admin_layout.html' %} +{% load i18n %} + +{% block title %}{% trans "Reports" %}{% endblock %} +{% block header %}{% trans "Reports" %}{% endblock %} + +{% block panel %} +
    + +
    + +
    + {% for report in reports %} +
    + {% include 'moderation/report_preview.html' with report=report %} +
    + {% endfor %} +
    + +{% endblock %} + diff --git a/bookwyrm/templates/notifications.html b/bookwyrm/templates/notifications.html index 80ee2250..3f0300bd 100644 --- a/bookwyrm/templates/notifications.html +++ b/bookwyrm/templates/notifications.html @@ -115,15 +115,7 @@
    - {% if related_status.content %} - - {{ related_status.content | safe | truncatewords_html:10 }}{% if related_status.mention_books %} {{ related_status.mention_books.first.title }}{% endif %} - - {% elif related_status.quote %} - {{ related_status.quote | safe | truncatewords_html:10 }} - {% elif related_status.rating %} - {% include 'snippets/stars.html' with rating=related_status.rating %} - {% endif %} + {% include 'snippets/status_preview.html' with status=related_status %}
    {{ related_status.published_date | post_date }} diff --git a/bookwyrm/templates/search_results.html b/bookwyrm/templates/search_results.html index 4e8481f0..13497df8 100644 --- a/bookwyrm/templates/search_results.html +++ b/bookwyrm/templates/search_results.html @@ -68,6 +68,10 @@ {% include 'snippets/toggle/close_button.html' with text=button_text small=True controls_text="more-results" %} {% endif %}
    + + {% endif %}
    diff --git a/bookwyrm/templates/settings/admin_layout.html b/bookwyrm/templates/settings/admin_layout.html index 16741436..a34fe638 100644 --- a/bookwyrm/templates/settings/admin_layout.html +++ b/bookwyrm/templates/settings/admin_layout.html @@ -18,6 +18,10 @@ {% url 'settings-invites' as url %} {% trans "Invites" %} +
  • + {% url 'settings-reports' as url %} + {% trans "Reports" %} +
  • {% url 'settings-federation' as url %} {% trans "Federated Servers" %} @@ -42,7 +46,7 @@ {% endif %} -
    +
    {% block panel %}{% endblock %}
    diff --git a/bookwyrm/templates/snippets/report_button.html b/bookwyrm/templates/snippets/report_button.html new file mode 100644 index 00000000..2fa0a3f3 --- /dev/null +++ b/bookwyrm/templates/snippets/report_button.html @@ -0,0 +1,10 @@ +{% load i18n %} +{% load bookwyrm_tags %} +{% with 0|uuid as report_uuid %} + +{% trans "Report" as button_text %} +{% include 'snippets/toggle/toggle_button.html' with class="is-danger is-light is-small is-fullwidth" text=button_text controls_text="report" controls_uid=report_uuid focus="modal-title-report" disabled=is_current %} + +{% include 'moderation/report_modal.html' with user=user reporter=request.user controls_text="report" controls_uid=report_uuid %} + +{% endwith %} diff --git a/bookwyrm/templates/snippets/status/status_body.html b/bookwyrm/templates/snippets/status/status_body.html index 8d6c21ed..a7e8e884 100644 --- a/bookwyrm/templates/snippets/status/status_body.html +++ b/bookwyrm/templates/snippets/status/status_body.html @@ -18,7 +18,17 @@ {% block card-footer %}