forked from mirrors/bookwyrm
Merge branch 'main' into stopped-shelf
This commit is contained in:
commit
ec21d20b90
94 changed files with 10585 additions and 3417 deletions
|
@ -4,6 +4,7 @@ from django import forms
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.models.fields import ClearableFileInputWithWarning
|
from bookwyrm.models.fields import ClearableFileInputWithWarning
|
||||||
from .custom_form import CustomForm
|
from .custom_form import CustomForm
|
||||||
|
from .widgets import ArrayWidget, SelectDateWidget, Select
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=missing-class-docstring
|
# pylint: disable=missing-class-docstring
|
||||||
|
@ -14,14 +15,6 @@ class CoverForm(CustomForm):
|
||||||
help_texts = {f: None for f in fields}
|
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 EditionForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Edition
|
model = models.Edition
|
||||||
|
@ -56,16 +49,16 @@ class EditionForm(CustomForm):
|
||||||
"publishers": forms.TextInput(
|
"publishers": forms.TextInput(
|
||||||
attrs={"aria-describedby": "desc_publishers_help desc_publishers"}
|
attrs={"aria-describedby": "desc_publishers_help desc_publishers"}
|
||||||
),
|
),
|
||||||
"first_published_date": forms.SelectDateWidget(
|
"first_published_date": SelectDateWidget(
|
||||||
attrs={"aria-describedby": "desc_first_published_date"}
|
attrs={"aria-describedby": "desc_first_published_date"}
|
||||||
),
|
),
|
||||||
"published_date": forms.SelectDateWidget(
|
"published_date": SelectDateWidget(
|
||||||
attrs={"aria-describedby": "desc_published_date"}
|
attrs={"aria-describedby": "desc_published_date"}
|
||||||
),
|
),
|
||||||
"cover": ClearableFileInputWithWarning(
|
"cover": ClearableFileInputWithWarning(
|
||||||
attrs={"aria-describedby": "desc_cover"}
|
attrs={"aria-describedby": "desc_cover"}
|
||||||
),
|
),
|
||||||
"physical_format": forms.Select(
|
"physical_format": Select(
|
||||||
attrs={"aria-describedby": "desc_physical_format"}
|
attrs={"aria-describedby": "desc_physical_format"}
|
||||||
),
|
),
|
||||||
"physical_format_detail": forms.TextInput(
|
"physical_format_detail": forms.TextInput(
|
||||||
|
@ -85,3 +78,27 @@ class EditionForm(CustomForm):
|
||||||
),
|
),
|
||||||
"ASIN": forms.TextInput(attrs={"aria-describedby": "desc_ASIN"}),
|
"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):
|
class ReadThroughForm(CustomForm):
|
||||||
def clean(self):
|
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()
|
cleaned_data = super().clean()
|
||||||
start_date = cleaned_data.get("start_date")
|
start_date = cleaned_data.get("start_date")
|
||||||
finish_date = cleaned_data.get("finish_date")
|
finish_date = cleaned_data.get("finish_date")
|
||||||
|
|
|
@ -42,4 +42,4 @@ class InviteRequestForm(CustomForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.InviteRequest
|
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
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -389,7 +389,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
|
||||||
self.alt_field = alt_field
|
self.alt_field = alt_field
|
||||||
super().__init__(*args, **kwargs)
|
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):
|
def set_field_from_activity(self, instance, data, save=True, overwrite=True):
|
||||||
"""helper function for assinging a value to the field"""
|
"""helper function for assinging a value to the field"""
|
||||||
value = getattr(data, self.get_activitypub_field())
|
value = getattr(data, self.get_activitypub_field())
|
||||||
|
|
|
@ -39,15 +39,14 @@ class UserRelationship(BookWyrmModel):
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""clear the template cache"""
|
"""clear the template cache"""
|
||||||
# invalidate the template cache
|
clear_cache(self.user_subject, self.user_object)
|
||||||
cache.delete_many(
|
|
||||||
[
|
|
||||||
f"relationship-{self.user_subject.id}-{self.user_object.id}",
|
|
||||||
f"relationship-{self.user_object.id}-{self.user_subject.id}",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
super().save(*args, **kwargs)
|
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:
|
class Meta:
|
||||||
"""relationships should be unique"""
|
"""relationships should be unique"""
|
||||||
|
|
||||||
|
@ -90,7 +89,9 @@ class UserFollows(ActivityMixin, UserRelationship):
|
||||||
user_object=self.user_subject,
|
user_object=self.user_subject,
|
||||||
)
|
)
|
||||||
).exists():
|
).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
|
# don't broadcast this type of relationship -- accepts and requests
|
||||||
# are handled by the UserFollowRequest model
|
# are handled by the UserFollowRequest model
|
||||||
super().save(*args, broadcast=False, **kwargs)
|
super().save(*args, broadcast=False, **kwargs)
|
||||||
|
@ -98,11 +99,12 @@ class UserFollows(ActivityMixin, UserRelationship):
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_request(cls, follow_request):
|
def from_request(cls, follow_request):
|
||||||
"""converts a follow request into a follow relationship"""
|
"""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_subject=follow_request.user_subject,
|
||||||
user_object=follow_request.user_object,
|
user_object=follow_request.user_object,
|
||||||
remote_id=follow_request.remote_id,
|
remote_id=follow_request.remote_id,
|
||||||
)
|
)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||||
|
@ -133,7 +135,9 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||||
user_object=self.user_subject,
|
user_object=self.user_subject,
|
||||||
)
|
)
|
||||||
).exists():
|
).exists():
|
||||||
raise IntegrityError()
|
raise IntegrityError(
|
||||||
|
"Attempting to follow blocked user", self.user_subject, self.user_object
|
||||||
|
)
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
if broadcast and self.user_subject.local and not self.user_object.local:
|
if broadcast and self.user_subject.local and not self.user_object.local:
|
||||||
|
@ -174,7 +178,8 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
UserFollows.from_request(self)
|
UserFollows.from_request(self)
|
||||||
self.delete()
|
if self.id:
|
||||||
|
self.delete()
|
||||||
|
|
||||||
def reject(self):
|
def reject(self):
|
||||||
"""generate a Reject for this follow request"""
|
"""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_subject, user_object=self.user_object)
|
||||||
| Q(user_subject=self.user_object, user_object=self.user_subject)
|
| Q(user_subject=self.user_object, user_object=self.user_subject)
|
||||||
).delete()
|
).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
|
# registration
|
||||||
allow_registration = models.BooleanField(default=False)
|
allow_registration = models.BooleanField(default=False)
|
||||||
allow_invite_requests = models.BooleanField(default=True)
|
allow_invite_requests = models.BooleanField(default=True)
|
||||||
|
invite_request_question = models.BooleanField(default=False)
|
||||||
require_confirm_email = models.BooleanField(default=True)
|
require_confirm_email = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
invite_question_text = models.CharField(
|
||||||
|
max_length=255, blank=True, default="What is your favourite book?"
|
||||||
|
)
|
||||||
# images
|
# images
|
||||||
logo = models.ImageField(upload_to="logos/", null=True, blank=True)
|
logo = models.ImageField(upload_to="logos/", null=True, blank=True)
|
||||||
logo_small = 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)
|
return urljoin(STATIC_FULL_URL, default_path)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
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:
|
if not self.require_confirm_email:
|
||||||
User.objects.filter(is_active=False, deactivation_reason="pending").update(
|
User.objects.filter(is_active=False, deactivation_reason="pending").update(
|
||||||
is_active=True, deactivation_reason=None
|
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)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -150,6 +157,7 @@ class InviteRequest(BookWyrmModel):
|
||||||
invite = models.ForeignKey(
|
invite = models.ForeignKey(
|
||||||
SiteInvite, on_delete=models.SET_NULL, null=True, blank=True
|
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)
|
invite_sent = models.BooleanField(default=False)
|
||||||
ignored = models.BooleanField(default=False)
|
ignored = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
|
|
@ -289,6 +289,7 @@ LANGUAGES = [
|
||||||
("no-no", _("Norsk (Norwegian)")),
|
("no-no", _("Norsk (Norwegian)")),
|
||||||
("pt-br", _("Português do Brasil (Brazilian Portuguese)")),
|
("pt-br", _("Português do Brasil (Brazilian Portuguese)")),
|
||||||
("pt-pt", _("Português Europeu (European Portuguese)")),
|
("pt-pt", _("Português Europeu (European Portuguese)")),
|
||||||
|
("ro-ro", _("Română (Romanian)")),
|
||||||
("sv-se", _("Svenska (Swedish)")),
|
("sv-se", _("Svenska (Swedish)")),
|
||||||
("zh-hans", _("简体中文 (Simplified Chinese)")),
|
("zh-hans", _("简体中文 (Simplified Chinese)")),
|
||||||
("zh-hant", _("繁體中文 (Traditional Chinese)")),
|
("zh-hant", _("繁體中文 (Traditional Chinese)")),
|
||||||
|
|
|
@ -36,6 +36,18 @@ body {
|
||||||
flex-direction: column;
|
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 {
|
button {
|
||||||
border: none;
|
border: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -129,14 +141,6 @@ button:focus-visible .button-invisible-overlay {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** Tooltips
|
|
||||||
******************************************************************************/
|
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** States
|
/** States
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,9 @@ $primary: #005e50;
|
||||||
$primary-light: #1d2b28;
|
$primary-light: #1d2b28;
|
||||||
$info: #1f4666;
|
$info: #1f4666;
|
||||||
$success: #246447;
|
$success: #246447;
|
||||||
|
$success-light: #0d2f1e;
|
||||||
$warning: #8b6c15;
|
$warning: #8b6c15;
|
||||||
|
$warning-light: #372e13;
|
||||||
$danger: #872538;
|
$danger: #872538;
|
||||||
$danger-light: #481922;
|
$danger-light: #481922;
|
||||||
$light: #393939;
|
$light: #393939;
|
||||||
|
@ -26,6 +28,8 @@ $background-body: rgb(24, 27, 28);
|
||||||
$background-secondary: rgb(28, 30, 32);
|
$background-secondary: rgb(28, 30, 32);
|
||||||
$background-tertiary: rgb(32, 34, 36);
|
$background-tertiary: rgb(32, 34, 36);
|
||||||
$modal-background-background-color: rgba($black, 0.8);
|
$modal-background-background-color: rgba($black, 0.8);
|
||||||
|
$scrollbar-track: $background-secondary;
|
||||||
|
$scrollbar-thumb: $light;
|
||||||
|
|
||||||
/* highlight colors */
|
/* highlight colors */
|
||||||
$primary-highlight: $primary;
|
$primary-highlight: $primary;
|
||||||
|
|
|
@ -19,6 +19,8 @@ $scheme-main: $white-bis;
|
||||||
$background-body: $white;
|
$background-body: $white;
|
||||||
$background-secondary: $white-ter;
|
$background-secondary: $white-ter;
|
||||||
$background-tertiary: $white-bis;
|
$background-tertiary: $white-bis;
|
||||||
|
$scrollbar-track: $background-secondary;
|
||||||
|
$scrollbar-thumb: $grey-lighter;
|
||||||
|
|
||||||
/* highlight colors */
|
/* highlight colors */
|
||||||
$primary-highlight: $primary-light;
|
$primary-highlight: $primary-light;
|
||||||
|
|
|
@ -99,7 +99,7 @@
|
||||||
<p>
|
<p>
|
||||||
{% url "conduct" as coc_path %}
|
{% url "conduct" as coc_path %}
|
||||||
{% blocktrans trimmed with site_name=site.name %}
|
{% 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 %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -208,9 +208,17 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{% if book.parent_work.editions.count > 1 %}
|
{% with work=book.parent_work %}
|
||||||
<p>{% blocktrans with path=book.parent_work.local_path count=book.parent_work.editions.count %}<a href="{{ path }}/editions">{{ count }} editions</a>{% endblocktrans %}</p>
|
<p>
|
||||||
{% endif %}
|
<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>
|
</div>
|
||||||
|
|
||||||
{# user's relationship to the book #}
|
{# user's relationship to the book #}
|
||||||
|
|
|
@ -3,18 +3,24 @@
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
{% load utilities %}
|
{% 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 %}
|
{% block content %}
|
||||||
<header class="block">
|
<header class="block">
|
||||||
<h1 class="title level-left">
|
<h1 class="title level-left">
|
||||||
{% if book %}
|
{% if book.title %}
|
||||||
{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}
|
{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans "Add Book" %}
|
{% trans "Add Book" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
{% if book %}
|
{% if book.created_date %}
|
||||||
<dl>
|
<dl>
|
||||||
<dt class="is-pulled-left mr-5 has-text-weight-semibold">{% trans "Added:" %}</dt>
|
<dt class="is-pulled-left mr-5 has-text-weight-semibold">{% trans "Added:" %}</dt>
|
||||||
<dd class="ml-2">{{ book.created_date | naturaltime }}</dd>
|
<dd class="ml-2">{{ book.created_date | naturaltime }}</dd>
|
||||||
|
@ -33,7 +39,7 @@
|
||||||
|
|
||||||
<form
|
<form
|
||||||
class="block"
|
class="block"
|
||||||
{% if book %}
|
{% if book.id %}
|
||||||
name="edit-book"
|
name="edit-book"
|
||||||
action="{{ book.local_path }}/{% if confirm_mode %}confirm{% else %}edit{% endif %}"
|
action="{{ book.local_path }}/{% if confirm_mode %}confirm{% else %}edit{% endif %}"
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -97,7 +103,7 @@
|
||||||
<input type="radio" name="parent_work" value="{{ match.parent_work.id }}"> {{ match.parent_work.title }}
|
<input type="radio" name="parent_work" value="{{ match.parent_work.id }}"> {{ match.parent_work.title }}
|
||||||
</label>
|
</label>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<label>
|
<label class="label mt-2">
|
||||||
<input type="radio" name="parent_work" value="0" required> {% trans "This is a new work" %}
|
<input type="radio" name="parent_work" value="0" required> {% trans "This is a new work" %}
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
@ -119,7 +125,7 @@
|
||||||
{% if not confirm_mode %}
|
{% if not confirm_mode %}
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
<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>
|
<a class="button" href="{{ book.local_path }}">{% trans "Cancel" %}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="/" class="button" data-back>
|
<a href="/" class="button" data-back>
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
<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="columns">
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
<section class="block">
|
<section class="block">
|
||||||
|
@ -153,8 +155,7 @@
|
||||||
<label class="label" for="id_first_published_date">
|
<label class="label" for="id_first_published_date">
|
||||||
{% trans "First published date:" %}
|
{% trans "First published date:" %}
|
||||||
</label>
|
</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" %}
|
{% include 'snippets/form_errors.html' with errors_list=form.first_published_date.errors id="desc_first_published_date" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -162,7 +163,7 @@
|
||||||
<label class="label" for="id_published_date">
|
<label class="label" for="id_published_date">
|
||||||
{% trans "Published date:" %}
|
{% trans "Published date:" %}
|
||||||
</label>
|
</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" %}
|
{% include 'snippets/form_errors.html' with errors_list=form.published_date.errors id="desc_published_date" %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -175,6 +176,8 @@
|
||||||
</h2>
|
</h2>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
{% if book.authors.exists %}
|
{% 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>
|
<fieldset>
|
||||||
{% for author in book.authors.all %}
|
{% for author in book.authors.all %}
|
||||||
<div class="is-flex is-justify-content-space-between">
|
<div class="is-flex is-justify-content-space-between">
|
||||||
|
@ -255,9 +258,7 @@
|
||||||
<label class="label" for="id_physical_format">
|
<label class="label" for="id_physical_format">
|
||||||
{% trans "Format:" %}
|
{% trans "Format:" %}
|
||||||
</label>
|
</label>
|
||||||
<div class="select">
|
{{ form.physical_format }}
|
||||||
{{ form.physical_format }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% include 'snippets/form_errors.html' with errors_list=form.physical_format.errors id="desc_physical_format" %}
|
{% include 'snippets/form_errors.html' with errors_list=form.physical_format.errors id="desc_physical_format" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -46,7 +46,36 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="block">
|
||||||
{% include 'snippets/pagination.html' with page=editions path=request.path %}
|
{% include 'snippets/pagination.html' with page=editions path=request.path %}
|
||||||
</div>
|
</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 %}
|
{% endblock %}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
<header class="block">
|
<header class="block">
|
||||||
<h1 class="title">
|
<h1 class="title">
|
||||||
{% blocktrans with title=book|book_title %}
|
{% blocktrans trimmed with title=book|book_title %}
|
||||||
Links for "<em>{{ title }}</em>"
|
Links for "<em>{{ title }}</em>"
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
<section class="block">
|
<section class="block">
|
||||||
{% trans "Can't find your code?" as button_text %}
|
<form name="fallback" method="GET" action="{% url 'resend-link' %}" autocomplete="off">
|
||||||
{% include "snippets/toggle/open_button.html" with text=button_text controls_text="resend_form" focus="resend_form_header" %}
|
<button
|
||||||
{% include "confirm_email/resend_form.html" with controls_text="resend_form" %}
|
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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
|
@ -5,7 +5,19 @@
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<h2 class="title is-4">{% trans "Your Books" %}</h2>
|
<h2 class="title is-4">{% trans "Your Books" %}</h2>
|
||||||
{% if not suggested_books %}
|
{% 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 %}
|
{% else %}
|
||||||
{% with active_book=request.GET.book %}
|
{% with active_book=request.GET.book %}
|
||||||
<div class="tab-group">
|
<div class="tab-group">
|
||||||
|
|
|
@ -14,28 +14,32 @@
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-pulled-left" for="source">
|
<label class="label" for="source">
|
||||||
{% trans "Data source:" %}
|
{% trans "Data source:" %}
|
||||||
</label>
|
</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>
|
||||||
|
|
||||||
<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">
|
<div class="field">
|
||||||
<label class="label" for="id_csv_file">{% trans "Data file:" %}</label>
|
<label class="label" for="id_csv_file">{% trans "Data file:" %}</label>
|
||||||
{{ import_form.csv_file }}
|
{{ import_form.csv_file }}
|
||||||
|
@ -63,7 +67,7 @@
|
||||||
<div class="content block">
|
<div class="content block">
|
||||||
<h2 class="title">{% trans "Recent Imports" %}</h2>
|
<h2 class="title">{% trans "Recent Imports" %}</h2>
|
||||||
{% if not jobs %}
|
{% if not jobs %}
|
||||||
<p>{% trans "No recent imports" %}</p>
|
<p><em>{% trans "No recent imports" %}</em></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for job in jobs %}
|
{% 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" %}
|
{% include 'snippets/form_errors.html' with errors_list=request_form.email.errors id="desc_request_email" %}
|
||||||
</div>
|
</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>
|
<button type="submit" class="button is-link">{% trans "Submit" %}</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
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>
|
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Delete Account" %}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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>
|
<h2 class="menu-label">{% trans "Relationships" %}</h2>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li>
|
<li>
|
||||||
|
|
|
@ -17,7 +17,14 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block modal-form-open %}
|
{% 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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block modal-body %}
|
{% block modal-body %}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
<div class="block table-container">
|
<div class="block table-container">
|
||||||
<table class="table is-striped">
|
<table class="table is-striped is-fullwidth">
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
{% url 'settings-announcements' as url %}
|
{% url 'settings-announcements' as url %}
|
||||||
|
|
|
@ -154,7 +154,7 @@
|
||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="table is-striped">
|
<table class="table is-striped is-fullwidth">
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<label for="id_string_match">{% trans "String match" %}</label>
|
<label for="id_string_match">{% trans "String match" %}</label>
|
||||||
|
|
|
@ -10,26 +10,26 @@
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
||||||
<div class="columns block has-text-centered is-mobile is-multiline">
|
<div class="columns block has-text-centered is-mobile is-multiline">
|
||||||
<div class="column is-3-desktop is-6-mobile">
|
<div class="column is-3-desktop is-6-mobile is-flex">
|
||||||
<div class="notification">
|
<div class="notification is-flex-grow-1">
|
||||||
<h3>{% trans "Total users" %}</h3>
|
<h3>{% trans "Total users" %}</h3>
|
||||||
<p class="title is-5">{{ users|intcomma }}</p>
|
<p class="title is-5">{{ users|intcomma }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3-desktop is-6-mobile">
|
<div class="column is-3-desktop is-6-mobil is-flexe">
|
||||||
<div class="notification">
|
<div class="notification is-flex-grow-1">
|
||||||
<h3>{% trans "Active this month" %}</h3>
|
<h3>{% trans "Active this month" %}</h3>
|
||||||
<p class="title is-5">{{ active_users|intcomma }}</p>
|
<p class="title is-5">{{ active_users|intcomma }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3-desktop is-6-mobile">
|
<div class="column is-3-desktop is-6-mobile is-flex">
|
||||||
<div class="notification">
|
<div class="notification is-flex-grow-1">
|
||||||
<h3>{% trans "Statuses" %}</h3>
|
<h3>{% trans "Statuses" %}</h3>
|
||||||
<p class="title is-5">{{ statuses|intcomma }}</p>
|
<p class="title is-5">{{ statuses|intcomma }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3-desktop is-6-mobile">
|
<div class="column is-3-desktop is-6-mobile is-flex">
|
||||||
<div class="notification">
|
<div class="notification is-flex-grow-1">
|
||||||
<h3>{% trans "Works" %}</h3>
|
<h3>{% trans "Works" %}</h3>
|
||||||
<p class="title is-5">{{ works|intcomma }}</p>
|
<p class="title is-5">{{ works|intcomma }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,8 +38,8 @@
|
||||||
|
|
||||||
<div class="columns block is-multiline">
|
<div class="columns block is-multiline">
|
||||||
{% if reports %}
|
{% if reports %}
|
||||||
<div class="column">
|
<div class="column is-flex">
|
||||||
<a href="{% url 'settings-reports' %}" class="notification is-warning is-block">
|
<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 %}
|
{% blocktrans trimmed count counter=reports with display_count=reports|intcomma %}
|
||||||
{{ display_count }} open report
|
{{ display_count }} open report
|
||||||
{% plural %}
|
{% plural %}
|
||||||
|
@ -50,8 +50,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if pending_domains %}
|
{% if pending_domains %}
|
||||||
<div class="column">
|
<div class="column is-flex">
|
||||||
<a href="{% url 'settings-link-domain' %}" class="notification is-primary is-block">
|
<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 %}
|
{% blocktrans trimmed count counter=pending_domains with display_count=pending_domains|intcomma %}
|
||||||
{{ display_count }} domain needs review
|
{{ display_count }} domain needs review
|
||||||
{% plural %}
|
{% plural %}
|
||||||
|
@ -62,8 +62,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not site.allow_registration and site.allow_invite_requests and invite_requests %}
|
{% if not site.allow_registration and site.allow_invite_requests and invite_requests %}
|
||||||
<div class="column">
|
<div class="column is-flex">
|
||||||
<a href="{% url 'settings-invite-requests' %}" class="notification is-block is-success">
|
<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 %}
|
{% blocktrans trimmed count counter=invite_requests with display_count=invite_requests|intcomma %}
|
||||||
{{ display_count }} invite request
|
{{ display_count }} invite request
|
||||||
{% plural %}
|
{% plural %}
|
||||||
|
@ -74,8 +74,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if current_version %}
|
{% if current_version %}
|
||||||
<div class="column">
|
<div class="column is-flex">
|
||||||
<a href="https://docs.joinbookwyrm.com/updating-your-instance.html" class="notification is-block is-warning" target="_blank">
|
<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 %}
|
{% blocktrans trimmed with current=current_version available=available_version %}
|
||||||
An update is available! You're running v{{ current }} and the latest release is {{ available }}.
|
An update is available! You're running v{{ current }} and the latest release is {{ available }}.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="table is-striped">
|
<table class="table is-striped is-fullwidth">
|
||||||
<tr>
|
<tr>
|
||||||
{% url 'settings-federation' as url %}
|
{% url 'settings-federation' as url %}
|
||||||
<th>
|
<th>
|
||||||
|
|
|
@ -40,6 +40,9 @@
|
||||||
{% include 'snippets/table-sort-header.html' with field="invite__invitees__created_date" sort=sort text=text %}
|
{% include 'snippets/table-sort-header.html' with field="invite__invitees__created_date" sort=sort text=text %}
|
||||||
</th>
|
</th>
|
||||||
<th>{% trans "Email" %}</th>
|
<th>{% trans "Email" %}</th>
|
||||||
|
{% if site.invite_request_question %}
|
||||||
|
<th>{% trans "Answer" %}</th>
|
||||||
|
{% endif %}
|
||||||
<th>
|
<th>
|
||||||
{% trans "Status" as text %}
|
{% trans "Status" as text %}
|
||||||
{% include 'snippets/table-sort-header.html' with field="invite__times_used" sort=sort text=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.created_date | naturaltime }}</td>
|
||||||
<td>{{ req.invite.invitees.first.created_date | naturaltime }}</td>
|
<td>{{ req.invite.invitees.first.created_date | naturaltime }}</td>
|
||||||
<td>{{ req.email }}</td>
|
<td>{{ req.email }}</td>
|
||||||
|
{% if site.invite_request_question %}
|
||||||
|
<td>{{ req.answer }}</td>
|
||||||
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
{% if req.invite.times_used %}
|
{% if req.invite.times_used %}
|
||||||
{% trans "Accepted" %}
|
{% trans "Accepted" %}
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
|
|
||||||
<div class="field">
|
<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">
|
<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>
|
</div>
|
||||||
|
|
||||||
{% include 'snippets/form_errors.html' with errors_list=form.address.errors id="desc_address" %}
|
{% 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>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
<div class="column">
|
<div class="column is-clipped">
|
||||||
{% block panel %}{% endblock %}
|
{% block panel %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -44,5 +44,6 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% include 'snippets/pagination.html' with page=reports path=request.path %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -139,12 +139,6 @@
|
||||||
{% trans "Allow registration" %}
|
{% trans "Allow registration" %}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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">
|
<div class="field">
|
||||||
<label class="label mb-0" for="id_require_confirm_email">
|
<label class="label mb-0" for="id_require_confirm_email">
|
||||||
{{ site_form.require_confirm_email }}
|
{{ site_form.require_confirm_email }}
|
||||||
|
@ -152,6 +146,24 @@
|
||||||
</label>
|
</label>
|
||||||
<p class="help" id="desc_require_confirm_email">{% trans "(Recommended if registration is open)" %}</p>
|
<p class="help" id="desc_require_confirm_email">{% trans "(Recommended if registration is open)" %}</p>
|
||||||
</div>
|
</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">
|
<div class="field">
|
||||||
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
|
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
|
||||||
{{ site_form.registration_closed_text }}
|
{{ site_form.registration_closed_text }}
|
||||||
|
|
|
@ -88,7 +88,7 @@
|
||||||
<section class="block content">
|
<section class="block content">
|
||||||
<h2 class="title is-4">{% trans "Available Themes" %}</h2>
|
<h2 class="title is-4">{% trans "Available Themes" %}</h2>
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="table is-striped">
|
<table class="table is-striped is-fullwidth">
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
{% trans "Theme name" %}
|
{% trans "Theme name" %}
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-container block">
|
<div class="table-container block">
|
||||||
<table class="table is-striped">
|
<table class="table is-striped is-fullwidth">
|
||||||
<tr>
|
<tr>
|
||||||
{% url 'settings-users' as url %}
|
{% url 'settings-users' as url %}
|
||||||
<th>
|
<th>
|
||||||
|
@ -61,10 +61,25 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<tr>
|
<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.created_date }}</td>
|
||||||
<td>{{ user.last_active_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" %}
|
{% if status != "local" %}
|
||||||
<td>
|
<td>
|
||||||
{% if user.federated_server %}
|
{% if user.federated_server %}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<div class="column is-flex is-flex-direction-column">
|
<div class="column is-flex is-flex-direction-column">
|
||||||
<h4 class="title is-4">{% trans "Profile" %}</h4>
|
<h4 class="title is-4">{% trans "Profile" %}</h4>
|
||||||
<div class="box is-flex-grow-1">
|
<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 %}
|
{% if user.summary %}
|
||||||
<div class="box content has-background-secondary is-shadowless">
|
<div class="box content has-background-secondary is-shadowless">
|
||||||
{{ user.summary|to_markdown|safe }}
|
{{ user.summary|to_markdown|safe }}
|
||||||
|
@ -14,6 +14,10 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p class="mt-2"><a href="{{ user.local_path }}">{% trans "View user profile" %}</a></p>
|
<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>
|
</div>
|
||||||
<div class="column is-flex is-flex-direction-column is-4">
|
<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>
|
<dt class="is-pulled-left mr-5">{% trans "Blocked by count:" %}</dt>
|
||||||
<dd>{{ user.blocked_by.count }}</dd>
|
<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>
|
<dt class="is-pulled-left mr-5">{% trans "Last active date:" %}</dt>
|
||||||
<dd>{{ user.last_active_date }}</dd>
|
<dd>{{ user.last_active_date }}</dd>
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
{% with goal.progress as progress %}
|
{% with goal.progress as progress %}
|
||||||
<p>
|
<p>
|
||||||
{% if progress.percent >= 100 %}
|
{% if progress.percent >= 100 %}
|
||||||
{% trans "Success!" %}
|
{% trans "Success!" context "Goal successfully completed" %}
|
||||||
{% elif progress.percent %}
|
{% elif progress.percent %}
|
||||||
{% blocktrans with percent=progress.percent %}{{ percent }}% complete!{% endblocktrans %}
|
{% blocktrans with percent=progress.percent %}{{ percent }}% complete!{% endblocktrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -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 %}
|
{% 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 %}
|
{% endif %}
|
||||||
</p>
|
</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 %}
|
{% endwith %}
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
<form name="reading-progress-{{ uuid }}" action="{% url 'reading-status-update' book.id %}" method="POST" class="submit-status">
|
<form name="reading-progress-{{ uuid }}" action="{% url 'reading-status-update' book.id %}" method="POST" class="submit-status">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="id" value="{{ readthrough.id }}">
|
<input type="hidden" name="id" value="{{ readthrough.id }}">
|
||||||
|
<input type="hidden" name="start_date" value="{{ readthrough.start_date|date:'Y-m-d' }}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block reading-dates %}
|
{% block reading-dates %}
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% 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' %}
|
{% if status_type == 'Review' %}
|
||||||
<header class="mb-2">
|
<header class="mb-2">
|
||||||
<h3
|
<h3
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
|
|
||||||
<div class="card-footer-item">
|
<div class="card-footer-item">
|
||||||
{% trans "Reply" as button_text %}
|
{% 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>
|
||||||
<div class="card-footer-item">
|
<div class="card-footer-item">
|
||||||
{% include 'snippets/boost_button.html' with status=status %}
|
{% include 'snippets/boost_button.html' with status=status %}
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% if not moderation_mode %}
|
{% if not moderation_mode %}
|
||||||
<div class="card-footer-item">
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<p><a href="{{ user.remote_id }}">{{ user.username }}</a></p>
|
<p><a href="{{ user.remote_id }}">{{ user.username }}</a></p>
|
||||||
<p>{% blocktrans with date=user.created_date|naturaltime %}Joined {{ date }}{% endblocktrans %}</p>
|
<p>{% blocktrans with date=user.created_date|naturaltime %}Joined {{ date }}{% endblocktrans %}</p>
|
||||||
<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-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>
|
<a href="{% url 'user-following' user|username %}">{% blocktrans with counter=user.following.count %}{{ counter }} following{% endblocktrans %}</a>
|
||||||
|
|
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>
|
|
@ -60,7 +60,7 @@ class EditBookViews(TestCase):
|
||||||
|
|
||||||
def test_edit_book_create_page(self):
|
def test_edit_book_create_page(self):
|
||||||
"""there are so many views, this just makes sure it LOADS"""
|
"""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 = self.factory.get("")
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
request.user.is_superuser = True
|
request.user.is_superuser = True
|
||||||
|
|
|
@ -360,10 +360,17 @@ class RegisterViews(TestCase):
|
||||||
result = view(request)
|
result = view(request)
|
||||||
validate_html(result.render())
|
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"""
|
"""try again"""
|
||||||
request = self.factory.post("", {"email": "mouse@mouse.com"})
|
request = self.factory.post("", {"email": "mouse@mouse.com"})
|
||||||
request.user = self.anonymous_user
|
request.user = self.anonymous_user
|
||||||
with patch("bookwyrm.emailing.send_email.delay") as mock:
|
with patch("bookwyrm.emailing.send_email.delay") as mock:
|
||||||
views.resend_link(request)
|
views.ResendConfirmEmail.as_view()(request)
|
||||||
self.assertEqual(mock.call_count, 1)
|
self.assertEqual(mock.call_count, 1)
|
||||||
|
|
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)
|
|
@ -71,7 +71,7 @@ urlpatterns = [
|
||||||
views.ConfirmEmailCode.as_view(),
|
views.ConfirmEmailCode.as_view(),
|
||||||
name="confirm-email-code",
|
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"^logout/?$", views.Logout.as_view(), name="logout"),
|
||||||
re_path(
|
re_path(
|
||||||
r"^password-reset/?$",
|
r"^password-reset/?$",
|
||||||
|
@ -475,6 +475,12 @@ urlpatterns = [
|
||||||
views.ChangePassword.as_view(),
|
views.ChangePassword.as_view(),
|
||||||
name="prefs-password",
|
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/delete/?$", views.DeleteUser.as_view(), name="prefs-delete"),
|
||||||
re_path(r"^preferences/block/?$", views.Block.as_view(), name="prefs-block"),
|
re_path(r"^preferences/block/?$", views.Block.as_view(), name="prefs-block"),
|
||||||
re_path(r"^block/(?P<user_id>\d+)/?$", views.Block.as_view()),
|
re_path(r"^block/(?P<user_id>\d+)/?$", views.Block.as_view()),
|
||||||
|
@ -524,7 +530,10 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
re_path(rf"{BOOK_PATH}/edit/?$", views.EditBook.as_view(), name="edit-book"),
|
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(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(r"^create-book/confirm/?$", views.ConfirmEditBook.as_view()),
|
||||||
re_path(rf"{BOOK_PATH}/editions(.json)?/?$", views.Editions.as_view()),
|
re_path(rf"{BOOK_PATH}/editions(.json)?/?$", views.Editions.as_view()),
|
||||||
re_path(
|
re_path(
|
||||||
|
|
|
@ -28,6 +28,7 @@ from .admin.user_admin import UserAdmin, UserAdminList
|
||||||
# user preferences
|
# user preferences
|
||||||
from .preferences.change_password import ChangePassword
|
from .preferences.change_password import ChangePassword
|
||||||
from .preferences.edit_user import EditUser
|
from .preferences.edit_user import EditUser
|
||||||
|
from .preferences.export import Export, export_user_book_data
|
||||||
from .preferences.delete_user import DeleteUser
|
from .preferences.delete_user import DeleteUser
|
||||||
from .preferences.block import Block, unblock
|
from .preferences.block import Block, unblock
|
||||||
|
|
||||||
|
@ -39,7 +40,12 @@ from .books.books import (
|
||||||
resolve_book,
|
resolve_book,
|
||||||
)
|
)
|
||||||
from .books.books import update_book_from_remote
|
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.editions import Editions, switch_edition
|
||||||
from .books.links import BookFileLinks, AddFileLink, delete_link
|
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.about import about, privacy, conduct
|
||||||
from .landing.landing import Home, Landing
|
from .landing.landing import Home, Landing
|
||||||
from .landing.login import Login, Logout
|
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
|
from .landing.password import PasswordResetRequest, PasswordReset
|
||||||
|
|
||||||
# shelves
|
# shelves
|
||||||
|
|
|
@ -96,6 +96,7 @@ class ManageInviteRequests(View):
|
||||||
"created_date",
|
"created_date",
|
||||||
"invite__times_used",
|
"invite__times_used",
|
||||||
"invite__invitees__created_date",
|
"invite__invitees__created_date",
|
||||||
|
"answer",
|
||||||
]
|
]
|
||||||
# pylint: disable=consider-using-f-string
|
# pylint: disable=consider-using-f-string
|
||||||
if not sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
|
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(
|
invite_request = get_object_or_404(
|
||||||
models.InviteRequest, id=request.POST.get("invite-request")
|
models.InviteRequest, id=request.POST.get("invite-request")
|
||||||
)
|
)
|
||||||
|
|
||||||
# only create a new invite if one doesn't exist already (resending)
|
# only create a new invite if one doesn't exist already (resending)
|
||||||
if not invite_request.invite:
|
if not invite_request.invite:
|
||||||
invite_request.invite = models.SiteInvite.objects.create(
|
invite_request.invite = models.SiteInvite.objects.create(
|
||||||
|
@ -170,10 +172,7 @@ class InviteRequest(View):
|
||||||
received = True
|
received = True
|
||||||
form.save()
|
form.save()
|
||||||
|
|
||||||
data = {
|
data = {"request_form": form, "request_received": received}
|
||||||
"request_form": form,
|
|
||||||
"request_received": received,
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, "landing/landing.html", data)
|
return TemplateResponse(request, "landing/landing.html", data)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
""" moderation via flagged posts and users """
|
""" moderation via flagged posts and users """
|
||||||
from django.contrib.auth.decorators import login_required, permission_required
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
|
from django.core.paginator import Paginator
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
|
@ -7,6 +8,7 @@ from django.utils.decorators import method_decorator
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
from bookwyrm import forms, models
|
from bookwyrm import forms, models
|
||||||
|
from bookwyrm.settings import PAGE_LENGTH
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
|
@ -34,10 +36,17 @@ class ReportsAdmin(View):
|
||||||
if username:
|
if username:
|
||||||
filters["user__username__icontains"] = username
|
filters["user__username__icontains"] = username
|
||||||
filters["resolved"] = resolved
|
filters["resolved"] = resolved
|
||||||
|
|
||||||
|
reports = models.Report.objects.filter(**filters)
|
||||||
|
paginated = Paginator(reports, PAGE_LENGTH)
|
||||||
|
page = paginated.get_page(request.GET.get("page"))
|
||||||
data = {
|
data = {
|
||||||
"resolved": resolved,
|
"resolved": resolved,
|
||||||
"server": server,
|
"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)
|
return TemplateResponse(request, "settings/reports/reports.html", data)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
""" the good people stuff! the authors! """
|
""" the good people stuff! the authors! """
|
||||||
from django.contrib.auth.decorators import login_required, permission_required
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db.models import Avg, Q
|
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
@ -27,9 +26,8 @@ class Author(View):
|
||||||
return ActivitypubResponse(author.to_activity())
|
return ActivitypubResponse(author.to_activity())
|
||||||
|
|
||||||
books = (
|
books = (
|
||||||
models.Work.objects.filter(Q(authors=author) | Q(editions__authors=author))
|
models.Work.objects.filter(editions__authors=author)
|
||||||
.annotate(Avg("editions__review__rating"))
|
.order_by("created_date")
|
||||||
.order_by("editions__review__rating__avg")
|
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
""" the good stuff! the books! """
|
""" the good stuff! the books! """
|
||||||
from re import sub
|
from re import sub, findall
|
||||||
from dateutil.parser import parse as dateparse
|
|
||||||
from django.contrib.auth.decorators import login_required, permission_required
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
from django.contrib.postgres.search import SearchRank, SearchVector
|
from django.contrib.postgres.search import SearchRank, SearchVector
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.http import HttpResponseBadRequest
|
from django.http import HttpResponseBadRequest
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.datastructures import MultiValueDictKeyError
|
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
from bookwyrm import book_search, forms, models
|
from bookwyrm import book_search, forms, models
|
||||||
|
@ -30,105 +29,27 @@ from .books import set_cover_from_url
|
||||||
class EditBook(View):
|
class EditBook(View):
|
||||||
"""edit a book"""
|
"""edit a book"""
|
||||||
|
|
||||||
def get(self, request, book_id=None):
|
def get(self, request, book_id):
|
||||||
"""info about a book"""
|
"""info about a book"""
|
||||||
book = None
|
book = get_edition(book_id)
|
||||||
if book_id:
|
if not book.description:
|
||||||
book = get_edition(book_id)
|
book.description = book.parent_work.description
|
||||||
if not book.description:
|
|
||||||
book.description = book.parent_work.description
|
|
||||||
data = {"book": book, "form": forms.EditionForm(instance=book)}
|
data = {"book": book, "form": forms.EditionForm(instance=book)}
|
||||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||||
|
|
||||||
# pylint: disable=too-many-locals
|
def post(self, request, book_id):
|
||||||
def post(self, request, book_id=None):
|
|
||||||
"""edit a book cool"""
|
"""edit a book cool"""
|
||||||
# returns None if no match is found
|
book = get_object_or_404(models.Edition, id=book_id)
|
||||||
book = models.Edition.objects.filter(id=book_id).first()
|
|
||||||
form = forms.EditionForm(request.POST, request.FILES, instance=book)
|
form = forms.EditionForm(request.POST, request.FILES, instance=book)
|
||||||
|
|
||||||
data = {"book": book, "form": form}
|
data = {"book": book, "form": form}
|
||||||
if not form.is_valid():
|
if not form.is_valid():
|
||||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||||
|
|
||||||
# filter out empty author fields
|
data = add_authors(request, data)
|
||||||
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]
|
|
||||||
|
|
||||||
# either of the above cases requires additional confirmation
|
# either of the above cases requires additional confirmation
|
||||||
if add_author or not book:
|
if data.get("add_author"):
|
||||||
# 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
|
|
||||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||||
|
|
||||||
remove_authors = request.POST.getlist("remove_authors")
|
remove_authors = request.POST.getlist("remove_authors")
|
||||||
|
@ -136,15 +57,156 @@ class EditBook(View):
|
||||||
book.authors.remove(author_id)
|
book.authors.remove(author_id)
|
||||||
|
|
||||||
book = form.save(commit=False)
|
book = form.save(commit=False)
|
||||||
|
|
||||||
url = request.POST.get("cover-url")
|
url = request.POST.get("cover-url")
|
||||||
if url:
|
if url:
|
||||||
image = set_cover_from_url(url)
|
image = set_cover_from_url(url)
|
||||||
if image:
|
if image:
|
||||||
book.cover.save(*image, save=False)
|
book.cover.save(*image, save=False)
|
||||||
|
|
||||||
book.save()
|
book.save()
|
||||||
return redirect(f"/book/{book.id}")
|
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(login_required, name="dispatch")
|
||||||
@method_decorator(
|
@method_decorator(
|
||||||
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
|
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
|
||||||
|
@ -168,6 +230,13 @@ class ConfirmEditBook(View):
|
||||||
# save book
|
# save book
|
||||||
book = form.save()
|
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
|
# get or create author as needed
|
||||||
for i in range(int(request.POST.get("author-match-count", 0))):
|
for i in range(int(request.POST.get("author-match-count", 0))):
|
||||||
match = request.POST.get(f"author_match-{i}")
|
match = request.POST.get(f"author_match-{i}")
|
||||||
|
@ -201,7 +270,7 @@ class ConfirmEditBook(View):
|
||||||
book.authors.add(author)
|
book.authors.add(author)
|
||||||
|
|
||||||
# create work, if needed
|
# create work, if needed
|
||||||
if not book_id:
|
if not book.parent_work:
|
||||||
work_match = request.POST.get("parent_work")
|
work_match = request.POST.get("parent_work")
|
||||||
if work_match and work_match != "0":
|
if work_match and work_match != "0":
|
||||||
work = get_object_or_404(models.Work, id=work_match)
|
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 import View
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import forms, models
|
||||||
from bookwyrm.activitypub import ActivitypubResponse
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
from bookwyrm.settings import PAGE_LENGTH
|
from bookwyrm.settings import PAGE_LENGTH
|
||||||
from bookwyrm.views.helpers import is_api_request
|
from bookwyrm.views.helpers import is_api_request
|
||||||
|
@ -65,6 +65,7 @@ class Editions(View):
|
||||||
page.number, on_each_side=2, on_ends=1
|
page.number, on_each_side=2, on_ends=1
|
||||||
),
|
),
|
||||||
"work": work,
|
"work": work,
|
||||||
|
"work_form": forms.EditionFromWorkForm(instance=work),
|
||||||
"languages": languages,
|
"languages": languages,
|
||||||
"formats": set(
|
"formats": set(
|
||||||
e.physical_format.lower() for e in editions if e.physical_format
|
e.physical_format.lower() for e in editions if e.physical_format
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import re
|
import re
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.db import IntegrityError
|
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
|
from bookwyrm.models.relationship import clear_cache
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
get_user_from_username,
|
get_user_from_username,
|
||||||
handle_remote_webfinger,
|
handle_remote_webfinger,
|
||||||
|
@ -22,17 +22,17 @@ def follow(request):
|
||||||
"""follow another user, here or abroad"""
|
"""follow another user, here or abroad"""
|
||||||
username = request.POST["user"]
|
username = request.POST["user"]
|
||||||
to_follow = get_user_from_username(request.user, username)
|
to_follow = get_user_from_username(request.user, username)
|
||||||
|
clear_cache(request.user, to_follow)
|
||||||
|
|
||||||
try:
|
follow_request, created = models.UserFollowRequest.objects.get_or_create(
|
||||||
models.UserFollowRequest.objects.create(
|
user_subject=request.user,
|
||||||
user_subject=request.user,
|
user_object=to_follow,
|
||||||
user_object=to_follow,
|
)
|
||||||
)
|
|
||||||
except IntegrityError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if request.GET.get("next"):
|
if not created:
|
||||||
return redirect(request.GET.get("next", "/"))
|
# 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)
|
return redirect(to_follow.local_path)
|
||||||
|
|
||||||
|
@ -49,14 +49,14 @@ def unfollow(request):
|
||||||
user_subject=request.user, user_object=to_unfollow
|
user_subject=request.user, user_object=to_unfollow
|
||||||
).delete()
|
).delete()
|
||||||
except models.UserFollows.DoesNotExist:
|
except models.UserFollows.DoesNotExist:
|
||||||
pass
|
clear_cache(request.user, to_unfollow)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
models.UserFollowRequest.objects.get(
|
models.UserFollowRequest.objects.get(
|
||||||
user_subject=request.user, user_object=to_unfollow
|
user_subject=request.user, user_object=to_unfollow
|
||||||
).delete()
|
).delete()
|
||||||
except models.UserFollowRequest.DoesNotExist:
|
except models.UserFollowRequest.DoesNotExist:
|
||||||
pass
|
clear_cache(request.user, to_unfollow)
|
||||||
|
|
||||||
# this is handled with ajax so it shouldn't really matter
|
# this is handled with ajax so it shouldn't really matter
|
||||||
return redirect(request.headers.get("Referer", "/"))
|
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.template.response import TemplateResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views import View
|
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 django.views.decorators.debug import sensitive_variables, sensitive_post_parameters
|
||||||
|
|
||||||
from bookwyrm import emailing, forms, models
|
from bookwyrm import emailing, forms, models
|
||||||
|
@ -129,12 +128,22 @@ class ConfirmEmail(View):
|
||||||
return ConfirmEmailCode().get(request, code)
|
return ConfirmEmailCode().get(request, code)
|
||||||
|
|
||||||
|
|
||||||
@require_POST
|
class ResendConfirmEmail(View):
|
||||||
def resend_link(request):
|
"""you probably didn't get the email because celery is slow but you can try this"""
|
||||||
"""resend confirmation link"""
|
|
||||||
email = request.POST.get("email")
|
def get(self, request, error=False):
|
||||||
user = get_object_or_404(models.User, email=email)
|
"""resend link landing page"""
|
||||||
emailing.email_confirmation_email(user)
|
return TemplateResponse(request, "confirm_email/resend.html", {"error": error})
|
||||||
return TemplateResponse(
|
|
||||||
request, "confirm_email/confirm_email.html", {"valid": True}
|
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
|
3
bw-dev
3
bw-dev
|
@ -123,6 +123,7 @@ case "$CMD" in
|
||||||
git checkout l10n_main locale/no_NO
|
git checkout l10n_main locale/no_NO
|
||||||
git checkout l10n_main locale/pt_PT
|
git checkout l10n_main locale/pt_PT
|
||||||
git checkout l10n_main locale/pt_BR
|
git checkout l10n_main locale/pt_BR
|
||||||
|
git checkout l10n_main locale/ro_RO
|
||||||
git checkout l10n_main locale/sv_SE
|
git checkout l10n_main locale/sv_SE
|
||||||
git checkout l10n_main locale/zh_Hans
|
git checkout l10n_main locale/zh_Hans
|
||||||
git checkout l10n_main locale/zh_Hant
|
git checkout l10n_main locale/zh_Hant
|
||||||
|
@ -163,7 +164,7 @@ case "$CMD" in
|
||||||
update)
|
update)
|
||||||
git pull
|
git pull
|
||||||
docker-compose build
|
docker-compose build
|
||||||
./update.sh
|
# ./update.sh
|
||||||
runweb python manage.py migrate
|
runweb python manage.py migrate
|
||||||
runweb python manage.py collectstatic --no-input
|
runweb python manage.py collectstatic --no-input
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
|
|
|
@ -13,6 +13,7 @@ CELERY_BROKER_URL = f"redis://:{REDIS_BROKER_PASSWORD}@{REDIS_BROKER_HOST}:{REDI
|
||||||
CELERY_RESULT_BACKEND = f"redis://:{REDIS_BROKER_PASSWORD}@{REDIS_BROKER_HOST}:{REDIS_BROKER_PORT}/{REDIS_BROKER_DB_INDEX}"
|
CELERY_RESULT_BACKEND = f"redis://:{REDIS_BROKER_PASSWORD}@{REDIS_BROKER_HOST}:{REDIS_BROKER_PORT}/{REDIS_BROKER_DB_INDEX}"
|
||||||
|
|
||||||
CELERY_DEFAULT_QUEUE = "low_priority"
|
CELERY_DEFAULT_QUEUE = "low_priority"
|
||||||
|
CELERY_CREATE_MISSING_QUEUES = True
|
||||||
|
|
||||||
CELERY_ACCEPT_CONTENT = ["json"]
|
CELERY_ACCEPT_CONTENT = ["json"]
|
||||||
CELERY_TASK_SERIALIZER = "json"
|
CELERY_TASK_SERIALIZER = "json"
|
||||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
BIN
locale/ro_RO/LC_MESSAGES/django.mo
Normal file
BIN
locale/ro_RO/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
5119
locale/ro_RO/LC_MESSAGES/django.po
Normal file
5119
locale/ro_RO/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
37
update.sh
Executable file
37
update.sh
Executable file
|
@ -0,0 +1,37 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# determine inital and target versions
|
||||||
|
initial_version="`./bw-dev runweb python manage.py instance_version --current`"
|
||||||
|
target_version="`./bw-dev runweb python manage.py instance_version --target`"
|
||||||
|
|
||||||
|
initial_version="`echo $initial_version | tail -n 1 | xargs`"
|
||||||
|
target_version="`echo $target_version | tail -n 1 | xargs`"
|
||||||
|
if [[ "$initial_version" = "$target_version" ]]; then
|
||||||
|
echo "Already up to date; version $initial_version"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "---------------------------------------"
|
||||||
|
echo "Updating from version: $initial_version"
|
||||||
|
echo ".......... to version: $target_version"
|
||||||
|
echo "---------------------------------------"
|
||||||
|
|
||||||
|
function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }
|
||||||
|
|
||||||
|
# execute scripts between initial and target
|
||||||
|
for version in `ls -A updates/ | sort -V `; do
|
||||||
|
if version_gt $initial_version $version; then
|
||||||
|
# too early
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if version_gt $version $target_version; then
|
||||||
|
# too late
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "Running tasks for version $version"
|
||||||
|
./updates/$version
|
||||||
|
done
|
||||||
|
|
||||||
|
./bw-dev runweb python manage.py instance_version --update
|
||||||
|
echo "✨ ----------- Done! --------------- ✨"
|
Loading…
Reference in a new issue