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 %}
{% trans "Added:" %} {{ book.created_date | naturaltime }} {% trans "Updated:" %} {{ book.updated_date | naturaltime }} {% trans "Last edited by:" %} {{ book.last_edited_by.display_name }}
- Edit "{{ book.title }}"
+ {% if book %}
+ {% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}
+ {% else %}
+ {% trans "Add Book" %}
+ {% endif %}
+ {% if book %}
{{ comment.note }}
+ ++ {% if report.note %}{{ report.note }}{% else %}{% trans "No notes provided" %}{% endif %} +
+