Merge branch 'bookwyrm-social:main' into url-names
This commit is contained in:
commit
5a2bf64864
135 changed files with 15863 additions and 3417 deletions
|
@ -4,6 +4,7 @@ from django import forms
|
|||
from bookwyrm import models
|
||||
from bookwyrm.models.fields import ClearableFileInputWithWarning
|
||||
from .custom_form import CustomForm
|
||||
from .widgets import ArrayWidget, SelectDateWidget, Select
|
||||
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
|
@ -14,14 +15,6 @@ class CoverForm(CustomForm):
|
|||
help_texts = {f: None for f in fields}
|
||||
|
||||
|
||||
class ArrayWidget(forms.widgets.TextInput):
|
||||
# pylint: disable=unused-argument
|
||||
# pylint: disable=no-self-use
|
||||
def value_from_datadict(self, data, files, name):
|
||||
"""get all values for this name"""
|
||||
return [i for i in data.getlist(name) if i]
|
||||
|
||||
|
||||
class EditionForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Edition
|
||||
|
@ -56,16 +49,16 @@ class EditionForm(CustomForm):
|
|||
"publishers": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_publishers_help desc_publishers"}
|
||||
),
|
||||
"first_published_date": forms.SelectDateWidget(
|
||||
"first_published_date": SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_first_published_date"}
|
||||
),
|
||||
"published_date": forms.SelectDateWidget(
|
||||
"published_date": SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_published_date"}
|
||||
),
|
||||
"cover": ClearableFileInputWithWarning(
|
||||
attrs={"aria-describedby": "desc_cover"}
|
||||
),
|
||||
"physical_format": forms.Select(
|
||||
"physical_format": Select(
|
||||
attrs={"aria-describedby": "desc_physical_format"}
|
||||
),
|
||||
"physical_format_detail": forms.TextInput(
|
||||
|
@ -85,3 +78,27 @@ class EditionForm(CustomForm):
|
|||
),
|
||||
"ASIN": forms.TextInput(attrs={"aria-describedby": "desc_ASIN"}),
|
||||
}
|
||||
|
||||
|
||||
class EditionFromWorkForm(CustomForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# make all fields hidden
|
||||
for visible in self.visible_fields():
|
||||
visible.field.widget = forms.HiddenInput()
|
||||
|
||||
class Meta:
|
||||
model = models.Work
|
||||
fields = [
|
||||
"title",
|
||||
"subtitle",
|
||||
"authors",
|
||||
"description",
|
||||
"languages",
|
||||
"series",
|
||||
"series_number",
|
||||
"subjects",
|
||||
"subject_places",
|
||||
"cover",
|
||||
"first_published_date",
|
||||
]
|
||||
|
|
|
@ -45,7 +45,7 @@ class ReportForm(CustomForm):
|
|||
|
||||
class ReadThroughForm(CustomForm):
|
||||
def clean(self):
|
||||
"""make sure the email isn't in use by a registered user"""
|
||||
"""don't let readthroughs end before they start"""
|
||||
cleaned_data = super().clean()
|
||||
start_date = cleaned_data.get("start_date")
|
||||
finish_date = cleaned_data.get("finish_date")
|
||||
|
|
|
@ -42,4 +42,4 @@ class InviteRequestForm(CustomForm):
|
|||
|
||||
class Meta:
|
||||
model = models.InviteRequest
|
||||
fields = ["email"]
|
||||
fields = ["email", "answer"]
|
||||
|
|
70
bookwyrm/forms/widgets.py
Normal file
70
bookwyrm/forms/widgets.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
""" using django model forms """
|
||||
from django import forms
|
||||
|
||||
|
||||
class ArrayWidget(forms.widgets.TextInput):
|
||||
"""Inputs for postgres array fields"""
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
# pylint: disable=no-self-use
|
||||
def value_from_datadict(self, data, files, name):
|
||||
"""get all values for this name"""
|
||||
return [i for i in data.getlist(name) if i]
|
||||
|
||||
|
||||
class Select(forms.Select):
|
||||
"""custom template for select widget"""
|
||||
|
||||
template_name = "widgets/select.html"
|
||||
|
||||
|
||||
class SelectDateWidget(forms.SelectDateWidget):
|
||||
"""
|
||||
A widget that splits date input into two <select> boxes and a numerical year.
|
||||
"""
|
||||
|
||||
template_name = "widgets/addon_multiwidget.html"
|
||||
select_widget = Select
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
"""sets individual widgets"""
|
||||
context = super().get_context(name, value, attrs)
|
||||
date_context = {}
|
||||
year_name = self.year_field % name
|
||||
date_context["year"] = forms.NumberInput().get_context(
|
||||
name=year_name,
|
||||
value=context["widget"]["value"]["year"],
|
||||
attrs={
|
||||
**context["widget"]["attrs"],
|
||||
"id": f"id_{year_name}",
|
||||
"class": "input",
|
||||
},
|
||||
)
|
||||
month_choices = list(self.months.items())
|
||||
if not self.is_required:
|
||||
month_choices.insert(0, self.month_none_value)
|
||||
month_name = self.month_field % name
|
||||
date_context["month"] = self.select_widget(
|
||||
attrs, choices=month_choices
|
||||
).get_context(
|
||||
name=month_name,
|
||||
value=context["widget"]["value"]["month"],
|
||||
attrs={**context["widget"]["attrs"], "id": f"id_{month_name}"},
|
||||
)
|
||||
day_choices = [(i, i) for i in range(1, 32)]
|
||||
if not self.is_required:
|
||||
day_choices.insert(0, self.day_none_value)
|
||||
day_name = self.day_field % name
|
||||
date_context["day"] = self.select_widget(
|
||||
attrs,
|
||||
choices=day_choices,
|
||||
).get_context(
|
||||
name=day_name,
|
||||
value=context["widget"]["value"]["day"],
|
||||
attrs={**context["widget"]["attrs"], "id": f"id_{day_name}"},
|
||||
)
|
||||
subwidgets = []
|
||||
for field in self._parse_date_fmt():
|
||||
subwidgets.append(date_context[field]["widget"])
|
||||
context["widget"]["subwidgets"] = subwidgets
|
||||
return context
|
|
@ -105,16 +105,6 @@ def init_connectors():
|
|||
)
|
||||
|
||||
|
||||
def init_federated_servers():
|
||||
"""big no to nazis"""
|
||||
built_in_blocks = ["gab.ai", "gab.com"]
|
||||
for server in built_in_blocks:
|
||||
models.FederatedServer.objects.create(
|
||||
server_name=server,
|
||||
status="blocked",
|
||||
)
|
||||
|
||||
|
||||
def init_settings():
|
||||
"""info about the instance"""
|
||||
models.SiteSettings.objects.create(
|
||||
|
@ -163,7 +153,6 @@ class Command(BaseCommand):
|
|||
"group",
|
||||
"permission",
|
||||
"connector",
|
||||
"federatedserver",
|
||||
"settings",
|
||||
"linkdomain",
|
||||
]
|
||||
|
@ -176,8 +165,6 @@ class Command(BaseCommand):
|
|||
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":
|
||||
|
|
30
bookwyrm/migrations/0146_auto_20220316_2352.py
Normal file
30
bookwyrm/migrations/0146_auto_20220316_2352.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Generated by Django 3.2.12 on 2022-03-16 23:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0145_sitesettings_version"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="inviterequest",
|
||||
name="answer",
|
||||
field=models.TextField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="sitesettings",
|
||||
name="invite_question_text",
|
||||
field=models.CharField(
|
||||
blank=True, default="What is your favourite book?", max_length=255
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="sitesettings",
|
||||
name="invite_request_question",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
38
bookwyrm/migrations/0147_alter_user_preferred_language.py
Normal file
38
bookwyrm/migrations/0147_alter_user_preferred_language.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# Generated by Django 3.2.12 on 2022-03-26 16:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0146_auto_20220316_2352"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="preferred_language",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("en-us", "English"),
|
||||
("de-de", "Deutsch (German)"),
|
||||
("es-es", "Español (Spanish)"),
|
||||
("gl-es", "Galego (Galician)"),
|
||||
("it-it", "Italiano (Italian)"),
|
||||
("fr-fr", "Français (French)"),
|
||||
("lt-lt", "Lietuvių (Lithuanian)"),
|
||||
("no-no", "Norsk (Norwegian)"),
|
||||
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
|
||||
("pt-pt", "Português Europeu (European Portuguese)"),
|
||||
("ro-ro", "Română (Romanian)"),
|
||||
("sv-se", "Svenska (Swedish)"),
|
||||
("zh-hans", "简体中文 (Simplified Chinese)"),
|
||||
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
||||
],
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
39
bookwyrm/migrations/0148_alter_user_preferred_language.py
Normal file
39
bookwyrm/migrations/0148_alter_user_preferred_language.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
# Generated by Django 3.2.12 on 2022-03-31 14:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0147_alter_user_preferred_language"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="preferred_language",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("en-us", "English"),
|
||||
("de-de", "Deutsch (German)"),
|
||||
("es-es", "Español (Spanish)"),
|
||||
("gl-es", "Galego (Galician)"),
|
||||
("it-it", "Italiano (Italian)"),
|
||||
("fi-fi", "Suomi (Finnish)"),
|
||||
("fr-fr", "Français (French)"),
|
||||
("lt-lt", "Lietuvių (Lithuanian)"),
|
||||
("no-no", "Norsk (Norwegian)"),
|
||||
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
|
||||
("pt-pt", "Português Europeu (European Portuguese)"),
|
||||
("ro-ro", "Română (Romanian)"),
|
||||
("sv-se", "Svenska (Swedish)"),
|
||||
("zh-hans", "简体中文 (Simplified Chinese)"),
|
||||
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
||||
],
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -125,7 +125,7 @@ class ActivitypubFieldMixin:
|
|||
"""model_field_name to activitypubFieldName"""
|
||||
if self.activitypub_field:
|
||||
return self.activitypub_field
|
||||
name = self.name.split(".")[-1]
|
||||
name = self.name.rsplit(".", maxsplit=1)[-1]
|
||||
components = name.split("_")
|
||||
return components[0] + "".join(x.title() for x in components[1:])
|
||||
|
||||
|
@ -389,7 +389,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
|
|||
self.alt_field = alt_field
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
# pylint: disable=arguments-differ,arguments-renamed
|
||||
def set_field_from_activity(self, instance, data, save=True, overwrite=True):
|
||||
"""helper function for assinging a value to the field"""
|
||||
value = getattr(data, self.get_activitypub_field())
|
||||
|
|
|
@ -39,15 +39,14 @@ class UserRelationship(BookWyrmModel):
|
|||
|
||||
def save(self, *args, **kwargs):
|
||||
"""clear the template cache"""
|
||||
# invalidate the template cache
|
||||
cache.delete_many(
|
||||
[
|
||||
f"relationship-{self.user_subject.id}-{self.user_object.id}",
|
||||
f"relationship-{self.user_object.id}-{self.user_subject.id}",
|
||||
]
|
||||
)
|
||||
clear_cache(self.user_subject, self.user_object)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""clear the template cache"""
|
||||
clear_cache(self.user_subject, self.user_object)
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
"""relationships should be unique"""
|
||||
|
||||
|
@ -90,7 +89,9 @@ class UserFollows(ActivityMixin, UserRelationship):
|
|||
user_object=self.user_subject,
|
||||
)
|
||||
).exists():
|
||||
raise IntegrityError()
|
||||
raise IntegrityError(
|
||||
"Attempting to follow blocked user", self.user_subject, self.user_object
|
||||
)
|
||||
# don't broadcast this type of relationship -- accepts and requests
|
||||
# are handled by the UserFollowRequest model
|
||||
super().save(*args, broadcast=False, **kwargs)
|
||||
|
@ -98,11 +99,12 @@ class UserFollows(ActivityMixin, UserRelationship):
|
|||
@classmethod
|
||||
def from_request(cls, follow_request):
|
||||
"""converts a follow request into a follow relationship"""
|
||||
return cls.objects.create(
|
||||
obj, _ = cls.objects.get_or_create(
|
||||
user_subject=follow_request.user_subject,
|
||||
user_object=follow_request.user_object,
|
||||
remote_id=follow_request.remote_id,
|
||||
)
|
||||
return obj
|
||||
|
||||
|
||||
class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||
|
@ -133,7 +135,9 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
|||
user_object=self.user_subject,
|
||||
)
|
||||
).exists():
|
||||
raise IntegrityError()
|
||||
raise IntegrityError(
|
||||
"Attempting to follow blocked user", self.user_subject, self.user_object
|
||||
)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
if broadcast and self.user_subject.local and not self.user_object.local:
|
||||
|
@ -174,7 +178,8 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
|||
|
||||
with transaction.atomic():
|
||||
UserFollows.from_request(self)
|
||||
self.delete()
|
||||
if self.id:
|
||||
self.delete()
|
||||
|
||||
def reject(self):
|
||||
"""generate a Reject for this follow request"""
|
||||
|
@ -207,3 +212,13 @@ class UserBlocks(ActivityMixin, UserRelationship):
|
|||
Q(user_subject=self.user_subject, user_object=self.user_object)
|
||||
| Q(user_subject=self.user_object, user_object=self.user_subject)
|
||||
).delete()
|
||||
|
||||
|
||||
def clear_cache(user_subject, user_object):
|
||||
"""clear relationship cache"""
|
||||
cache.delete_many(
|
||||
[
|
||||
f"relationship-{user_subject.id}-{user_object.id}",
|
||||
f"relationship-{user_object.id}-{user_subject.id}",
|
||||
]
|
||||
)
|
||||
|
|
|
@ -49,8 +49,12 @@ class SiteSettings(models.Model):
|
|||
# registration
|
||||
allow_registration = models.BooleanField(default=False)
|
||||
allow_invite_requests = models.BooleanField(default=True)
|
||||
invite_request_question = models.BooleanField(default=False)
|
||||
require_confirm_email = models.BooleanField(default=True)
|
||||
|
||||
invite_question_text = models.CharField(
|
||||
max_length=255, blank=True, default="What is your favourite book?"
|
||||
)
|
||||
# images
|
||||
logo = models.ImageField(upload_to="logos/", null=True, blank=True)
|
||||
logo_small = models.ImageField(upload_to="logos/", null=True, blank=True)
|
||||
|
@ -100,11 +104,14 @@ class SiteSettings(models.Model):
|
|||
return urljoin(STATIC_FULL_URL, default_path)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""if require_confirm_email is disabled, make sure no users are pending"""
|
||||
"""if require_confirm_email is disabled, make sure no users are pending,
|
||||
if enabled, make sure invite_question_text is not empty"""
|
||||
if not self.require_confirm_email:
|
||||
User.objects.filter(is_active=False, deactivation_reason="pending").update(
|
||||
is_active=True, deactivation_reason=None
|
||||
)
|
||||
if not self.invite_question_text:
|
||||
self.invite_question_text = "What is your favourite book?"
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
|
@ -150,6 +157,7 @@ class InviteRequest(BookWyrmModel):
|
|||
invite = models.ForeignKey(
|
||||
SiteInvite, on_delete=models.SET_NULL, null=True, blank=True
|
||||
)
|
||||
answer = models.TextField(max_length=50, unique=False, null=True, blank=True)
|
||||
invite_sent = models.BooleanField(default=False)
|
||||
ignored = models.BooleanField(default=False)
|
||||
|
||||
|
|
|
@ -284,11 +284,13 @@ LANGUAGES = [
|
|||
("es-es", _("Español (Spanish)")),
|
||||
("gl-es", _("Galego (Galician)")),
|
||||
("it-it", _("Italiano (Italian)")),
|
||||
("fi-fi", _("Suomi (Finnish)")),
|
||||
("fr-fr", _("Français (French)")),
|
||||
("lt-lt", _("Lietuvių (Lithuanian)")),
|
||||
("no-no", _("Norsk (Norwegian)")),
|
||||
("pt-br", _("Português do Brasil (Brazilian Portuguese)")),
|
||||
("pt-pt", _("Português Europeu (European Portuguese)")),
|
||||
("ro-ro", _("Română (Romanian)")),
|
||||
("sv-se", _("Svenska (Swedish)")),
|
||||
("zh-hans", _("简体中文 (Simplified Chinese)")),
|
||||
("zh-hant", _("繁體中文 (Traditional Chinese)")),
|
||||
|
|
|
@ -36,6 +36,18 @@ body {
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: $scrollbar-thumb;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: $scrollbar-track;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
margin: 0;
|
||||
|
@ -129,14 +141,6 @@ button:focus-visible .button-invisible-overlay {
|
|||
}
|
||||
|
||||
|
||||
|
||||
/** Tooltips
|
||||
******************************************************************************/
|
||||
|
||||
.tooltip {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/** States
|
||||
******************************************************************************/
|
||||
|
||||
|
|
|
@ -114,3 +114,17 @@ details[open] summary .details-close {
|
|||
padding-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/** Navbar details
|
||||
******************************************************************************/
|
||||
|
||||
#navbar-dropdown .navbar-item {
|
||||
color: $text;
|
||||
font-size: 0.875rem;
|
||||
padding: 0.375rem 3rem 0.375rem 1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#navbar-dropdown .navbar-item:hover {
|
||||
background-color: $background-secondary;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,9 @@ $primary: #005e50;
|
|||
$primary-light: #1d2b28;
|
||||
$info: #1f4666;
|
||||
$success: #246447;
|
||||
$success-light: #0d2f1e;
|
||||
$warning: #8b6c15;
|
||||
$warning-light: #372e13;
|
||||
$danger: #872538;
|
||||
$danger-light: #481922;
|
||||
$light: #393939;
|
||||
|
@ -26,6 +28,8 @@ $background-body: rgb(24, 27, 28);
|
|||
$background-secondary: rgb(28, 30, 32);
|
||||
$background-tertiary: rgb(32, 34, 36);
|
||||
$modal-background-background-color: rgba($black, 0.8);
|
||||
$scrollbar-track: $background-secondary;
|
||||
$scrollbar-thumb: $light;
|
||||
|
||||
/* highlight colors */
|
||||
$primary-highlight: $primary;
|
||||
|
|
|
@ -19,6 +19,8 @@ $scheme-main: $white-bis;
|
|||
$background-body: $white;
|
||||
$background-secondary: $white-ter;
|
||||
$background-tertiary: $white-bis;
|
||||
$scrollbar-track: $background-secondary;
|
||||
$scrollbar-thumb: $grey-lighter;
|
||||
|
||||
/* highlight colors */
|
||||
$primary-highlight: $primary-light;
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
<p>
|
||||
{% url "conduct" as coc_path %}
|
||||
{% blocktrans trimmed with site_name=site.name %}
|
||||
{{ site_name }}'s moderators and administrators keep the site up and running, enforce the <a href="coc_path">code of conduct</a>, and respond when users report spam and bad behavior.
|
||||
{{ site_name }}'s moderators and administrators keep the site up and running, enforce the <a href="{{ coc_path }}">code of conduct</a>, and respond when users report spam and bad behavior.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</header>
|
||||
|
|
|
@ -208,9 +208,17 @@
|
|||
{% endif %}
|
||||
|
||||
|
||||
{% if book.parent_work.editions.count > 1 %}
|
||||
<p>{% blocktrans with path=book.parent_work.local_path count=book.parent_work.editions.count %}<a href="{{ path }}/editions">{{ count }} editions</a>{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
{% with work=book.parent_work %}
|
||||
<p>
|
||||
<a href="{{ work.local_path }}/editions">
|
||||
{% blocktrans trimmed count counter=work.editions.count with count=work.editions.count|intcomma %}
|
||||
{{ count }} edition
|
||||
{% plural %}
|
||||
{{ count }} editions
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</p>
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{# user's relationship to the book #}
|
||||
|
|
|
@ -3,18 +3,24 @@
|
|||
{% load humanize %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block title %}{% if book %}{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}{% else %}{% trans "Add Book" %}{% endif %}{% endblock %}
|
||||
{% block title %}
|
||||
{% if book.title %}
|
||||
{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}
|
||||
{% else %}
|
||||
{% trans "Add Book" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<header class="block">
|
||||
<h1 class="title level-left">
|
||||
{% if book %}
|
||||
{% if book.title %}
|
||||
{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}
|
||||
{% else %}
|
||||
{% trans "Add Book" %}
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% if book %}
|
||||
{% if book.created_date %}
|
||||
<dl>
|
||||
<dt class="is-pulled-left mr-5 has-text-weight-semibold">{% trans "Added:" %}</dt>
|
||||
<dd class="ml-2">{{ book.created_date | naturaltime }}</dd>
|
||||
|
@ -33,7 +39,7 @@
|
|||
|
||||
<form
|
||||
class="block"
|
||||
{% if book %}
|
||||
{% if book.id %}
|
||||
name="edit-book"
|
||||
action="{{ book.local_path }}/{% if confirm_mode %}confirm{% else %}edit{% endif %}"
|
||||
{% else %}
|
||||
|
@ -97,7 +103,7 @@
|
|||
<input type="radio" name="parent_work" value="{{ match.parent_work.id }}"> {{ match.parent_work.title }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
<label>
|
||||
<label class="label mt-2">
|
||||
<input type="radio" name="parent_work" value="0" required> {% trans "This is a new work" %}
|
||||
</label>
|
||||
</fieldset>
|
||||
|
@ -119,7 +125,7 @@
|
|||
{% if not confirm_mode %}
|
||||
<div class="block">
|
||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||
{% if book %}
|
||||
{% if book.id %}
|
||||
<a class="button" href="{{ book.local_path }}">{% trans "Cancel" %}</a>
|
||||
{% else %}
|
||||
<a href="/" class="button" data-back>
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
{% csrf_token %}
|
||||
|
||||
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
||||
<input type="hidden" name="parent_work" value="{% firstof book.parent_work.id form.parent_work %}">
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<section class="block">
|
||||
|
@ -153,8 +155,7 @@
|
|||
<label class="label" for="id_first_published_date">
|
||||
{% trans "First published date:" %}
|
||||
</label>
|
||||
<input type="date" name="first_published_date" class="input" id="id_first_published_date"{% if form.first_published_date.value %} value="{{ form.first_published_date.value|date:'Y-m-d' }}"{% endif %} aria-describedby="desc_first_published_date">
|
||||
|
||||
{{ form.first_published_date }}
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.first_published_date.errors id="desc_first_published_date" %}
|
||||
</div>
|
||||
|
||||
|
@ -162,7 +163,7 @@
|
|||
<label class="label" for="id_published_date">
|
||||
{% trans "Published date:" %}
|
||||
</label>
|
||||
<input type="date" name="published_date" class="input" id="id_published_date"{% if form.published_date.value %} value="{{ form.published_date.value|date:'Y-m-d'}}"{% endif %} aria-describedby="desc_published_date">
|
||||
{{ form.published_date }}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.published_date.errors id="desc_published_date" %}
|
||||
</div>
|
||||
|
@ -175,6 +176,8 @@
|
|||
</h2>
|
||||
<div class="box">
|
||||
{% if book.authors.exists %}
|
||||
{# preserve authors if the book is unsaved #}
|
||||
<input type="hidden" name="authors" value="{% for author in book.authors.all %}{{ author.id }},{% endfor %}">
|
||||
<fieldset>
|
||||
{% for author in book.authors.all %}
|
||||
<div class="is-flex is-justify-content-space-between">
|
||||
|
@ -255,9 +258,7 @@
|
|||
<label class="label" for="id_physical_format">
|
||||
{% trans "Format:" %}
|
||||
</label>
|
||||
<div class="select">
|
||||
{{ form.physical_format }}
|
||||
</div>
|
||||
{{ form.physical_format }}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.physical_format.errors id="desc_physical_format" %}
|
||||
</div>
|
||||
|
|
|
@ -46,7 +46,36 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="block">
|
||||
{% include 'snippets/pagination.html' with page=editions path=request.path %}
|
||||
</div>
|
||||
|
||||
<div class="block has-text-centered help">
|
||||
<p>
|
||||
{% trans "Can't find the edition you're looking for?" %}
|
||||
</p>
|
||||
|
||||
<form action="{% url 'create-book-data' %}" method="POST" name="add-edition-form">
|
||||
{% csrf_token %}
|
||||
{{ work_form.title }}
|
||||
{{ work_form.subtitle }}
|
||||
{{ work_form.authors }}
|
||||
{{ work_form.description }}
|
||||
{{ work_form.languages }}
|
||||
{{ work_form.series }}
|
||||
{{ work_form.cover }}
|
||||
{{ work_form.first_published_date }}
|
||||
{% for subject in work.subjects %}
|
||||
<input type="hidden" name="subjects" value="{{ subject }}">
|
||||
{% endfor %}
|
||||
|
||||
<input type="hidden" name="parent_work" value="{{ work.id }}">
|
||||
<div>
|
||||
<button class="button is-small" type="submit">
|
||||
{% trans "Add another edition" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% trans "Help" as button_text %}
|
||||
{% include 'snippets/toggle/open_button.html' with text=button_text class="ml-3 is-rounded is-small has-background-body p-0 pb-1" icon="question-circle is-size-6" controls_text=controls_text controls_uid=controls_uid %}
|
||||
|
||||
<aside class="tooltip notification is-hidden transition-y is-pulled-left mb-2" id="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}">
|
||||
{% trans "Close" as button_text %}
|
||||
{% include 'snippets/toggle/close_button.html' with label=button_text class="delete" nonbutton=True controls_text=controls_text controls_uid=controls_uid %}
|
||||
|
||||
{% block tooltip_content %}{% endblock %}
|
||||
</aside>
|
|
@ -29,9 +29,16 @@
|
|||
</section>
|
||||
|
||||
<section class="block">
|
||||
{% trans "Can't find your code?" as button_text %}
|
||||
{% include "snippets/toggle/open_button.html" with text=button_text controls_text="resend_form" focus="resend_form_header" %}
|
||||
{% include "confirm_email/resend_form.html" with controls_text="resend_form" %}
|
||||
<form name="fallback" method="GET" action="{% url 'resend-link' %}" autocomplete="off">
|
||||
<button
|
||||
type="submit"
|
||||
class="button"
|
||||
data-modal-open="resend_form"
|
||||
>
|
||||
{% trans "Can't find your code?" %}
|
||||
</button>
|
||||
</form>
|
||||
{% include "confirm_email/resend_modal.html" with id="resend_form" %}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
|
10
bookwyrm/templates/confirm_email/resend.html
Normal file
10
bookwyrm/templates/confirm_email/resend.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
{% extends 'landing/layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}
|
||||
{% trans "Resend confirmation link" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "confirm_email/resend_modal.html" with active=True static=True id="resend-modal" %}
|
||||
{% endblock %}
|
|
@ -1,20 +0,0 @@
|
|||
{% extends "components/inline_form.html" %}
|
||||
{% load i18n %}
|
||||
{% block header %}
|
||||
{% trans "Resend confirmation link" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
<form name="resend" method="post" action="{% url 'resend-link' %}">
|
||||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label class="label" for="email">{% trans "Email address:" %}</label>
|
||||
<div class="control">
|
||||
<input type="text" name="email" class="input" required id="email">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-link">{% trans "Resend link" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
44
bookwyrm/templates/confirm_email/resend_modal.html
Normal file
44
bookwyrm/templates/confirm_email/resend_modal.html
Normal file
|
@ -0,0 +1,44 @@
|
|||
{% extends "components/modal.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal-title %}
|
||||
{% trans "Resend confirmation link" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-open %}
|
||||
<form name="resend" method="post" action="{% url 'resend-link' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label class="label" for="email">{% trans "Email address:" %}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
class="input"
|
||||
id="email"
|
||||
aria-described-by="id_email_errors"
|
||||
required
|
||||
>
|
||||
{% if error %}
|
||||
<div id="id_email_errors">
|
||||
<p class="help is-danger">
|
||||
{% trans "No user matching this email address found." %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<div class="control">
|
||||
<button class="button is-link">{% trans "Resend link" %}</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-close %}
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -43,7 +43,7 @@
|
|||
{% endif %}
|
||||
<p>
|
||||
<a href="https://joinbookwyrm.com/">
|
||||
{% trans "Join Bookwyrm" %}
|
||||
{% trans "Join BookWyrm" %}
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
|
|
@ -5,7 +5,19 @@
|
|||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Your Books" %}</h2>
|
||||
{% if not suggested_books %}
|
||||
<p>{% trans "There are no books here right now! Try searching for a book to get started" %}</p>
|
||||
|
||||
<div class="content">
|
||||
<p>{% trans "There are no books here right now! Try searching for a book to get started" %}</p>
|
||||
|
||||
<div class="box has-background-link-light">
|
||||
<p>{% trans "Do you have book data from another service like GoodReads?" %}</p>
|
||||
<a href="{% url 'import' %}">
|
||||
<span class="icon icon-list" aria-hidden="true"></span>
|
||||
{% trans "Import your reading history" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{% with active_book=request.GET.book %}
|
||||
<div class="tab-group">
|
||||
|
|
|
@ -14,28 +14,32 @@
|
|||
<div class="column is-half">
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-pulled-left" for="source">
|
||||
<label class="label" for="source">
|
||||
{% trans "Data source:" %}
|
||||
</label>
|
||||
{% include 'import/tooltip.html' with controls_text="goodreads-tooltip" %}
|
||||
|
||||
<div class="select">
|
||||
<select name="source" id="source" aria-describedby="desc_source">
|
||||
<option value="Goodreads" {% if current == 'Goodreads' %}selected{% endif %}>
|
||||
Goodreads (CSV)
|
||||
</option>
|
||||
<option value="Storygraph" {% if current == 'Storygraph' %}selected{% endif %}>
|
||||
Storygraph (CSV)
|
||||
</option>
|
||||
<option value="LibraryThing" {% if current == 'LibraryThing' %}selected{% endif %}>
|
||||
LibraryThing (TSV)
|
||||
</option>
|
||||
<option value="OpenLibrary" {% if current == 'OpenLibrary' %}selected{% endif %}>
|
||||
OpenLibrary (CSV)
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<p class="help" id="desc_source">
|
||||
{% trans 'You can download your Goodreads data from the <a href="https://www.goodreads.com/review/import" target="_blank" rel="noopener noreferrer">Import/Export page</a> of your Goodreads account.' %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="select block">
|
||||
<select name="source" id="source">
|
||||
<option value="Goodreads" {% if current == 'Goodreads' %}selected{% endif %}>
|
||||
Goodreads (CSV)
|
||||
</option>
|
||||
<option value="Storygraph" {% if current == 'Storygraph' %}selected{% endif %}>
|
||||
Storygraph (CSV)
|
||||
</option>
|
||||
<option value="LibraryThing" {% if current == 'LibraryThing' %}selected{% endif %}>
|
||||
LibraryThing (TSV)
|
||||
</option>
|
||||
<option value="OpenLibrary" {% if current == 'OpenLibrary' %}selected{% endif %}>
|
||||
OpenLibrary (CSV)
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_csv_file">{% trans "Data file:" %}</label>
|
||||
{{ import_form.csv_file }}
|
||||
|
@ -63,7 +67,7 @@
|
|||
<div class="content block">
|
||||
<h2 class="title">{% trans "Recent Imports" %}</h2>
|
||||
{% if not jobs %}
|
||||
<p>{% trans "No recent imports" %}</p>
|
||||
<p><em>{% trans "No recent imports" %}</em></p>
|
||||
{% endif %}
|
||||
<ul>
|
||||
{% for job in jobs %}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
{% extends 'components/tooltip.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block tooltip_content %}
|
||||
|
||||
{% trans 'You can download your Goodreads data from the <a href="https://www.goodreads.com/review/import" target="_blank" rel="noopener noreferrer">Import/Export page</a> of your Goodreads account.' %}
|
||||
|
||||
{% endblock %}
|
|
@ -70,6 +70,14 @@
|
|||
|
||||
{% include 'snippets/form_errors.html' with errors_list=request_form.email.errors id="desc_request_email" %}
|
||||
</div>
|
||||
{% if site.invite_request_question %}
|
||||
<div class="block">
|
||||
<label for="id_answer_register" class="label">{{ site.invite_question_text }}</label>
|
||||
<input type="answer" name="answer" maxlength="50" class="input" required="true" id="id_answer_register" aria-describedby="desc_answer_register">
|
||||
{% include 'snippets/form_errors.html' with errors_list=request_form.answer.errors id="desc_answer_register" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<button type="submit" class="button is-link">{% trans "Submit" %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
|
|
@ -90,64 +90,8 @@
|
|||
|
||||
<div class="navbar-end">
|
||||
{% if request.user.is_authenticated %}
|
||||
<div class="navbar-item mt-3 py-0 has-dropdown is-hoverable">
|
||||
<a
|
||||
href="{{ request.user.local_path }}"
|
||||
class="navbar-link pulldown-menu"
|
||||
role="button"
|
||||
aria-expanded="false"
|
||||
tabindex="0"
|
||||
aria-haspopup="true"
|
||||
aria-controls="navbar-dropdown"
|
||||
>
|
||||
{% include 'snippets/avatar.html' with user=request.user %}
|
||||
<span class="ml-2">{{ request.user.display_name }}</span>
|
||||
</a>
|
||||
<ul class="navbar-dropdown" id="navbar_dropdown">
|
||||
<li>
|
||||
<a href="{% url 'directory' %}" class="navbar-item">
|
||||
{% trans "Directory" %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'user-shelves' request.user.localname %}" class="navbar-item">
|
||||
{% trans 'Your Books' %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'direct-messages' %}" class="navbar-item">
|
||||
{% trans "Direct Messages" %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'prefs-profile' %}" class="navbar-item">
|
||||
{% trans 'Settings' %}
|
||||
</a>
|
||||
</li>
|
||||
{% if perms.bookwyrm.create_invites or perms.moderate_user %}
|
||||
<li class="navbar-divider" role="presentation"> </li>
|
||||
{% endif %}
|
||||
{% if perms.bookwyrm.create_invites and not site.allow_registration %}
|
||||
<li>
|
||||
<a href="{% url 'settings-invite-requests' %}" class="navbar-item">
|
||||
{% trans 'Invites' %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.bookwyrm.moderate_user %}
|
||||
<li>
|
||||
<a href="{% url 'settings-dashboard' %}" class="navbar-item">
|
||||
{% trans 'Admin' %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="navbar-divider" role="presentation"> </li>
|
||||
<li>
|
||||
<a href="{% url 'logout' %}" class="navbar-item">
|
||||
{% trans 'Log out' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="navbar-item mt-3 py-0">
|
||||
{% include 'user_menu.html' %}
|
||||
</div>
|
||||
<div class="navbar-item mt-3 py-0">
|
||||
<a href="{% url 'notifications' %}" class="tags has-addons">
|
||||
|
|
22
bookwyrm/templates/preferences/export.html
Normal file
22
bookwyrm/templates/preferences/export.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
{% extends 'preferences/layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "CSV Export" %}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
{% trans "CSV Export" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
<div class="block content">
|
||||
<p class="notification">
|
||||
{% trans "Your export will include all the books on your shelves, books you have reviewed, and books with reading activity." %}
|
||||
</p>
|
||||
<p>
|
||||
<a href="{% url 'prefs-export-file' %}" class="button">
|
||||
<span class="icon icon-download" aria-hidden="true"></span>
|
||||
<span>Download file</span>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -24,6 +24,17 @@
|
|||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Delete Account" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 class="menu-label">{% trans "Data" %}</h2>
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
{% url 'import' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Import" %}</a>
|
||||
</li>
|
||||
<li>
|
||||
{% url 'prefs-export' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "CSV export" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 class="menu-label">{% trans "Relationships" %}</h2>
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
|
|
|
@ -17,7 +17,14 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block modal-form-open %}
|
||||
<form name="add-readthrough-{{ readthrough.id }}" action="/create-readthrough" method="post">
|
||||
<form
|
||||
name="add-readthrough-{{ readthrough.id }}"
|
||||
{% if readthrough.id %}
|
||||
action="{% url 'edit-readthrough' %}"
|
||||
{% else %}
|
||||
action="{% url 'create-readthrough' %}"
|
||||
{% endif %}
|
||||
method="POST">
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
{% block panel %}
|
||||
<div class="block table-container">
|
||||
<table class="table is-striped">
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<tr>
|
||||
<th>
|
||||
{% url 'settings-announcements' as url %}
|
||||
|
|
|
@ -154,7 +154,7 @@
|
|||
</summary>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="table is-striped">
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<tr>
|
||||
<th>
|
||||
<label for="id_string_match">{% trans "String match" %}</label>
|
||||
|
|
|
@ -10,26 +10,26 @@
|
|||
{% block panel %}
|
||||
|
||||
<div class="columns block has-text-centered is-mobile is-multiline">
|
||||
<div class="column is-3-desktop is-6-mobile">
|
||||
<div class="notification">
|
||||
<div class="column is-3-desktop is-6-mobile is-flex">
|
||||
<div class="notification is-flex-grow-1">
|
||||
<h3>{% trans "Total users" %}</h3>
|
||||
<p class="title is-5">{{ users|intcomma }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3-desktop is-6-mobile">
|
||||
<div class="notification">
|
||||
<div class="column is-3-desktop is-6-mobil is-flexe">
|
||||
<div class="notification is-flex-grow-1">
|
||||
<h3>{% trans "Active this month" %}</h3>
|
||||
<p class="title is-5">{{ active_users|intcomma }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3-desktop is-6-mobile">
|
||||
<div class="notification">
|
||||
<div class="column is-3-desktop is-6-mobile is-flex">
|
||||
<div class="notification is-flex-grow-1">
|
||||
<h3>{% trans "Statuses" %}</h3>
|
||||
<p class="title is-5">{{ statuses|intcomma }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3-desktop is-6-mobile">
|
||||
<div class="notification">
|
||||
<div class="column is-3-desktop is-6-mobile is-flex">
|
||||
<div class="notification is-flex-grow-1">
|
||||
<h3>{% trans "Works" %}</h3>
|
||||
<p class="title is-5">{{ works|intcomma }}</p>
|
||||
</div>
|
||||
|
@ -38,8 +38,8 @@
|
|||
|
||||
<div class="columns block is-multiline">
|
||||
{% if reports %}
|
||||
<div class="column">
|
||||
<a href="{% url 'settings-reports' %}" class="notification is-warning is-block">
|
||||
<div class="column is-flex">
|
||||
<a href="{% url 'settings-reports' %}" class="notification is-warning is-block is-flex-grow-1">
|
||||
{% blocktrans trimmed count counter=reports with display_count=reports|intcomma %}
|
||||
{{ display_count }} open report
|
||||
{% plural %}
|
||||
|
@ -50,8 +50,8 @@
|
|||
{% endif %}
|
||||
|
||||
{% if pending_domains %}
|
||||
<div class="column">
|
||||
<a href="{% url 'settings-link-domain' %}" class="notification is-primary is-block">
|
||||
<div class="column is-flex">
|
||||
<a href="{% url 'settings-link-domain' %}" class="notification is-primary is-block is-flex-grow-1">
|
||||
{% blocktrans trimmed count counter=pending_domains with display_count=pending_domains|intcomma %}
|
||||
{{ display_count }} domain needs review
|
||||
{% plural %}
|
||||
|
@ -62,8 +62,8 @@
|
|||
{% endif %}
|
||||
|
||||
{% if not site.allow_registration and site.allow_invite_requests and invite_requests %}
|
||||
<div class="column">
|
||||
<a href="{% url 'settings-invite-requests' %}" class="notification is-block is-success">
|
||||
<div class="column is-flex">
|
||||
<a href="{% url 'settings-invite-requests' %}" class="notification is-block is-success is-flex-grow-1">
|
||||
{% blocktrans trimmed count counter=invite_requests with display_count=invite_requests|intcomma %}
|
||||
{{ display_count }} invite request
|
||||
{% plural %}
|
||||
|
@ -74,8 +74,8 @@
|
|||
{% endif %}
|
||||
|
||||
{% if current_version %}
|
||||
<div class="column">
|
||||
<a href="https://docs.joinbookwyrm.com/updating-your-instance.html" class="notification is-block is-warning" target="_blank">
|
||||
<div class="column is-flex">
|
||||
<a href="https://docs.joinbookwyrm.com/updating-your-instance.html" class="notification is-block is-warning is-flex-grow-1" target="_blank">
|
||||
{% blocktrans trimmed with current=current_version available=available_version %}
|
||||
An update is available! You're running v{{ current }} and the latest release is {{ available }}.
|
||||
{% endblocktrans %}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
||||
|
||||
{% block filter_fields %}
|
||||
{% include 'settings/federation/software_filter.html' %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -12,6 +12,9 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
|
||||
{% include 'settings/federation/instance_filters.html' %}
|
||||
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
{% url 'settings-federation' status='federated' as url %}
|
||||
|
@ -25,7 +28,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<table class="table is-striped">
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<tr>
|
||||
{% url 'settings-federation' as url %}
|
||||
<th>
|
||||
|
@ -36,6 +39,10 @@
|
|||
{% trans "Date added" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Last updated" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="updated_date" sort=sort text=text %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Software" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="application_type" sort=sort text=text %}
|
||||
|
@ -43,12 +50,12 @@
|
|||
<th>
|
||||
{% trans "Users" %}
|
||||
</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
</tr>
|
||||
{% for server in servers %}
|
||||
<tr>
|
||||
<td><a href="{% url 'settings-federated-server' server.id %}">{{ server.server_name }}</a></td>
|
||||
<td>{{ server.created_date }}</td>
|
||||
<td>{{ server.created_date|date:'Y-m-d' }}</td>
|
||||
<td>{{ server.updated_date|date:'Y-m-d' }}</td>
|
||||
<td>
|
||||
{% if server.application_type %}
|
||||
{{ server.application_type }}
|
||||
|
@ -56,7 +63,6 @@
|
|||
{% endif %}
|
||||
</td>
|
||||
<td>{{ server.user_set.count }}</td>
|
||||
<td>{{ server.get_status_display }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not servers %}
|
||||
|
|
19
bookwyrm/templates/settings/federation/software_filter.html
Normal file
19
bookwyrm/templates/settings/federation/software_filter.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
{% extends 'snippets/filters_panel/filter_field.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block filter %}
|
||||
<label class="label" for="id_server">{% trans "Software" %}</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select name="application_type">
|
||||
<option value="">-----</option>
|
||||
{% for option in software_options %}
|
||||
{% if option %}
|
||||
<option value="{{ option }}">{{ option }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -40,6 +40,9 @@
|
|||
{% include 'snippets/table-sort-header.html' with field="invite__invitees__created_date" sort=sort text=text %}
|
||||
</th>
|
||||
<th>{% trans "Email" %}</th>
|
||||
{% if site.invite_request_question %}
|
||||
<th>{% trans "Answer" %}</th>
|
||||
{% endif %}
|
||||
<th>
|
||||
{% trans "Status" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="invite__times_used" sort=sort text=text %}
|
||||
|
@ -54,6 +57,9 @@
|
|||
<td>{{ req.created_date | naturaltime }}</td>
|
||||
<td>{{ req.invite.invitees.first.created_date | naturaltime }}</td>
|
||||
<td>{{ req.email }}</td>
|
||||
{% if site.invite_request_question %}
|
||||
<td>{{ req.answer }}</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
{% if req.invite.times_used %}
|
||||
{% trans "Accepted" %}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
<div class="field">
|
||||
<input type="text" name="address" maxlength="255" class="input" required="" id="id_address" placeholder="190.0.2.0/24" aria-describedby="desc_address">
|
||||
<p class="help">{% trans "You can block IP ranges using CIDR syntax." %}</p>
|
||||
</div>
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.address.errors id="desc_address" %}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
{% extends 'components/tooltip.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block tooltip_content %}
|
||||
|
||||
{% trans "You can block IP ranges using CIDR syntax." %}
|
||||
|
||||
{% endblock %}
|
|
@ -93,7 +93,7 @@
|
|||
</ul>
|
||||
{% endif %}
|
||||
</nav>
|
||||
<div class="column">
|
||||
<div class="column is-clipped">
|
||||
{% block panel %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -44,5 +44,6 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% include 'snippets/pagination.html' with page=reports path=request.path %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -139,12 +139,6 @@
|
|||
{% trans "Allow registration" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_allow_invite_requests">
|
||||
{{ site_form.allow_invite_requests }}
|
||||
{% trans "Allow invite requests" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label mb-0" for="id_require_confirm_email">
|
||||
{{ site_form.require_confirm_email }}
|
||||
|
@ -152,6 +146,24 @@
|
|||
</label>
|
||||
<p class="help" id="desc_require_confirm_email">{% trans "(Recommended if registration is open)" %}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_allow_invite_requests">
|
||||
{{ site_form.allow_invite_requests }}
|
||||
{% trans "Allow invite requests" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_invite_requests_question">
|
||||
{{ site_form.invite_request_question }}
|
||||
{% trans "Set a question for invite requests" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_invite_question_text">
|
||||
{% trans "Question:" %}
|
||||
{{ site_form.invite_question_text }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
|
||||
{{ site_form.registration_closed_text }}
|
||||
|
@ -159,7 +171,7 @@
|
|||
<div class="field">
|
||||
<label class="label" for="id_invite_request_text">{% trans "Invite request text:" %}</label>
|
||||
{{ site_form.invite_request_text }}
|
||||
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=site_form.invite_request_text.errors id="desc_invite_request_text" %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -88,7 +88,7 @@
|
|||
<section class="block content">
|
||||
<h2 class="title is-4">{% trans "Available Themes" %}</h2>
|
||||
<div class="table-container">
|
||||
<table class="table is-striped">
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<tr>
|
||||
<th>
|
||||
{% trans "Theme name" %}
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
|
||||
<div class="table-container block">
|
||||
<table class="table is-striped">
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<tr>
|
||||
{% url 'settings-users' as url %}
|
||||
<th>
|
||||
|
@ -61,10 +61,25 @@
|
|||
</tr>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td><a href="{% url 'settings-user' user.id %}">{{ user|username }}</a></td>
|
||||
<td class="overflow-wrap-anywhere">
|
||||
<a href="{% url 'settings-user' user.id %}">{{ user|username }}</a>
|
||||
</td>
|
||||
<td>{{ user.created_date }}</td>
|
||||
<td>{{ user.last_active_date }}</td>
|
||||
<td>{% if user.is_active %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %}</td>
|
||||
<td>
|
||||
{% if user.is_active %}
|
||||
<span class="tag is-success" aria-hidden="true">
|
||||
<span class="icon icon-check"></span>
|
||||
</span>
|
||||
{% trans "Active" %}
|
||||
{% else %}
|
||||
<span class="tag is-warning" aria-hidden="true">
|
||||
<span class="icon icon-x"></span>
|
||||
</span>
|
||||
{% trans "Inactive" %}
|
||||
<span class="help">({{ user.get_deactivation_reason_display }})</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if status != "local" %}
|
||||
<td>
|
||||
{% if user.federated_server %}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<div class="column is-flex is-flex-direction-column">
|
||||
<h4 class="title is-4">{% trans "Profile" %}</h4>
|
||||
<div class="box is-flex-grow-1">
|
||||
{% include 'user/user_preview.html' with user=user %}
|
||||
{% include 'user/user_preview.html' with user=user admin_mode=True %}
|
||||
{% if user.summary %}
|
||||
<div class="box content has-background-secondary is-shadowless">
|
||||
{{ user.summary|to_markdown|safe }}
|
||||
|
@ -14,6 +14,10 @@
|
|||
{% endif %}
|
||||
|
||||
<p class="mt-2"><a href="{{ user.local_path }}">{% trans "View user profile" %}</a></p>
|
||||
{% url 'settings-user' user.id as url %}
|
||||
{% if not request.path == url %}
|
||||
<p class="mt-2"><a href="{{ url }}">{% trans "Go to user admin" %}</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-flex is-flex-direction-column is-4">
|
||||
|
@ -67,6 +71,9 @@
|
|||
<dt class="is-pulled-left mr-5">{% trans "Blocked by count:" %}</dt>
|
||||
<dd>{{ user.blocked_by.count }}</dd>
|
||||
|
||||
<dt class="is-pulled-left mr-5">{% trans "Date added:" %}</dt>
|
||||
<dd>{{ user.created_date }}</dd>
|
||||
|
||||
<dt class="is-pulled-left mr-5">{% trans "Last active date:" %}</dt>
|
||||
<dd>{{ user.last_active_date }}</dd>
|
||||
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
{% blocktrans with username=goal.user.display_name read_count=progress.count|intcomma goal_count=goal.goal|intcomma path=goal.local_path %}{{ username }} has read <a href="{{ path }}">{{ read_count }} of {{ goal_count}} books</a>.{% endblocktrans %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<progress class="progress is-large" value="{{ progress.count }}" max="{{ goal.goal }}" aria-hidden="true">{{ progress.percent }}%</progress>
|
||||
<progress
|
||||
class="progress is-large is-primary"
|
||||
value="{{ progress.count }}"
|
||||
max="{{ goal.goal }}"
|
||||
aria-hidden="true"
|
||||
>{{ progress.percent }}%</progress>
|
||||
|
||||
{% endwith %}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
<form name="reading-progress-{{ uuid }}" action="{% url 'reading-status-update' book.id %}" method="POST" class="submit-status">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="id" value="{{ readthrough.id }}">
|
||||
<input type="hidden" name="start_date" value="{{ readthrough.start_date|date:'Y-m-d' }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block reading-dates %}
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
<article class="column ml-3-tablet my-3-mobile">
|
||||
<article class="column ml-3-tablet my-3-mobile is-clipped">
|
||||
{% if status_type == 'Review' %}
|
||||
<header class="mb-2">
|
||||
<h3
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<div class="card-footer-item">
|
||||
{% trans "Reply" as button_text %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with controls_text="show_comment" controls_uid=status.id text=button_text icon_with_text="comment" class="is-small is-light is-transparent toggle-button" focus="id_content_reply" %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with controls_text="show_comment" controls_uid=status.id text=button_text icon_with_text="comment" class="is-small is-light toggle-button" focus="id_content_reply" %}
|
||||
</div>
|
||||
<div class="card-footer-item">
|
||||
{% include 'snippets/boost_button.html' with status=status %}
|
||||
|
@ -42,7 +42,7 @@
|
|||
</div>
|
||||
{% if not moderation_mode %}
|
||||
<div class="card-footer-item">
|
||||
{% include 'snippets/status/status_options.html' with class="is-small is-light is-transparent" right=True %}
|
||||
{% include 'snippets/status/status_options.html' with class="is-small is-light" right=True %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<p><a href="{{ user.remote_id }}">{{ user.username }}</a></p>
|
||||
<p>{% blocktrans with date=user.created_date|naturaltime %}Joined {{ date }}{% endblocktrans %}</p>
|
||||
<p>
|
||||
{% if request.user.id == user.id %}
|
||||
{% if request.user.id == user.id or admin_mode %}
|
||||
|
||||
<a href="{% url 'user-followers' user|username %}">{% blocktrans count counter=user.followers.count %}{{ counter }} follower{% plural %}{{ counter }} followers{% endblocktrans %}</a>,
|
||||
<a href="{% url 'user-following' user|username %}">{% blocktrans with counter=user.following.count %}{{ counter }} following{% endblocktrans %}</a>
|
||||
|
|
77
bookwyrm/templates/user_menu.html
Normal file
77
bookwyrm/templates/user_menu.html
Normal file
|
@ -0,0 +1,77 @@
|
|||
{% load utilities %}
|
||||
{% load i18n %}
|
||||
|
||||
<details class="dropdown" id="navbar-dropdown">
|
||||
<summary
|
||||
class="is-relative pulldown-menu dropdown-trigger"
|
||||
aria-label="{% trans 'View profile and more' %}"
|
||||
role="button"
|
||||
aria-haspopup="menu"
|
||||
>
|
||||
<span class="">
|
||||
{% include 'snippets/avatar.html' with user=request.user %}
|
||||
<span class="ml-2">{{ request.user.display_name }}</span>
|
||||
</span>
|
||||
<span class="icon icon-arrow-down is-hidden-mobile" aria-hidden="true"></span>
|
||||
</summary>
|
||||
|
||||
<div class="dropdown-menu">
|
||||
<ul
|
||||
class="dropdown-content"
|
||||
role="menu"
|
||||
>
|
||||
<li role="menuitem">
|
||||
<a href="{% url 'user-feed' user|username %}" class="navbar-item">
|
||||
{% trans "Profile" %}
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<a href="{% url 'directory' %}" class="navbar-item">
|
||||
{% trans "Directory" %}
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<a href="{% url 'user-shelves' request.user.localname %}" class="navbar-item">
|
||||
{% trans 'Your Books' %}
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<a href="{% url 'direct-messages' %}" class="navbar-item">
|
||||
{% trans "Direct Messages" %}
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<a href="{% url 'prefs-profile' %}" class="navbar-item">
|
||||
{% trans 'Settings' %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if perms.bookwyrm.create_invites or perms.moderate_user %}
|
||||
<li class="navbar-divider" role="presentation" aria-hidden="true"> </li>
|
||||
{% endif %}
|
||||
|
||||
{% if perms.bookwyrm.create_invites and not site.allow_registration %}
|
||||
<li role="menuitem">
|
||||
<a href="{% url 'settings-invite-requests' %}" class="navbar-item">
|
||||
{% trans 'Invites' %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.bookwyrm.moderate_user %}
|
||||
<li role="menuitem">
|
||||
<a href="{% url 'settings-dashboard' %}" class="navbar-item">
|
||||
{% trans 'Admin' %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="navbar-divider" role="presentation" aria-hidden="true"> </li>
|
||||
|
||||
<li role="menuitem">
|
||||
<a href="{% url 'logout' %}" class="navbar-item">
|
||||
{% trans 'Log out' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
9
bookwyrm/templates/widgets/addon_multiwidget.html
Normal file
9
bookwyrm/templates/widgets/addon_multiwidget.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
{% spaceless %}
|
||||
<div class="field has-addons">
|
||||
{% for widget in widget.subwidgets %}
|
||||
<div class="control{% if forloop.last %} is-expanded{% endif %}">
|
||||
{% include widget.template_name %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endspaceless %}
|
10
bookwyrm/templates/widgets/select.html
Normal file
10
bookwyrm/templates/widgets/select.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
<div class="select">
|
||||
<select
|
||||
name="{{ widget.name }}"
|
||||
{% include "django/forms/widgets/attrs.html" %}
|
||||
>{% for group_name, group_choices, group_index in widget.optgroups %}{% if group_name %}
|
||||
<optgroup label="{{ group_name }}">{% endif %}{% for option in group_choices %}
|
||||
{% include option.template_name with widget=option %}{% endfor %}{% if group_name %}
|
||||
</optgroup>{% endif %}{% endfor %}
|
||||
</select>
|
||||
</div>
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
""" import ALL the tests """
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -4,7 +4,10 @@ from bookwyrm import models
|
|||
|
||||
|
||||
class Author(TestCase):
|
||||
"""serialize author tests"""
|
||||
|
||||
def setUp(self):
|
||||
"""initial data"""
|
||||
self.book = models.Edition.objects.create(
|
||||
title="Example Edition",
|
||||
remote_id="https://example.com/book/1",
|
||||
|
@ -16,6 +19,7 @@ class Author(TestCase):
|
|||
)
|
||||
|
||||
def test_serialize_model(self):
|
||||
"""check presense of author fields"""
|
||||
activity = self.author.to_activity()
|
||||
self.assertEqual(activity["id"], self.author.remote_id)
|
||||
self.assertIsInstance(activity["aliases"], list)
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -90,7 +90,6 @@ class InitDB(TestCase):
|
|||
self.assertEqual(Group.objects.count(), 3)
|
||||
self.assertTrue(Permission.objects.exists())
|
||||
self.assertEqual(models.Connector.objects.count(), 3)
|
||||
self.assertEqual(models.FederatedServer.objects.count(), 2)
|
||||
self.assertEqual(models.SiteSettings.objects.count(), 1)
|
||||
self.assertEqual(models.LinkDomain.objects.count(), 5)
|
||||
|
||||
|
@ -102,7 +101,6 @@ class InitDB(TestCase):
|
|||
# everything should have been called
|
||||
self.assertEqual(Group.objects.count(), 3)
|
||||
self.assertEqual(models.Connector.objects.count(), 0)
|
||||
self.assertEqual(models.FederatedServer.objects.count(), 0)
|
||||
self.assertEqual(models.SiteSettings.objects.count(), 0)
|
||||
self.assertEqual(models.LinkDomain.objects.count(), 0)
|
||||
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -38,7 +38,7 @@ class BaseModel(TestCase):
|
|||
|
||||
def test_remote_id(self):
|
||||
"""these should be generated"""
|
||||
self.test_model.id = 1
|
||||
self.test_model.id = 1 # pylint: disable=invalid-name
|
||||
expected = self.test_model.get_remote_id()
|
||||
self.assertEqual(expected, f"https://{DOMAIN}/bookwyrmtestmodel/1")
|
||||
|
||||
|
|
|
@ -162,6 +162,7 @@ class ModelFields(TestCase):
|
|||
class TestActivity(ActivityObject):
|
||||
"""real simple mock"""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
to: List[str]
|
||||
cc: List[str]
|
||||
id: str = "http://hi.com"
|
||||
|
|
|
@ -17,7 +17,7 @@ class User(TestCase):
|
|||
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
|
||||
self.user = models.User.objects.create_user(
|
||||
"mouse@%s" % DOMAIN,
|
||||
f"mouse@{DOMAIN}",
|
||||
"mouse@mouse.mouse",
|
||||
"mouseword",
|
||||
local=True,
|
||||
|
@ -107,7 +107,7 @@ class User(TestCase):
|
|||
def test_get_or_create_remote_server(self):
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://%s/.well-known/nodeinfo" % DOMAIN,
|
||||
f"https://{DOMAIN}/.well-known/nodeinfo",
|
||||
json={"links": [{"href": "http://www.example.com"}, {}]},
|
||||
)
|
||||
responses.add(
|
||||
|
@ -124,7 +124,7 @@ class User(TestCase):
|
|||
@responses.activate
|
||||
def test_get_or_create_remote_server_no_wellknown(self):
|
||||
responses.add(
|
||||
responses.GET, "https://%s/.well-known/nodeinfo" % DOMAIN, status=404
|
||||
responses.GET, f"https://{DOMAIN}/.well-known/nodeinfo", status=404
|
||||
)
|
||||
|
||||
server = models.user.get_or_create_remote_server(DOMAIN)
|
||||
|
@ -136,7 +136,7 @@ class User(TestCase):
|
|||
def test_get_or_create_remote_server_no_links(self):
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://%s/.well-known/nodeinfo" % DOMAIN,
|
||||
f"https://{DOMAIN}/.well-known/nodeinfo",
|
||||
json={"links": [{"href": "http://www.example.com"}, {}]},
|
||||
)
|
||||
responses.add(responses.GET, "http://www.example.com", status=404)
|
||||
|
@ -150,7 +150,7 @@ class User(TestCase):
|
|||
def test_get_or_create_remote_server_unknown_format(self):
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://%s/.well-known/nodeinfo" % DOMAIN,
|
||||
f"https://{DOMAIN}/.well-known/nodeinfo",
|
||||
json={"links": [{"href": "http://www.example.com"}, {}]},
|
||||
)
|
||||
responses.add(responses.GET, "http://www.example.com", json={"fish": "salmon"})
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -64,8 +64,8 @@ class Signature(TestCase):
|
|||
|
||||
def send(self, signature, now, data, digest):
|
||||
"""test request"""
|
||||
c = Client()
|
||||
return c.post(
|
||||
client = Client()
|
||||
return client.post(
|
||||
urlsplit(self.rat.inbox).path,
|
||||
data=data,
|
||||
content_type="application/json",
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -60,7 +60,7 @@ class EditBookViews(TestCase):
|
|||
|
||||
def test_edit_book_create_page(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.EditBook.as_view()
|
||||
view = views.CreateBook.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
request.user.is_superuser = True
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -61,7 +61,7 @@ class InboxActivities(TestCase):
|
|||
self.assertEqual(models.Notification.objects.count(), 0)
|
||||
activity = {
|
||||
"type": "Announce",
|
||||
"id": "%s/boost" % self.status.remote_id,
|
||||
"id": f"{self.status.remote_id}/boost",
|
||||
"actor": self.remote_user.remote_id,
|
||||
"object": self.status.remote_id,
|
||||
"to": ["https://www.w3.org/ns/activitystreams#public"],
|
||||
|
@ -94,7 +94,7 @@ class InboxActivities(TestCase):
|
|||
self.assertEqual(models.Notification.objects.count(), 0)
|
||||
activity = {
|
||||
"type": "Announce",
|
||||
"id": "%s/boost" % self.status.remote_id,
|
||||
"id": f"{self.status.remote_id}/boost",
|
||||
"actor": self.remote_user.remote_id,
|
||||
"object": "https://remote.com/status/1",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#public"],
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -360,10 +360,17 @@ class RegisterViews(TestCase):
|
|||
result = view(request)
|
||||
validate_html(result.render())
|
||||
|
||||
def test_resend_link(self, *_):
|
||||
def test_resend_link_get(self, *_):
|
||||
"""try again"""
|
||||
request = self.factory.get("")
|
||||
request.user = self.anonymous_user
|
||||
result = views.ResendConfirmEmail.as_view()(request)
|
||||
validate_html(result.render())
|
||||
|
||||
def test_resend_link_post(self, *_):
|
||||
"""try again"""
|
||||
request = self.factory.post("", {"email": "mouse@mouse.com"})
|
||||
request.user = self.anonymous_user
|
||||
with patch("bookwyrm.emailing.send_email.delay") as mock:
|
||||
views.resend_link(request)
|
||||
views.ResendConfirmEmail.as_view()(request)
|
||||
self.assertEqual(mock.call_count, 1)
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -66,7 +66,7 @@ class AuthorViews(TestCase):
|
|||
def test_author_page_edition_author(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Author.as_view()
|
||||
another_book = models.Edition.objects.create(
|
||||
models.Edition.objects.create(
|
||||
title="Example Edition",
|
||||
remote_id="https://example.com/book/1",
|
||||
parent_work=self.work,
|
||||
|
|
69
bookwyrm/tests/views/test_export.py
Normal file
69
bookwyrm/tests/views/test_export.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.http import StreamingHttpResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
|
||||
@patch("bookwyrm.activitystreams.add_status_task.delay")
|
||||
@patch("bookwyrm.activitystreams.populate_stream_task.delay")
|
||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
|
||||
class ExportViews(TestCase):
|
||||
"""viewing and creating statuses"""
|
||||
|
||||
def setUp(self):
|
||||
"""we need basic test data and mocks"""
|
||||
self.factory = RequestFactory()
|
||||
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||
):
|
||||
self.local_user = models.User.objects.create_user(
|
||||
"mouse@local.com",
|
||||
"mouse@mouse.com",
|
||||
"mouseword",
|
||||
local=True,
|
||||
localname="mouse",
|
||||
remote_id="https://example.com/users/mouse",
|
||||
)
|
||||
self.work = models.Work.objects.create(title="Test Work")
|
||||
self.book = models.Edition.objects.create(
|
||||
title="Test Book",
|
||||
remote_id="https://example.com/book/1",
|
||||
parent_work=self.work,
|
||||
isbn_13="9781234567890",
|
||||
bnf_id="beep",
|
||||
)
|
||||
|
||||
def tst_export_get(self, *_):
|
||||
"""request export"""
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
result = views.Export.as_view()(request)
|
||||
validate_html(result.render())
|
||||
|
||||
def test_export_file(self, *_):
|
||||
"""simple export"""
|
||||
models.ShelfBook.objects.create(
|
||||
shelf=self.local_user.shelf_set.first(),
|
||||
user=self.local_user,
|
||||
book=self.book,
|
||||
)
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
export = views.export_user_book_data(request)
|
||||
self.assertIsInstance(export, StreamingHttpResponse)
|
||||
self.assertEqual(export.status_code, 200)
|
||||
result = list(export.streaming_content)
|
||||
# pylint: disable=line-too-long
|
||||
self.assertEqual(
|
||||
result[0],
|
||||
b"title,author_text,remote_id,openlibrary_key,inventaire_id,librarything_key,goodreads_key,bnf_id,viaf,wikidata,asin,isbn_10,isbn_13,oclc_number,rating,review_name,review_cw,review_content\r\n",
|
||||
)
|
||||
expected = f"Test Book,,{self.book.remote_id},,,,,beep,,,,123456789X,9781234567890,,,,,\r\n"
|
||||
self.assertEqual(result[1].decode("utf-8"), expected)
|
|
@ -139,7 +139,7 @@ class ViewsHelpers(TestCase):
|
|||
}
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://example.com/.well-known/webfinger?resource=acct:%s" % username,
|
||||
f"https://example.com/.well-known/webfinger?resource=acct:{username}",
|
||||
json=wellknown,
|
||||
status=200,
|
||||
)
|
||||
|
|
|
@ -83,7 +83,7 @@ class UserViews(TestCase):
|
|||
def test_user_page_domain(self):
|
||||
"""when the user domain has dashes in it"""
|
||||
with patch("bookwyrm.models.user.set_remote_server"):
|
||||
self.remote_user = models.User.objects.create_user(
|
||||
models.User.objects.create_user(
|
||||
"nutria",
|
||||
"",
|
||||
"nutriaword",
|
||||
|
|
|
@ -71,7 +71,7 @@ urlpatterns = [
|
|||
views.ConfirmEmailCode.as_view(),
|
||||
name="confirm-email-code",
|
||||
),
|
||||
re_path(r"^resend-link/?$", views.resend_link, name="resend-link"),
|
||||
re_path(r"^resend-link/?$", views.ResendConfirmEmail.as_view(), name="resend-link"),
|
||||
re_path(r"^logout/?$", views.Logout.as_view(), name="logout"),
|
||||
re_path(
|
||||
r"^password-reset/?$",
|
||||
|
@ -481,6 +481,12 @@ urlpatterns = [
|
|||
views.ChangePassword.as_view(),
|
||||
name="prefs-password",
|
||||
),
|
||||
re_path(r"^preferences/export/?$", views.Export.as_view(), name="prefs-export"),
|
||||
re_path(
|
||||
r"^preferences/export/file/?$",
|
||||
views.export_user_book_data,
|
||||
name="prefs-export-file",
|
||||
),
|
||||
re_path(r"^preferences/delete/?$", views.DeleteUser.as_view(), name="prefs-delete"),
|
||||
re_path(r"^preferences/block/?$", views.Block.as_view(), name="prefs-block"),
|
||||
re_path(r"^block/(?P<user_id>\d+)/?$", views.Block.as_view()),
|
||||
|
@ -532,7 +538,10 @@ urlpatterns = [
|
|||
),
|
||||
re_path(rf"{BOOK_PATH}/edit/?$", views.EditBook.as_view(), name="edit-book"),
|
||||
re_path(rf"{BOOK_PATH}/confirm/?$", views.ConfirmEditBook.as_view()),
|
||||
re_path(r"^create-book/?$", views.EditBook.as_view(), name="create-book"),
|
||||
re_path(
|
||||
r"^create-book/data/?$", views.create_book_from_data, name="create-book-data"
|
||||
),
|
||||
re_path(r"^create-book/?$", views.CreateBook.as_view(), name="create-book"),
|
||||
re_path(r"^create-book/confirm/?$", views.ConfirmEditBook.as_view()),
|
||||
re_path(rf"{BOOK_PATH}/editions(.json)?/?$", views.Editions.as_view()),
|
||||
re_path(
|
||||
|
|
|
@ -28,6 +28,7 @@ from .admin.user_admin import UserAdmin, UserAdminList
|
|||
# user preferences
|
||||
from .preferences.change_password import ChangePassword
|
||||
from .preferences.edit_user import EditUser
|
||||
from .preferences.export import Export, export_user_book_data
|
||||
from .preferences.delete_user import DeleteUser
|
||||
from .preferences.block import Block, unblock
|
||||
|
||||
|
@ -39,7 +40,12 @@ from .books.books import (
|
|||
resolve_book,
|
||||
)
|
||||
from .books.books import update_book_from_remote
|
||||
from .books.edit_book import EditBook, ConfirmEditBook
|
||||
from .books.edit_book import (
|
||||
EditBook,
|
||||
ConfirmEditBook,
|
||||
CreateBook,
|
||||
create_book_from_data,
|
||||
)
|
||||
from .books.editions import Editions, switch_edition
|
||||
from .books.links import BookFileLinks, AddFileLink, delete_link
|
||||
|
||||
|
@ -47,7 +53,8 @@ from .books.links import BookFileLinks, AddFileLink, delete_link
|
|||
from .landing.about import about, privacy, conduct
|
||||
from .landing.landing import Home, Landing
|
||||
from .landing.login import Login, Logout
|
||||
from .landing.register import Register, ConfirmEmail, ConfirmEmailCode, resend_link
|
||||
from .landing.register import Register
|
||||
from .landing.register import ConfirmEmail, ConfirmEmailCode, ResendConfirmEmail
|
||||
from .landing.password import PasswordResetRequest, PasswordReset
|
||||
|
||||
# shelves
|
||||
|
|
|
@ -25,14 +25,23 @@ class Federation(View):
|
|||
|
||||
def get(self, request, status="federated"):
|
||||
"""list of servers"""
|
||||
servers = models.FederatedServer.objects.filter(status=status)
|
||||
|
||||
filters = {}
|
||||
if software := request.GET.get("application_type"):
|
||||
filters["application_type"] = software
|
||||
|
||||
servers = models.FederatedServer.objects.filter(status=status, **filters)
|
||||
|
||||
sort = request.GET.get("sort")
|
||||
sort_fields = ["created_date", "application_type", "server_name"]
|
||||
# pylint: disable=consider-using-f-string
|
||||
if not sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
|
||||
sort_fields = [
|
||||
"created_date",
|
||||
"updated_date",
|
||||
"application_type",
|
||||
"server_name",
|
||||
]
|
||||
if not sort in sort_fields + [f"-{f}" for f in sort_fields]:
|
||||
sort = "-created_date"
|
||||
servers = servers.order_by(sort)
|
||||
servers = servers.order_by(sort, "-created_date")
|
||||
|
||||
paginated = Paginator(servers, PAGE_LENGTH)
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
|
@ -49,6 +58,9 @@ class Federation(View):
|
|||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
"sort": sort,
|
||||
"software_options": models.FederatedServer.objects.values_list(
|
||||
"application_type", flat=True
|
||||
).distinct(),
|
||||
"form": forms.ServerForm(),
|
||||
}
|
||||
return TemplateResponse(request, "settings/federation/instance_list.html", data)
|
||||
|
|
|
@ -96,6 +96,7 @@ class ManageInviteRequests(View):
|
|||
"created_date",
|
||||
"invite__times_used",
|
||||
"invite__invitees__created_date",
|
||||
"answer",
|
||||
]
|
||||
# pylint: disable=consider-using-f-string
|
||||
if not sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
|
||||
|
@ -143,6 +144,7 @@ class ManageInviteRequests(View):
|
|||
invite_request = get_object_or_404(
|
||||
models.InviteRequest, id=request.POST.get("invite-request")
|
||||
)
|
||||
|
||||
# only create a new invite if one doesn't exist already (resending)
|
||||
if not invite_request.invite:
|
||||
invite_request.invite = models.SiteInvite.objects.create(
|
||||
|
@ -170,10 +172,7 @@ class InviteRequest(View):
|
|||
received = True
|
||||
form.save()
|
||||
|
||||
data = {
|
||||
"request_form": form,
|
||||
"request_received": received,
|
||||
}
|
||||
data = {"request_form": form, "request_received": received}
|
||||
return TemplateResponse(request, "landing/landing.html", data)
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
""" moderation via flagged posts and users """
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
|
@ -7,6 +8,7 @@ from django.utils.decorators import method_decorator
|
|||
from django.views import View
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
|
@ -34,10 +36,17 @@ class ReportsAdmin(View):
|
|||
if username:
|
||||
filters["user__username__icontains"] = username
|
||||
filters["resolved"] = resolved
|
||||
|
||||
reports = models.Report.objects.filter(**filters)
|
||||
paginated = Paginator(reports, PAGE_LENGTH)
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
data = {
|
||||
"resolved": resolved,
|
||||
"server": server,
|
||||
"reports": models.Report.objects.filter(**filters),
|
||||
"reports": page,
|
||||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
}
|
||||
return TemplateResponse(request, "settings/reports/reports.html", data)
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
""" the good people stuff! the authors! """
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Avg, Q
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
|
@ -31,9 +30,8 @@ class Author(View):
|
|||
return redirect_local_path
|
||||
|
||||
books = (
|
||||
models.Work.objects.filter(Q(authors=author) | Q(editions__authors=author))
|
||||
.annotate(Avg("editions__review__rating"))
|
||||
.order_by("editions__review__rating__avg")
|
||||
models.Work.objects.filter(editions__authors=author)
|
||||
.order_by("created_date")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
""" the good stuff! the books! """
|
||||
from re import sub
|
||||
from dateutil.parser import parse as dateparse
|
||||
from re import sub, findall
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.contrib.postgres.search import SearchRank, SearchVector
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.datastructures import MultiValueDictKeyError
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views import View
|
||||
|
||||
from bookwyrm import book_search, forms, models
|
||||
|
@ -30,105 +29,27 @@ from .books import set_cover_from_url
|
|||
class EditBook(View):
|
||||
"""edit a book"""
|
||||
|
||||
def get(self, request, book_id=None):
|
||||
def get(self, request, book_id):
|
||||
"""info about a book"""
|
||||
book = None
|
||||
if book_id:
|
||||
book = get_edition(book_id)
|
||||
if not book.description:
|
||||
book.description = book.parent_work.description
|
||||
book = get_edition(book_id)
|
||||
if not book.description:
|
||||
book.description = book.parent_work.description
|
||||
data = {"book": book, "form": forms.EditionForm(instance=book)}
|
||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def post(self, request, book_id=None):
|
||||
def post(self, request, book_id):
|
||||
"""edit a book cool"""
|
||||
# returns None if no match is found
|
||||
book = models.Edition.objects.filter(id=book_id).first()
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
form = forms.EditionForm(request.POST, request.FILES, instance=book)
|
||||
|
||||
data = {"book": book, "form": form}
|
||||
if not form.is_valid():
|
||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||
|
||||
# filter out empty author fields
|
||||
add_author = [author for author in request.POST.getlist("add_author") if author]
|
||||
if add_author:
|
||||
data["add_author"] = add_author
|
||||
data["author_matches"] = []
|
||||
data["isni_matches"] = []
|
||||
|
||||
for author in add_author:
|
||||
if not author:
|
||||
continue
|
||||
# check for existing authors
|
||||
vector = SearchVector("name", weight="A") + SearchVector(
|
||||
"aliases", weight="B"
|
||||
)
|
||||
|
||||
author_matches = (
|
||||
models.Author.objects.annotate(search=vector)
|
||||
.annotate(rank=SearchRank(vector, author))
|
||||
.filter(rank__gt=0.4)
|
||||
.order_by("-rank")[:5]
|
||||
)
|
||||
|
||||
isni_authors = find_authors_by_name(
|
||||
author, description=True
|
||||
) # find matches from ISNI API
|
||||
|
||||
# dedupe isni authors we already have in the DB
|
||||
exists = [
|
||||
i
|
||||
for i in isni_authors
|
||||
for a in author_matches
|
||||
if sub(r"\D", "", str(i.isni)) == sub(r"\D", "", str(a.isni))
|
||||
]
|
||||
|
||||
# pylint: disable=cell-var-from-loop
|
||||
matches = list(filter(lambda x: x not in exists, isni_authors))
|
||||
# combine existing and isni authors
|
||||
matches.extend(author_matches)
|
||||
|
||||
data["author_matches"].append(
|
||||
{
|
||||
"name": author.strip(),
|
||||
"matches": matches,
|
||||
"existing_isnis": exists,
|
||||
}
|
||||
)
|
||||
|
||||
# we're creating a new book
|
||||
if not book:
|
||||
# check if this is an edition of an existing work
|
||||
author_text = book.author_text if book else add_author
|
||||
data["book_matches"] = book_search.search(
|
||||
f'{form.cleaned_data.get("title")} {author_text}',
|
||||
min_confidence=0.5,
|
||||
)[:5]
|
||||
data = add_authors(request, data)
|
||||
|
||||
# either of the above cases requires additional confirmation
|
||||
if add_author or not book:
|
||||
# creting a book or adding an author to a book needs another step
|
||||
data["confirm_mode"] = True
|
||||
# this isn't preserved because it isn't part of the form obj
|
||||
data["remove_authors"] = request.POST.getlist("remove_authors")
|
||||
data["cover_url"] = request.POST.get("cover-url")
|
||||
|
||||
# make sure the dates are passed in as datetime, they're currently a string
|
||||
# QueryDicts are immutable, we need to copy
|
||||
formcopy = data["form"].data.copy()
|
||||
try:
|
||||
formcopy["first_published_date"] = dateparse(
|
||||
formcopy["first_published_date"]
|
||||
)
|
||||
except (MultiValueDictKeyError, ValueError):
|
||||
pass
|
||||
try:
|
||||
formcopy["published_date"] = dateparse(formcopy["published_date"])
|
||||
except (MultiValueDictKeyError, ValueError):
|
||||
pass
|
||||
data["form"].data = formcopy
|
||||
if data.get("add_author"):
|
||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||
|
||||
remove_authors = request.POST.getlist("remove_authors")
|
||||
|
@ -136,15 +57,156 @@ class EditBook(View):
|
|||
book.authors.remove(author_id)
|
||||
|
||||
book = form.save(commit=False)
|
||||
|
||||
url = request.POST.get("cover-url")
|
||||
if url:
|
||||
image = set_cover_from_url(url)
|
||||
if image:
|
||||
book.cover.save(*image, save=False)
|
||||
|
||||
book.save()
|
||||
return redirect(f"/book/{book.id}")
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
|
||||
)
|
||||
class CreateBook(View):
|
||||
"""brand new book"""
|
||||
|
||||
def get(self, request):
|
||||
"""info about a book"""
|
||||
data = {"form": forms.EditionForm()}
|
||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def post(self, request):
|
||||
"""create a new book"""
|
||||
# returns None if no match is found
|
||||
form = forms.EditionForm(request.POST, request.FILES)
|
||||
data = {"form": form}
|
||||
|
||||
# collect data provided by the work or import item
|
||||
parent_work_id = request.POST.get("parent_work")
|
||||
authors = None
|
||||
if request.POST.get("authors"):
|
||||
author_ids = findall(r"\d+", request.POST["authors"])
|
||||
authors = models.Author.objects.filter(id__in=author_ids)
|
||||
|
||||
# fake book in case we need to keep editing
|
||||
if parent_work_id:
|
||||
data["book"] = {
|
||||
"parent_work": {"id": parent_work_id},
|
||||
"authors": authors,
|
||||
}
|
||||
|
||||
if not form.is_valid():
|
||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||
|
||||
data = add_authors(request, data)
|
||||
|
||||
# check if this is an edition of an existing work
|
||||
author_text = ", ".join(data.get("add_author", []))
|
||||
data["book_matches"] = book_search.search(
|
||||
f'{form.cleaned_data.get("title")} {author_text}',
|
||||
min_confidence=0.1,
|
||||
)[:5]
|
||||
|
||||
# go to confirm mode
|
||||
if not parent_work_id or data.get("add_author"):
|
||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||
|
||||
with transaction.atomic():
|
||||
book = form.save()
|
||||
parent_work = get_object_or_404(models.Work, id=parent_work_id)
|
||||
book.parent_work = parent_work
|
||||
|
||||
if authors:
|
||||
book.authors.add(*authors)
|
||||
|
||||
url = request.POST.get("cover-url")
|
||||
if url:
|
||||
image = set_cover_from_url(url)
|
||||
if image:
|
||||
book.cover.save(*image, save=False)
|
||||
|
||||
book.save()
|
||||
return redirect(f"/book/{book.id}")
|
||||
|
||||
|
||||
def add_authors(request, data):
|
||||
"""helper for adding authors"""
|
||||
add_author = [author for author in request.POST.getlist("add_author") if author]
|
||||
if not add_author:
|
||||
return data
|
||||
|
||||
data["add_author"] = add_author
|
||||
data["author_matches"] = []
|
||||
data["isni_matches"] = []
|
||||
|
||||
# creting a book or adding an author to a book needs another step
|
||||
data["confirm_mode"] = True
|
||||
# this isn't preserved because it isn't part of the form obj
|
||||
data["remove_authors"] = request.POST.getlist("remove_authors")
|
||||
data["cover_url"] = request.POST.get("cover-url")
|
||||
|
||||
for author in add_author:
|
||||
# filter out empty author fields
|
||||
if not author:
|
||||
continue
|
||||
# check for existing authors
|
||||
vector = SearchVector("name", weight="A") + SearchVector("aliases", weight="B")
|
||||
|
||||
author_matches = (
|
||||
models.Author.objects.annotate(search=vector)
|
||||
.annotate(rank=SearchRank(vector, author))
|
||||
.filter(rank__gt=0.4)
|
||||
.order_by("-rank")[:5]
|
||||
)
|
||||
|
||||
isni_authors = find_authors_by_name(
|
||||
author, description=True
|
||||
) # find matches from ISNI API
|
||||
|
||||
# dedupe isni authors we already have in the DB
|
||||
exists = [
|
||||
i
|
||||
for i in isni_authors
|
||||
for a in author_matches
|
||||
if sub(r"\D", "", str(i.isni)) == sub(r"\D", "", str(a.isni))
|
||||
]
|
||||
|
||||
# pylint: disable=cell-var-from-loop
|
||||
matches = list(filter(lambda x: x not in exists, isni_authors))
|
||||
# combine existing and isni authors
|
||||
matches.extend(author_matches)
|
||||
|
||||
data["author_matches"].append(
|
||||
{
|
||||
"name": author.strip(),
|
||||
"matches": matches,
|
||||
"existing_isnis": exists,
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.edit_book", raise_exception=True)
|
||||
def create_book_from_data(request):
|
||||
"""create a book with starter data"""
|
||||
author_ids = findall(r"\d+", request.POST.get("authors"))
|
||||
book = {
|
||||
"parent_work": {"id": request.POST.get("parent_work")},
|
||||
"authors": models.Author.objects.filter(id__in=author_ids).all(),
|
||||
"subjects": request.POST.getlist("subjects"),
|
||||
}
|
||||
|
||||
data = {"book": book, "form": forms.EditionForm(request.POST)}
|
||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
|
||||
|
@ -168,6 +230,13 @@ class ConfirmEditBook(View):
|
|||
# save book
|
||||
book = form.save()
|
||||
|
||||
# add known authors
|
||||
authors = None
|
||||
if request.POST.get("authors"):
|
||||
author_ids = findall(r"\d+", request.POST["authors"])
|
||||
authors = models.Author.objects.filter(id__in=author_ids)
|
||||
book.authors.add(*authors)
|
||||
|
||||
# get or create author as needed
|
||||
for i in range(int(request.POST.get("author-match-count", 0))):
|
||||
match = request.POST.get(f"author_match-{i}")
|
||||
|
@ -201,7 +270,7 @@ class ConfirmEditBook(View):
|
|||
book.authors.add(author)
|
||||
|
||||
# create work, if needed
|
||||
if not book_id:
|
||||
if not book.parent_work:
|
||||
work_match = request.POST.get("parent_work")
|
||||
if work_match and work_match != "0":
|
||||
work = get_object_or_404(models.Work, id=work_match)
|
||||
|
|
|
@ -11,7 +11,7 @@ from django.template.response import TemplateResponse
|
|||
from django.views import View
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.views.helpers import is_api_request
|
||||
|
@ -65,6 +65,7 @@ class Editions(View):
|
|||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
"work": work,
|
||||
"work_form": forms.EditionFromWorkForm(instance=work),
|
||||
"languages": languages,
|
||||
"formats": set(
|
||||
e.physical_format.lower() for e in editions if e.physical_format
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
import urllib.parse
|
||||
import re
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db import IntegrityError
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.models.relationship import clear_cache
|
||||
from .helpers import (
|
||||
get_user_from_username,
|
||||
handle_remote_webfinger,
|
||||
|
@ -22,17 +22,17 @@ def follow(request):
|
|||
"""follow another user, here or abroad"""
|
||||
username = request.POST["user"]
|
||||
to_follow = get_user_from_username(request.user, username)
|
||||
clear_cache(request.user, to_follow)
|
||||
|
||||
try:
|
||||
models.UserFollowRequest.objects.create(
|
||||
user_subject=request.user,
|
||||
user_object=to_follow,
|
||||
)
|
||||
except IntegrityError:
|
||||
pass
|
||||
follow_request, created = models.UserFollowRequest.objects.get_or_create(
|
||||
user_subject=request.user,
|
||||
user_object=to_follow,
|
||||
)
|
||||
|
||||
if request.GET.get("next"):
|
||||
return redirect(request.GET.get("next", "/"))
|
||||
if not created:
|
||||
# this request probably failed to connect with the remote
|
||||
# that means we should save to trigger a re-broadcast
|
||||
follow_request.save()
|
||||
|
||||
return redirect(to_follow.local_path)
|
||||
|
||||
|
@ -49,14 +49,14 @@ def unfollow(request):
|
|||
user_subject=request.user, user_object=to_unfollow
|
||||
).delete()
|
||||
except models.UserFollows.DoesNotExist:
|
||||
pass
|
||||
clear_cache(request.user, to_unfollow)
|
||||
|
||||
try:
|
||||
models.UserFollowRequest.objects.get(
|
||||
user_subject=request.user, user_object=to_unfollow
|
||||
).delete()
|
||||
except models.UserFollowRequest.DoesNotExist:
|
||||
pass
|
||||
clear_cache(request.user, to_unfollow)
|
||||
|
||||
# this is handled with ajax so it shouldn't really matter
|
||||
return redirect(request.headers.get("Referer", "/"))
|
||||
|
|
|
@ -5,7 +5,6 @@ from django.shortcuts import get_object_or_404, redirect
|
|||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views.decorators.debug import sensitive_variables, sensitive_post_parameters
|
||||
|
||||
from bookwyrm import emailing, forms, models
|
||||
|
@ -129,12 +128,22 @@ class ConfirmEmail(View):
|
|||
return ConfirmEmailCode().get(request, code)
|
||||
|
||||
|
||||
@require_POST
|
||||
def resend_link(request):
|
||||
"""resend confirmation link"""
|
||||
email = request.POST.get("email")
|
||||
user = get_object_or_404(models.User, email=email)
|
||||
emailing.email_confirmation_email(user)
|
||||
return TemplateResponse(
|
||||
request, "confirm_email/confirm_email.html", {"valid": True}
|
||||
)
|
||||
class ResendConfirmEmail(View):
|
||||
"""you probably didn't get the email because celery is slow but you can try this"""
|
||||
|
||||
def get(self, request, error=False):
|
||||
"""resend link landing page"""
|
||||
return TemplateResponse(request, "confirm_email/resend.html", {"error": error})
|
||||
|
||||
def post(self, request):
|
||||
"""resend confirmation link"""
|
||||
email = request.POST.get("email")
|
||||
try:
|
||||
user = models.User.objects.get(email=email)
|
||||
except models.User.DoesNotExist:
|
||||
return self.get(request, error=True)
|
||||
|
||||
emailing.email_confirmation_email(user)
|
||||
return TemplateResponse(
|
||||
request, "confirm_email/confirm_email.html", {"valid": True}
|
||||
)
|
||||
|
|
97
bookwyrm/views/preferences/export.py
Normal file
97
bookwyrm/views/preferences/export.py
Normal file
|
@ -0,0 +1,97 @@
|
|||
""" Let users export their book data """
|
||||
import csv
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db.models import Q
|
||||
from django.http import StreamingHttpResponse
|
||||
from django.template.response import TemplateResponse
|
||||
from django.views import View
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.http import require_GET
|
||||
|
||||
from bookwyrm import models
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class Export(View):
|
||||
"""Let users export data"""
|
||||
|
||||
def get(self, request):
|
||||
"""Request csv file"""
|
||||
return TemplateResponse(request, "preferences/export.html")
|
||||
|
||||
|
||||
@login_required
|
||||
@require_GET
|
||||
def export_user_book_data(request):
|
||||
"""Streaming the csv file of a user's book data"""
|
||||
data = (
|
||||
models.Edition.viewer_aware_objects(request.user)
|
||||
.filter(
|
||||
Q(shelves__user=request.user)
|
||||
| Q(readthrough__user=request.user)
|
||||
| Q(review__user=request.user)
|
||||
| Q(comment__user=request.user)
|
||||
| Q(quotation__user=request.user)
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
generator = csv_row_generator(data, request.user)
|
||||
|
||||
pseudo_buffer = Echo()
|
||||
writer = csv.writer(pseudo_buffer)
|
||||
# for testing, if you want to see the results in the browser:
|
||||
# from django.http import JsonResponse
|
||||
# return JsonResponse(list(generator), safe=False)
|
||||
return StreamingHttpResponse(
|
||||
(writer.writerow(row) for row in generator),
|
||||
content_type="text/csv",
|
||||
headers={"Content-Disposition": 'attachment; filename="bookwyrm-export.csv"'},
|
||||
)
|
||||
|
||||
|
||||
def csv_row_generator(books, user):
|
||||
"""generate a csv entry for the user's book"""
|
||||
deduplication_fields = [
|
||||
f.name
|
||||
for f in models.Edition._meta.get_fields() # pylint: disable=protected-access
|
||||
if getattr(f, "deduplication_field", False)
|
||||
]
|
||||
fields = (
|
||||
["title", "author_text"]
|
||||
+ deduplication_fields
|
||||
+ ["rating", "review_name", "review_cw", "review_content"]
|
||||
)
|
||||
yield fields
|
||||
for book in books:
|
||||
# I think this is more efficient than doing a subquery in the view? but idk
|
||||
review_rating = (
|
||||
models.Review.objects.filter(user=user, book=book, rating__isnull=False)
|
||||
.order_by("-published_date")
|
||||
.first()
|
||||
)
|
||||
|
||||
book.rating = review_rating.rating if review_rating else None
|
||||
|
||||
review = (
|
||||
models.Review.objects.filter(user=user, book=book, content__isnull=False)
|
||||
.order_by("-published_date")
|
||||
.first()
|
||||
)
|
||||
if review:
|
||||
book.review_name = review.name
|
||||
book.review_cw = review.content_warning
|
||||
book.review_content = review.raw_content
|
||||
yield [getattr(book, field, "") or "" for field in fields]
|
||||
|
||||
|
||||
class Echo:
|
||||
"""An object that implements just the write method of the file-like
|
||||
interface. (https://docs.djangoproject.com/en/3.2/howto/outputting-csv/)
|
||||
"""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def write(self, value):
|
||||
"""Write the value by returning it, instead of storing in a buffer."""
|
||||
return value
|
Loading…
Add table
Add a link
Reference in a new issue