Merge branch 'main' into list-privacy
This commit is contained in:
commit
74368ab159
116 changed files with 15287 additions and 3453 deletions
|
@ -105,16 +105,6 @@ def init_connectors():
|
|||
)
|
||||
|
||||
|
||||
def init_federated_servers():
|
||||
"""big no to nazis"""
|
||||
built_in_blocks = ["gab.ai", "gab.com"]
|
||||
for server in built_in_blocks:
|
||||
models.FederatedServer.objects.create(
|
||||
server_name=server,
|
||||
status="blocked",
|
||||
)
|
||||
|
||||
|
||||
def init_settings():
|
||||
"""info about the instance"""
|
||||
models.SiteSettings.objects.create(
|
||||
|
@ -163,7 +153,6 @@ class Command(BaseCommand):
|
|||
"group",
|
||||
"permission",
|
||||
"connector",
|
||||
"federatedserver",
|
||||
"settings",
|
||||
"linkdomain",
|
||||
]
|
||||
|
@ -176,8 +165,6 @@ class Command(BaseCommand):
|
|||
init_permissions()
|
||||
if not limit or limit == "connector":
|
||||
init_connectors()
|
||||
if not limit or limit == "federatedserver":
|
||||
init_federated_servers()
|
||||
if not limit or limit == "settings":
|
||||
init_settings()
|
||||
if not limit or limit == "linkdomain":
|
||||
|
|
38
bookwyrm/migrations/0147_alter_user_preferred_language.py
Normal file
38
bookwyrm/migrations/0147_alter_user_preferred_language.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# Generated by Django 3.2.12 on 2022-03-26 16:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0146_auto_20220316_2352"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="preferred_language",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("en-us", "English"),
|
||||
("de-de", "Deutsch (German)"),
|
||||
("es-es", "Español (Spanish)"),
|
||||
("gl-es", "Galego (Galician)"),
|
||||
("it-it", "Italiano (Italian)"),
|
||||
("fr-fr", "Français (French)"),
|
||||
("lt-lt", "Lietuvių (Lithuanian)"),
|
||||
("no-no", "Norsk (Norwegian)"),
|
||||
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
|
||||
("pt-pt", "Português Europeu (European Portuguese)"),
|
||||
("ro-ro", "Română (Romanian)"),
|
||||
("sv-se", "Svenska (Swedish)"),
|
||||
("zh-hans", "简体中文 (Simplified Chinese)"),
|
||||
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
||||
],
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
39
bookwyrm/migrations/0148_alter_user_preferred_language.py
Normal file
39
bookwyrm/migrations/0148_alter_user_preferred_language.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
# Generated by Django 3.2.12 on 2022-03-31 14:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0147_alter_user_preferred_language"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="preferred_language",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("en-us", "English"),
|
||||
("de-de", "Deutsch (German)"),
|
||||
("es-es", "Español (Spanish)"),
|
||||
("gl-es", "Galego (Galician)"),
|
||||
("it-it", "Italiano (Italian)"),
|
||||
("fi-fi", "Suomi (Finnish)"),
|
||||
("fr-fr", "Français (French)"),
|
||||
("lt-lt", "Lietuvių (Lithuanian)"),
|
||||
("no-no", "Norsk (Norwegian)"),
|
||||
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
|
||||
("pt-pt", "Português Europeu (European Portuguese)"),
|
||||
("ro-ro", "Română (Romanian)"),
|
||||
("sv-se", "Svenska (Swedish)"),
|
||||
("zh-hans", "简体中文 (Simplified Chinese)"),
|
||||
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
||||
],
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -8,6 +8,7 @@ from django.db.models import Q
|
|||
from django.dispatch import receiver
|
||||
from django.http import Http404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.text import slugify
|
||||
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from .fields import RemoteIdField
|
||||
|
@ -35,10 +36,11 @@ class BookWyrmModel(models.Model):
|
|||
remote_id = RemoteIdField(null=True, activitypub_field="id")
|
||||
|
||||
def get_remote_id(self):
|
||||
"""generate a url that resolves to the local object"""
|
||||
"""generate the url that resolves to the local object, without a slug"""
|
||||
base_path = f"https://{DOMAIN}"
|
||||
if hasattr(self, "user"):
|
||||
base_path = f"{base_path}{self.user.local_path}"
|
||||
|
||||
model_name = type(self).__name__.lower()
|
||||
return f"{base_path}/{model_name}/{self.id}"
|
||||
|
||||
|
@ -49,8 +51,20 @@ class BookWyrmModel(models.Model):
|
|||
|
||||
@property
|
||||
def local_path(self):
|
||||
"""how to link to this object in the local app"""
|
||||
return self.get_remote_id().replace(f"https://{DOMAIN}", "")
|
||||
"""how to link to this object in the local app, with a slug"""
|
||||
local = self.get_remote_id().replace(f"https://{DOMAIN}", "")
|
||||
|
||||
name = None
|
||||
if hasattr(self, "name_field"):
|
||||
name = getattr(self, self.name_field)
|
||||
elif hasattr(self, "name"):
|
||||
name = self.name
|
||||
|
||||
if name:
|
||||
slug = slugify(name)
|
||||
local = f"{local}/s/{slug}"
|
||||
|
||||
return local
|
||||
|
||||
def raise_visible_to_user(self, viewer):
|
||||
"""is a user authorized to view an object?"""
|
||||
|
|
|
@ -125,7 +125,7 @@ class ActivitypubFieldMixin:
|
|||
"""model_field_name to activitypubFieldName"""
|
||||
if self.activitypub_field:
|
||||
return self.activitypub_field
|
||||
name = self.name.split(".")[-1]
|
||||
name = self.name.rsplit(".", maxsplit=1)[-1]
|
||||
components = name.split("_")
|
||||
return components[0] + "".join(x.title() for x in components[1:])
|
||||
|
||||
|
@ -389,7 +389,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
|
|||
self.alt_field = alt_field
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
# pylint: disable=arguments-differ,arguments-renamed
|
||||
def set_field_from_activity(self, instance, data, save=True, overwrite=True):
|
||||
"""helper function for assinging a value to the field"""
|
||||
value = getattr(data, self.get_activitypub_field())
|
||||
|
|
|
@ -39,15 +39,14 @@ class UserRelationship(BookWyrmModel):
|
|||
|
||||
def save(self, *args, **kwargs):
|
||||
"""clear the template cache"""
|
||||
# invalidate the template cache
|
||||
cache.delete_many(
|
||||
[
|
||||
f"relationship-{self.user_subject.id}-{self.user_object.id}",
|
||||
f"relationship-{self.user_object.id}-{self.user_subject.id}",
|
||||
]
|
||||
)
|
||||
clear_cache(self.user_subject, self.user_object)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""clear the template cache"""
|
||||
clear_cache(self.user_subject, self.user_object)
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
"""relationships should be unique"""
|
||||
|
||||
|
@ -90,7 +89,9 @@ class UserFollows(ActivityMixin, UserRelationship):
|
|||
user_object=self.user_subject,
|
||||
)
|
||||
).exists():
|
||||
raise IntegrityError()
|
||||
raise IntegrityError(
|
||||
"Attempting to follow blocked user", self.user_subject, self.user_object
|
||||
)
|
||||
# don't broadcast this type of relationship -- accepts and requests
|
||||
# are handled by the UserFollowRequest model
|
||||
super().save(*args, broadcast=False, **kwargs)
|
||||
|
@ -98,11 +99,12 @@ class UserFollows(ActivityMixin, UserRelationship):
|
|||
@classmethod
|
||||
def from_request(cls, follow_request):
|
||||
"""converts a follow request into a follow relationship"""
|
||||
return cls.objects.create(
|
||||
obj, _ = cls.objects.get_or_create(
|
||||
user_subject=follow_request.user_subject,
|
||||
user_object=follow_request.user_object,
|
||||
remote_id=follow_request.remote_id,
|
||||
)
|
||||
return obj
|
||||
|
||||
|
||||
class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||
|
@ -133,7 +135,9 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
|||
user_object=self.user_subject,
|
||||
)
|
||||
).exists():
|
||||
raise IntegrityError()
|
||||
raise IntegrityError(
|
||||
"Attempting to follow blocked user", self.user_subject, self.user_object
|
||||
)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
if broadcast and self.user_subject.local and not self.user_object.local:
|
||||
|
@ -174,7 +178,8 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
|||
|
||||
with transaction.atomic():
|
||||
UserFollows.from_request(self)
|
||||
self.delete()
|
||||
if self.id:
|
||||
self.delete()
|
||||
|
||||
def reject(self):
|
||||
"""generate a Reject for this follow request"""
|
||||
|
@ -207,3 +212,13 @@ class UserBlocks(ActivityMixin, UserRelationship):
|
|||
Q(user_subject=self.user_subject, user_object=self.user_object)
|
||||
| Q(user_subject=self.user_object, user_object=self.user_subject)
|
||||
).delete()
|
||||
|
||||
|
||||
def clear_cache(user_subject, user_object):
|
||||
"""clear relationship cache"""
|
||||
cache.delete_many(
|
||||
[
|
||||
f"relationship-{user_subject.id}-{user_object.id}",
|
||||
f"relationship-{user_object.id}-{user_subject.id}",
|
||||
]
|
||||
)
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.db import models
|
|||
from django.utils import timezone
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
|
||||
from .base_model import BookWyrmModel
|
||||
from . import fields
|
||||
|
@ -65,6 +66,11 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
|||
identifier = self.identifier or self.get_identifier()
|
||||
return f"{base_path}/books/{identifier}"
|
||||
|
||||
@property
|
||||
def local_path(self):
|
||||
"""No slugs"""
|
||||
return self.get_remote_id().replace(f"https://{DOMAIN}", "")
|
||||
|
||||
def raise_not_deletable(self, viewer):
|
||||
"""don't let anyone delete a default shelf"""
|
||||
super().raise_not_deletable(viewer)
|
||||
|
|
|
@ -284,11 +284,13 @@ LANGUAGES = [
|
|||
("es-es", _("Español (Spanish)")),
|
||||
("gl-es", _("Galego (Galician)")),
|
||||
("it-it", _("Italiano (Italian)")),
|
||||
("fi-fi", _("Suomi (Finnish)")),
|
||||
("fr-fr", _("Français (French)")),
|
||||
("lt-lt", _("Lietuvių (Lithuanian)")),
|
||||
("no-no", _("Norsk (Norwegian)")),
|
||||
("pt-br", _("Português do Brasil (Brazilian Portuguese)")),
|
||||
("pt-pt", _("Português Europeu (European Portuguese)")),
|
||||
("ro-ro", _("Română (Romanian)")),
|
||||
("sv-se", _("Svenska (Swedish)")),
|
||||
("zh-hans", _("简体中文 (Simplified Chinese)")),
|
||||
("zh-hant", _("繁體中文 (Traditional Chinese)")),
|
||||
|
|
|
@ -36,6 +36,18 @@ body {
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: $scrollbar-thumb;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: $scrollbar-track;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
margin: 0;
|
||||
|
|
|
@ -114,3 +114,17 @@ details[open] summary .details-close {
|
|||
padding-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/** Navbar details
|
||||
******************************************************************************/
|
||||
|
||||
#navbar-dropdown .navbar-item {
|
||||
color: $text;
|
||||
font-size: 0.875rem;
|
||||
padding: 0.375rem 3rem 0.375rem 1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#navbar-dropdown .navbar-item:hover {
|
||||
background-color: $background-secondary;
|
||||
}
|
||||
|
|
|
@ -23,3 +23,8 @@
|
|||
.has-background-tertiary {
|
||||
background-color: $background-tertiary !important;
|
||||
}
|
||||
|
||||
/* Workaround for dark theme as .has-text-black doesn't give desired effect. */
|
||||
.has-text-default {
|
||||
color: $text !important;
|
||||
}
|
||||
|
|
|
@ -28,6 +28,8 @@ $background-body: rgb(24, 27, 28);
|
|||
$background-secondary: rgb(28, 30, 32);
|
||||
$background-tertiary: rgb(32, 34, 36);
|
||||
$modal-background-background-color: rgba($black, 0.8);
|
||||
$scrollbar-track: $background-secondary;
|
||||
$scrollbar-thumb: $light;
|
||||
|
||||
/* highlight colors */
|
||||
$primary-highlight: $primary;
|
||||
|
@ -51,6 +53,7 @@ $link-hover: $white-bis;
|
|||
$link-hover-border: #51595d;
|
||||
$link-focus: $white-bis;
|
||||
$link-active: $white-bis;
|
||||
$link-light: #0d1c26;
|
||||
|
||||
/* bulma overrides */
|
||||
$background: $background-secondary;
|
||||
|
@ -81,6 +84,13 @@ $progress-value-background-color: $border-light;
|
|||
$family-primary: $family-sans-serif;
|
||||
$family-secondary: $family-sans-serif;
|
||||
|
||||
.has-text-muted {
|
||||
color: $grey-lighter !important;
|
||||
}
|
||||
|
||||
.has-text-more-muted {
|
||||
color: $grey-light !important;
|
||||
}
|
||||
|
||||
@import "../bookwyrm.scss";
|
||||
@import "../vendor/icons.css";
|
||||
|
|
|
@ -19,6 +19,8 @@ $scheme-main: $white-bis;
|
|||
$background-body: $white;
|
||||
$background-secondary: $white-ter;
|
||||
$background-tertiary: $white-bis;
|
||||
$scrollbar-track: $background-secondary;
|
||||
$scrollbar-thumb: $grey-lighter;
|
||||
|
||||
/* highlight colors */
|
||||
$primary-highlight: $primary-light;
|
||||
|
@ -55,5 +57,13 @@ $invisible-overlay-background-color: rgba($scheme-invert, 0.66);
|
|||
$family-primary: $family-sans-serif;
|
||||
$family-secondary: $family-sans-serif;
|
||||
|
||||
.has-text-muted {
|
||||
color: $grey-dark !important;
|
||||
}
|
||||
|
||||
.has-text-more-muted {
|
||||
color: $grey !important;
|
||||
}
|
||||
|
||||
@import "../bookwyrm.scss";
|
||||
@import "../vendor/icons.css";
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
<p>
|
||||
{% url "conduct" as coc_path %}
|
||||
{% blocktrans trimmed with site_name=site.name %}
|
||||
{{ site_name }}'s moderators and administrators keep the site up and running, enforce the <a href="coc_path">code of conduct</a>, and respond when users report spam and bad behavior.
|
||||
{{ site_name }}'s moderators and administrators keep the site up and running, enforce the <a href="{{ coc_path }}">code of conduct</a>, and respond when users report spam and bad behavior.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</header>
|
||||
|
|
|
@ -284,7 +284,7 @@
|
|||
{% if user_statuses.review_count or user_statuses.comment_count or user_statuses.quotation_count %}
|
||||
<nav class="tabs">
|
||||
<ul>
|
||||
{% url 'book' book.id as tab_url %}
|
||||
{% url 'book' book.id book.name|slugify as tab_url %}
|
||||
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
||||
<a href="{{ tab_url }}#reviews">{% trans "Reviews" %} ({{ review_count }})</a>
|
||||
</li>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
<div class="column my-3-mobile ml-3-tablet mr-auto">
|
||||
<h2 class="title is-5 mb-1">
|
||||
<a href="{{ book.local_path }}" class="has-text-black">
|
||||
<a href="{{ book.local_path }}" class="has-text-default">
|
||||
{{ book|book_title }}
|
||||
</a>
|
||||
</h2>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="{% url 'book' book.id %}">{{ book|book_title }}</a></li>
|
||||
<li><a href="{% url 'book' book.id book.name|slugify %}">{{ book|book_title }}</a></li>
|
||||
<li class="is-active">
|
||||
<a href="#" aria-current="page">
|
||||
{% trans "Edit links" %}
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
{% endif %}
|
||||
<p>
|
||||
<a href="https://joinbookwyrm.com/">
|
||||
{% trans "Join Bookwyrm" %}
|
||||
{% trans "Join BookWyrm" %}
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
{% for membership in group.memberships.all %}
|
||||
{% with member=membership.user %}
|
||||
<div class="box has-text-centered is-shadowless has-background-tertiary my-2 mx-2 member_{{ member.id }}">
|
||||
<a href="{{ member.local_path }}" class="has-text-black">
|
||||
<a href="{{ member.local_path }}" class="has-text-default">
|
||||
{% include 'snippets/avatar.html' with user=member large=True %}
|
||||
<span title="{{ member.display_name }}" class="is-block is-6 has-text-weight-bold">{{ member.display_name|truncatechars:10 }}</span>
|
||||
<span title="@{{ member|username }}" class="is-block pb-3">@{{ member|username|truncatechars:8 }}</span>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<div class="column is-flex is-flex-grow-0">
|
||||
{% for user in suggested_users %}
|
||||
<div class="box has-text-centered is-shadowless has-background-tertiary m-2">
|
||||
<a href="{{ user.local_path }}" class="has-text-black">
|
||||
<a href="{{ user.local_path }}" class="has-text-default">
|
||||
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||
<span title="{{ user.display_name }}" class="is-block is-6 has-text-weight-bold">{{ user.display_name|truncatechars:10 }}</span>
|
||||
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
|
||||
|
|
|
@ -90,64 +90,8 @@
|
|||
|
||||
<div class="navbar-end">
|
||||
{% if request.user.is_authenticated %}
|
||||
<div class="navbar-item mt-3 py-0 has-dropdown is-hoverable">
|
||||
<a
|
||||
href="{{ request.user.local_path }}"
|
||||
class="navbar-link pulldown-menu"
|
||||
role="button"
|
||||
aria-expanded="false"
|
||||
tabindex="0"
|
||||
aria-haspopup="true"
|
||||
aria-controls="navbar-dropdown"
|
||||
>
|
||||
{% include 'snippets/avatar.html' with user=request.user %}
|
||||
<span class="ml-2">{{ request.user.display_name }}</span>
|
||||
</a>
|
||||
<ul class="navbar-dropdown" id="navbar_dropdown">
|
||||
<li>
|
||||
<a href="{% url 'directory' %}" class="navbar-item">
|
||||
{% trans "Directory" %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'user-shelves' request.user.localname %}" class="navbar-item">
|
||||
{% trans 'Your Books' %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'direct-messages' %}" class="navbar-item">
|
||||
{% trans "Direct Messages" %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'prefs-profile' %}" class="navbar-item">
|
||||
{% trans 'Settings' %}
|
||||
</a>
|
||||
</li>
|
||||
{% if perms.bookwyrm.create_invites or perms.moderate_user %}
|
||||
<li class="navbar-divider" role="presentation"> </li>
|
||||
{% endif %}
|
||||
{% if perms.bookwyrm.create_invites and not site.allow_registration %}
|
||||
<li>
|
||||
<a href="{% url 'settings-invite-requests' %}" class="navbar-item">
|
||||
{% trans 'Invites' %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.bookwyrm.moderate_user %}
|
||||
<li>
|
||||
<a href="{% url 'settings-dashboard' %}" class="navbar-item">
|
||||
{% trans 'Admin' %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="navbar-divider" role="presentation"> </li>
|
||||
<li>
|
||||
<a href="{% url 'logout' %}" class="navbar-item">
|
||||
{% trans 'Log out' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="navbar-item mt-3 py-0">
|
||||
{% include 'user_menu.html' %}
|
||||
</div>
|
||||
<div class="navbar-item mt-3 py-0">
|
||||
<a href="{% url 'notifications' %}" class="tags has-addons">
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="{% url 'lists' %}">{% trans "Lists" %}</a></li>
|
||||
<li><a href="{% url 'list' list.id %}">{{ list.name|truncatechars:30 }}</a></li>
|
||||
<li><a href="{% url 'list' list_id=list.id slug=list.name|slugify %}">{{ list.name|truncatechars:30 }}</a></li>
|
||||
<li class="is-active">
|
||||
<a href="#" aria-current="page">
|
||||
{% trans "Curate" %}
|
||||
|
|
|
@ -180,7 +180,7 @@
|
|||
<h2 class="title is-5">
|
||||
{% trans "Sort List" %}
|
||||
</h2>
|
||||
<form name="sort" action="{% url 'list' list.id %}" method="GET" class="block">
|
||||
<form name="sort" action="{% url 'list' list_id=list.id slug=list.name|slugify %}" method="GET" class="block">
|
||||
<div class="field">
|
||||
<label class="label" for="id_sort_by">{% trans "Sort By" %}</label>
|
||||
<div class="select is-fullwidth">
|
||||
|
@ -207,7 +207,7 @@
|
|||
{% trans "Suggest Books" %}
|
||||
{% endif %}
|
||||
</h2>
|
||||
<form name="search" action="{% url 'list' list.id %}" method="GET" class="block">
|
||||
<form name="search" action="{% url 'list' list_id=list.id slug=list.name|slugify %}" method="GET" class="block">
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<input aria-label="{% trans 'Search for a book' %}" class="input" type="text" name="q" placeholder="{% trans 'Search for a book' %}" value="{{ query }}">
|
||||
|
@ -221,7 +221,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{% if query %}
|
||||
<p class="help"><a href="{% url 'list' list.id %}">{% trans "Clear search" %}</a></p>
|
||||
<p class="help"><a href="{% url 'list' list_id=list.id slug=list.name|slugify %}">{% trans "Clear search" %}</a></p>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% if not suggested_books %}
|
||||
|
|
|
@ -47,12 +47,12 @@
|
|||
|
||||
|
||||
{% block preview %}
|
||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-grey-dark{% endif %}">
|
||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-muted{% endif %}">
|
||||
<div class="columns">
|
||||
<div class="column is-clipped">
|
||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||
</div>
|
||||
<div class="column is-narrow has-grey-dark">
|
||||
<div class="column is-narrow has-text-muted">
|
||||
{{ related_status.published_date|timesince }}
|
||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||
</div>
|
||||
|
|
|
@ -47,12 +47,12 @@
|
|||
|
||||
|
||||
{% block preview %}
|
||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-grey-dark{% endif %}">
|
||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-muted{% endif %}">
|
||||
<div class="columns">
|
||||
<div class="column is-clipped">
|
||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||
</div>
|
||||
<div class="column is-narrow has-grey-dark">
|
||||
<div class="column is-narrow has-text-muted">
|
||||
{{ related_status.published_date|timesince }}
|
||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% load notification_page_tags %}
|
||||
{% related_status notification as related_status %}
|
||||
<div class="notification {% if notification.id in unread %}has-background-primary{% endif %}">
|
||||
<div class="columns is-mobile {% if notification.id in unread %}has-text-white{% else %}has-text-grey{% endif %}">
|
||||
<div class="columns is-mobile {% if notification.id in unread %}has-text-white{% else %}has-text-more-muted{% endif %}">
|
||||
<div class="column is-narrow is-size-3">
|
||||
<a class="icon" href="{% block primary_link %}{% endblock %}">
|
||||
{% block icon %}{% endblock %}
|
||||
|
|
|
@ -48,12 +48,12 @@
|
|||
|
||||
|
||||
{% block preview %}
|
||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-black{% endif %}">
|
||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-default{% endif %}">
|
||||
<div class="columns">
|
||||
<div class="column is-clipped">
|
||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||
</div>
|
||||
<div class="column is-narrow has-text-black">
|
||||
<div class="column is-narrow has-text-default">
|
||||
{{ related_status.published_date|timesince }}
|
||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||
</div>
|
||||
|
|
|
@ -51,12 +51,12 @@
|
|||
|
||||
|
||||
{% block preview %}
|
||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-black{% endif %}">
|
||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-default{% endif %}">
|
||||
<div class="columns">
|
||||
<div class="column is-clipped">
|
||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||
</div>
|
||||
<div class="column is-narrow has-text-black">
|
||||
<div class="column is-narrow has-text-default">
|
||||
{{ related_status.published_date|timesince }}
|
||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||
</div>
|
||||
|
|
22
bookwyrm/templates/preferences/export.html
Normal file
22
bookwyrm/templates/preferences/export.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
{% extends 'preferences/layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "CSV Export" %}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
{% trans "CSV Export" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
<div class="block content">
|
||||
<p class="notification">
|
||||
{% trans "Your export will include all the books on your shelves, books you have reviewed, and books with reading activity." %}
|
||||
</p>
|
||||
<p>
|
||||
<a href="{% url 'prefs-export-file' %}" class="button">
|
||||
<span class="icon icon-download" aria-hidden="true"></span>
|
||||
<span>Download file</span>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -24,6 +24,17 @@
|
|||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Delete Account" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 class="menu-label">{% trans "Data" %}</h2>
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
{% url 'import' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Import" %}</a>
|
||||
</li>
|
||||
<li>
|
||||
{% url 'prefs-export' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "CSV export" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 class="menu-label">{% trans "Relationships" %}</h2>
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
||||
|
||||
{% block filter_fields %}
|
||||
{% include 'settings/federation/software_filter.html' %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -12,6 +12,9 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
|
||||
{% include 'settings/federation/instance_filters.html' %}
|
||||
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
{% url 'settings-federation' status='federated' as url %}
|
||||
|
@ -36,6 +39,10 @@
|
|||
{% trans "Date added" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Last updated" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="updated_date" sort=sort text=text %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Software" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="application_type" sort=sort text=text %}
|
||||
|
@ -43,12 +50,12 @@
|
|||
<th>
|
||||
{% trans "Users" %}
|
||||
</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
</tr>
|
||||
{% for server in servers %}
|
||||
<tr>
|
||||
<td><a href="{% url 'settings-federated-server' server.id %}">{{ server.server_name }}</a></td>
|
||||
<td>{{ server.created_date }}</td>
|
||||
<td>{{ server.created_date|date:'Y-m-d' }}</td>
|
||||
<td>{{ server.updated_date|date:'Y-m-d' }}</td>
|
||||
<td>
|
||||
{% if server.application_type %}
|
||||
{{ server.application_type }}
|
||||
|
@ -56,7 +63,6 @@
|
|||
{% endif %}
|
||||
</td>
|
||||
<td>{{ server.user_set.count }}</td>
|
||||
<td>{{ server.get_status_display }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not servers %}
|
||||
|
|
19
bookwyrm/templates/settings/federation/software_filter.html
Normal file
19
bookwyrm/templates/settings/federation/software_filter.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
{% extends 'snippets/filters_panel/filter_field.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block filter %}
|
||||
<label class="label" for="id_server">{% trans "Software" %}</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select name="application_type">
|
||||
<option value="">-----</option>
|
||||
{% for option in software_options %}
|
||||
{% if option %}
|
||||
<option value="{{ option }}">{{ option }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -14,6 +14,11 @@
|
|||
{% blocktrans with username=goal.user.display_name read_count=progress.count|intcomma goal_count=goal.goal|intcomma path=goal.local_path %}{{ username }} has read <a href="{{ path }}">{{ read_count }} of {{ goal_count}} books</a>.{% endblocktrans %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<progress class="progress is-large" value="{{ progress.count }}" max="{{ goal.goal }}" aria-hidden="true">{{ progress.percent }}%</progress>
|
||||
<progress
|
||||
class="progress is-large is-primary"
|
||||
value="{{ progress.count }}"
|
||||
max="{{ goal.goal }}"
|
||||
aria-hidden="true"
|
||||
>{{ progress.percent }}%</progress>
|
||||
|
||||
{% endwith %}
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
<article class="column ml-3-tablet my-3-mobile">
|
||||
<article class="column ml-3-tablet my-3-mobile is-clipped">
|
||||
{% if status_type == 'Review' %}
|
||||
<header class="mb-2">
|
||||
<h3
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<div class="card-footer-item">
|
||||
{% trans "Reply" as button_text %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with controls_text="show_comment" controls_uid=status.id text=button_text icon_with_text="comment" class="is-small is-light is-transparent toggle-button" focus="id_content_reply" %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with controls_text="show_comment" controls_uid=status.id text=button_text icon_with_text="comment" class="is-small is-light toggle-button" focus="id_content_reply" %}
|
||||
</div>
|
||||
<div class="card-footer-item">
|
||||
{% include 'snippets/boost_button.html' with status=status %}
|
||||
|
@ -42,7 +42,7 @@
|
|||
</div>
|
||||
{% if not moderation_mode %}
|
||||
<div class="card-footer-item">
|
||||
{% include 'snippets/status/status_options.html' with class="is-small is-light is-transparent" right=True %}
|
||||
{% include 'snippets/status/status_options.html' with class="is-small is-light" right=True %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{% for user in suggested_users %}
|
||||
<div class="column is-flex is-flex-grow-0">
|
||||
<div class="box has-text-centered is-shadowless has-background-tertiary m-0">
|
||||
<a href="{{ user.local_path }}" class="has-text-black">
|
||||
<a href="{{ user.local_path }}" class="has-text-default">
|
||||
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||
<span title="{{ user.display_name }}" class="is-block is-6 has-text-weight-bold">{{ user.display_name|truncatechars:10 }}</span>
|
||||
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
|
||||
|
|
77
bookwyrm/templates/user_menu.html
Normal file
77
bookwyrm/templates/user_menu.html
Normal file
|
@ -0,0 +1,77 @@
|
|||
{% load utilities %}
|
||||
{% load i18n %}
|
||||
|
||||
<details class="dropdown" id="navbar-dropdown">
|
||||
<summary
|
||||
class="is-relative pulldown-menu dropdown-trigger"
|
||||
aria-label="{% trans 'View profile and more' %}"
|
||||
role="button"
|
||||
aria-haspopup="menu"
|
||||
>
|
||||
<span class="">
|
||||
{% include 'snippets/avatar.html' with user=request.user %}
|
||||
<span class="ml-2">{{ request.user.display_name }}</span>
|
||||
</span>
|
||||
<span class="icon icon-arrow-down is-hidden-mobile" aria-hidden="true"></span>
|
||||
</summary>
|
||||
|
||||
<div class="dropdown-menu">
|
||||
<ul
|
||||
class="dropdown-content"
|
||||
role="menu"
|
||||
>
|
||||
<li role="menuitem">
|
||||
<a href="{% url 'user-feed' request.user.localname %}" class="navbar-item">
|
||||
{% trans "Profile" %}
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<a href="{% url 'directory' %}" class="navbar-item">
|
||||
{% trans "Directory" %}
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<a href="{% url 'user-shelves' request.user.localname %}" class="navbar-item">
|
||||
{% trans 'Your Books' %}
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<a href="{% url 'direct-messages' %}" class="navbar-item">
|
||||
{% trans "Direct Messages" %}
|
||||
</a>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
<a href="{% url 'prefs-profile' %}" class="navbar-item">
|
||||
{% trans 'Settings' %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if perms.bookwyrm.create_invites or perms.moderate_user %}
|
||||
<li class="navbar-divider" role="presentation" aria-hidden="true"> </li>
|
||||
{% endif %}
|
||||
|
||||
{% if perms.bookwyrm.create_invites and not site.allow_registration %}
|
||||
<li role="menuitem">
|
||||
<a href="{% url 'settings-invite-requests' %}" class="navbar-item">
|
||||
{% trans 'Invites' %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.bookwyrm.moderate_user %}
|
||||
<li role="menuitem">
|
||||
<a href="{% url 'settings-dashboard' %}" class="navbar-item">
|
||||
{% trans 'Admin' %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="navbar-divider" role="presentation" aria-hidden="true"> </li>
|
||||
|
||||
<li role="menuitem">
|
||||
<a href="{% url 'logout' %}" class="navbar-item">
|
||||
{% trans 'Log out' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
""" import ALL the tests """
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -4,7 +4,10 @@ from bookwyrm import models
|
|||
|
||||
|
||||
class Author(TestCase):
|
||||
"""serialize author tests"""
|
||||
|
||||
def setUp(self):
|
||||
"""initial data"""
|
||||
self.book = models.Edition.objects.create(
|
||||
title="Example Edition",
|
||||
remote_id="https://example.com/book/1",
|
||||
|
@ -16,6 +19,7 @@ class Author(TestCase):
|
|||
)
|
||||
|
||||
def test_serialize_model(self):
|
||||
"""check presense of author fields"""
|
||||
activity = self.author.to_activity()
|
||||
self.assertEqual(activity["id"], self.author.remote_id)
|
||||
self.assertIsInstance(activity["aliases"], list)
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -101,6 +101,7 @@ class AbstractConnector(TestCase):
|
|||
result = self.connector.get_or_create_book(
|
||||
f"https://{DOMAIN}/book/{self.book.id}"
|
||||
)
|
||||
|
||||
self.assertEqual(models.Book.objects.count(), 1)
|
||||
self.assertEqual(result, self.book)
|
||||
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -90,7 +90,6 @@ class InitDB(TestCase):
|
|||
self.assertEqual(Group.objects.count(), 3)
|
||||
self.assertTrue(Permission.objects.exists())
|
||||
self.assertEqual(models.Connector.objects.count(), 3)
|
||||
self.assertEqual(models.FederatedServer.objects.count(), 2)
|
||||
self.assertEqual(models.SiteSettings.objects.count(), 1)
|
||||
self.assertEqual(models.LinkDomain.objects.count(), 5)
|
||||
|
||||
|
@ -102,7 +101,6 @@ class InitDB(TestCase):
|
|||
# everything should have been called
|
||||
self.assertEqual(Group.objects.count(), 3)
|
||||
self.assertEqual(models.Connector.objects.count(), 0)
|
||||
self.assertEqual(models.FederatedServer.objects.count(), 0)
|
||||
self.assertEqual(models.SiteSettings.objects.count(), 0)
|
||||
self.assertEqual(models.LinkDomain.objects.count(), 0)
|
||||
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -38,7 +38,7 @@ class BaseModel(TestCase):
|
|||
|
||||
def test_remote_id(self):
|
||||
"""these should be generated"""
|
||||
self.test_model.id = 1
|
||||
self.test_model.id = 1 # pylint: disable=invalid-name
|
||||
expected = self.test_model.get_remote_id()
|
||||
self.assertEqual(expected, f"https://{DOMAIN}/bookwyrmtestmodel/1")
|
||||
|
||||
|
|
|
@ -162,6 +162,7 @@ class ModelFields(TestCase):
|
|||
class TestActivity(ActivityObject):
|
||||
"""real simple mock"""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
to: List[str]
|
||||
cc: List[str]
|
||||
id: str = "http://hi.com"
|
||||
|
|
|
@ -17,7 +17,7 @@ class User(TestCase):
|
|||
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
|
||||
self.user = models.User.objects.create_user(
|
||||
"mouse@%s" % DOMAIN,
|
||||
f"mouse@{DOMAIN}",
|
||||
"mouse@mouse.mouse",
|
||||
"mouseword",
|
||||
local=True,
|
||||
|
@ -107,7 +107,7 @@ class User(TestCase):
|
|||
def test_get_or_create_remote_server(self):
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://%s/.well-known/nodeinfo" % DOMAIN,
|
||||
f"https://{DOMAIN}/.well-known/nodeinfo",
|
||||
json={"links": [{"href": "http://www.example.com"}, {}]},
|
||||
)
|
||||
responses.add(
|
||||
|
@ -124,7 +124,7 @@ class User(TestCase):
|
|||
@responses.activate
|
||||
def test_get_or_create_remote_server_no_wellknown(self):
|
||||
responses.add(
|
||||
responses.GET, "https://%s/.well-known/nodeinfo" % DOMAIN, status=404
|
||||
responses.GET, f"https://{DOMAIN}/.well-known/nodeinfo", status=404
|
||||
)
|
||||
|
||||
server = models.user.get_or_create_remote_server(DOMAIN)
|
||||
|
@ -136,7 +136,7 @@ class User(TestCase):
|
|||
def test_get_or_create_remote_server_no_links(self):
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://%s/.well-known/nodeinfo" % DOMAIN,
|
||||
f"https://{DOMAIN}/.well-known/nodeinfo",
|
||||
json={"links": [{"href": "http://www.example.com"}, {}]},
|
||||
)
|
||||
responses.add(responses.GET, "http://www.example.com", status=404)
|
||||
|
@ -150,7 +150,7 @@ class User(TestCase):
|
|||
def test_get_or_create_remote_server_unknown_format(self):
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://%s/.well-known/nodeinfo" % DOMAIN,
|
||||
f"https://{DOMAIN}/.well-known/nodeinfo",
|
||||
json={"links": [{"href": "http://www.example.com"}, {}]},
|
||||
)
|
||||
responses.add(responses.GET, "http://www.example.com", json={"fish": "salmon"})
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -64,8 +64,8 @@ class Signature(TestCase):
|
|||
|
||||
def send(self, signature, now, data, digest):
|
||||
"""test request"""
|
||||
c = Client()
|
||||
return c.post(
|
||||
client = Client()
|
||||
return client.post(
|
||||
urlsplit(self.rat.inbox).path,
|
||||
data=data,
|
||||
content_type="application/json",
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -61,7 +61,7 @@ class InboxActivities(TestCase):
|
|||
self.assertEqual(models.Notification.objects.count(), 0)
|
||||
activity = {
|
||||
"type": "Announce",
|
||||
"id": "%s/boost" % self.status.remote_id,
|
||||
"id": f"{self.status.remote_id}/boost",
|
||||
"actor": self.remote_user.remote_id,
|
||||
"object": self.status.remote_id,
|
||||
"to": ["https://www.w3.org/ns/activitystreams#public"],
|
||||
|
@ -94,7 +94,7 @@ class InboxActivities(TestCase):
|
|||
self.assertEqual(models.Notification.objects.count(), 0)
|
||||
activity = {
|
||||
"type": "Announce",
|
||||
"id": "%s/boost" % self.status.remote_id,
|
||||
"id": f"{self.status.remote_id}/boost",
|
||||
"actor": self.remote_user.remote_id,
|
||||
"object": "https://remote.com/status/1",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#public"],
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from . import *
|
||||
# pylint: disable=missing-module-docstring
|
||||
from . import * # pylint: disable=import-self
|
||||
|
|
|
@ -66,7 +66,7 @@ class AuthorViews(TestCase):
|
|||
def test_author_page_edition_author(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Author.as_view()
|
||||
another_book = models.Edition.objects.create(
|
||||
models.Edition.objects.create(
|
||||
title="Example Edition",
|
||||
remote_id="https://example.com/book/1",
|
||||
parent_work=self.work,
|
||||
|
|
69
bookwyrm/tests/views/test_export.py
Normal file
69
bookwyrm/tests/views/test_export.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.http import StreamingHttpResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
@patch("bookwyrm.activitystreams.add_book_statuses_task.delay")
|
||||
@patch("bookwyrm.activitystreams.add_status_task.delay")
|
||||
@patch("bookwyrm.activitystreams.populate_stream_task.delay")
|
||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
|
||||
class ExportViews(TestCase):
|
||||
"""viewing and creating statuses"""
|
||||
|
||||
def setUp(self):
|
||||
"""we need basic test data and mocks"""
|
||||
self.factory = RequestFactory()
|
||||
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||
):
|
||||
self.local_user = models.User.objects.create_user(
|
||||
"mouse@local.com",
|
||||
"mouse@mouse.com",
|
||||
"mouseword",
|
||||
local=True,
|
||||
localname="mouse",
|
||||
remote_id="https://example.com/users/mouse",
|
||||
)
|
||||
self.work = models.Work.objects.create(title="Test Work")
|
||||
self.book = models.Edition.objects.create(
|
||||
title="Test Book",
|
||||
remote_id="https://example.com/book/1",
|
||||
parent_work=self.work,
|
||||
isbn_13="9781234567890",
|
||||
bnf_id="beep",
|
||||
)
|
||||
|
||||
def tst_export_get(self, *_):
|
||||
"""request export"""
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
result = views.Export.as_view()(request)
|
||||
validate_html(result.render())
|
||||
|
||||
def test_export_file(self, *_):
|
||||
"""simple export"""
|
||||
models.ShelfBook.objects.create(
|
||||
shelf=self.local_user.shelf_set.first(),
|
||||
user=self.local_user,
|
||||
book=self.book,
|
||||
)
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
export = views.export_user_book_data(request)
|
||||
self.assertIsInstance(export, StreamingHttpResponse)
|
||||
self.assertEqual(export.status_code, 200)
|
||||
result = list(export.streaming_content)
|
||||
# pylint: disable=line-too-long
|
||||
self.assertEqual(
|
||||
result[0],
|
||||
b"title,author_text,remote_id,openlibrary_key,inventaire_id,librarything_key,goodreads_key,bnf_id,viaf,wikidata,asin,isbn_10,isbn_13,oclc_number,rating,review_name,review_cw,review_content\r\n",
|
||||
)
|
||||
expected = f"Test Book,,{self.book.remote_id},,,,,beep,,,,123456789X,9781234567890,,,,,\r\n"
|
||||
self.assertEqual(result[1].decode("utf-8"), expected)
|
|
@ -139,7 +139,7 @@ class ViewsHelpers(TestCase):
|
|||
}
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://example.com/.well-known/webfinger?resource=acct:%s" % username,
|
||||
f"https://example.com/.well-known/webfinger?resource=acct:{username}",
|
||||
json=wellknown,
|
||||
status=200,
|
||||
)
|
||||
|
|
|
@ -83,7 +83,7 @@ class UserViews(TestCase):
|
|||
def test_user_page_domain(self):
|
||||
"""when the user domain has dashes in it"""
|
||||
with patch("bookwyrm.models.user.set_remote_server"):
|
||||
self.remote_user = models.User.objects.create_user(
|
||||
models.User.objects.create_user(
|
||||
"nutria",
|
||||
"",
|
||||
"nutriaword",
|
||||
|
|
|
@ -391,6 +391,9 @@ urlpatterns = [
|
|||
re_path(
|
||||
r"^group/(?P<group_id>\d+)(.json)?/?$", views.Group.as_view(), name="group"
|
||||
),
|
||||
re_path(
|
||||
rf"^group/(?P<group_id>\d+){regex.SLUG}/?$", views.Group.as_view(), name="group"
|
||||
),
|
||||
re_path(
|
||||
r"^group/delete/(?P<group_id>\d+)/?$", views.delete_group, name="delete-group"
|
||||
),
|
||||
|
@ -417,7 +420,10 @@ urlpatterns = [
|
|||
re_path(rf"{USER_PATH}/lists/?$", views.UserLists.as_view(), name="user-lists"),
|
||||
re_path(r"^list/?$", views.Lists.as_view(), name="lists"),
|
||||
re_path(r"^list/saved/?$", views.SavedLists.as_view(), name="saved-lists"),
|
||||
re_path(r"^list/(?P<list_id>\d+)(.json)?/?$", views.List.as_view(), name="list"),
|
||||
re_path(r"^list/(?P<list_id>\d+)(\.json)?/?$", views.List.as_view(), name="list"),
|
||||
re_path(
|
||||
rf"^list/(?P<list_id>\d+){regex.SLUG}/?$", views.List.as_view(), name="list"
|
||||
),
|
||||
re_path(
|
||||
r"^list/(?P<list_id>\d+)/item/(?P<list_item>\d+)/?$",
|
||||
views.ListItem.as_view(),
|
||||
|
@ -475,12 +481,19 @@ urlpatterns = [
|
|||
views.ChangePassword.as_view(),
|
||||
name="prefs-password",
|
||||
),
|
||||
re_path(r"^preferences/export/?$", views.Export.as_view(), name="prefs-export"),
|
||||
re_path(
|
||||
r"^preferences/export/file/?$",
|
||||
views.export_user_book_data,
|
||||
name="prefs-export-file",
|
||||
),
|
||||
re_path(r"^preferences/delete/?$", views.DeleteUser.as_view(), name="prefs-delete"),
|
||||
re_path(r"^preferences/block/?$", views.Block.as_view(), name="prefs-block"),
|
||||
re_path(r"^block/(?P<user_id>\d+)/?$", views.Block.as_view()),
|
||||
re_path(r"^unblock/(?P<user_id>\d+)/?$", views.unblock),
|
||||
# statuses
|
||||
re_path(rf"{STATUS_PATH}(.json)?/?$", views.Status.as_view(), name="status"),
|
||||
re_path(rf"{STATUS_PATH}{regex.SLUG}/?$", views.Status.as_view(), name="status"),
|
||||
re_path(rf"{STATUS_PATH}/activity/?$", views.Status.as_view(), name="status"),
|
||||
re_path(
|
||||
rf"{STATUS_PATH}/replies(.json)?/?$", views.Replies.as_view(), name="replies"
|
||||
|
@ -517,6 +530,7 @@ urlpatterns = [
|
|||
re_path(r"^unboost/(?P<status_id>\d+)/?$", views.Unboost.as_view()),
|
||||
# books
|
||||
re_path(rf"{BOOK_PATH}(.json)?/?$", views.Book.as_view(), name="book"),
|
||||
re_path(rf"{BOOK_PATH}{regex.SLUG}/?$", views.Book.as_view(), name="book"),
|
||||
re_path(
|
||||
rf"{BOOK_PATH}/(?P<user_statuses>review|comment|quote)/?$",
|
||||
views.Book.as_view(),
|
||||
|
@ -574,6 +588,11 @@ urlpatterns = [
|
|||
re_path(
|
||||
r"^author/(?P<author_id>\d+)(.json)?/?$", views.Author.as_view(), name="author"
|
||||
),
|
||||
re_path(
|
||||
rf"^author/(?P<author_id>\d+){regex.SLUG}/?$",
|
||||
views.Author.as_view(),
|
||||
name="author",
|
||||
),
|
||||
re_path(
|
||||
r"^author/(?P<author_id>\d+)/edit/?$",
|
||||
views.EditAuthor.as_view(),
|
||||
|
|
|
@ -6,5 +6,6 @@ STRICT_LOCALNAME = r"@[a-zA-Z_\-\.0-9]+"
|
|||
USERNAME = rf"{LOCALNAME}(@{DOMAIN})?"
|
||||
STRICT_USERNAME = rf"\B{STRICT_LOCALNAME}(@{DOMAIN})?\b"
|
||||
FULL_USERNAME = rf"{LOCALNAME}@{DOMAIN}\b"
|
||||
SLUG = r"/s/(?P<slug>[-_a-z0-9]*)"
|
||||
# should match (BookWyrm/1.0.0; or (BookWyrm/99.1.2;
|
||||
BOOKWYRM_USER_AGENT = r"\(BookWyrm/[0-9]+\.[0-9]+\.[0-9]+;"
|
||||
|
|
|
@ -28,6 +28,7 @@ from .admin.user_admin import UserAdmin, UserAdminList
|
|||
# user preferences
|
||||
from .preferences.change_password import ChangePassword
|
||||
from .preferences.edit_user import EditUser
|
||||
from .preferences.export import Export, export_user_book_data
|
||||
from .preferences.delete_user import DeleteUser
|
||||
from .preferences.block import Block, unblock
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ class Dashboard(View):
|
|||
status="pending"
|
||||
).count(),
|
||||
"invite_requests": models.InviteRequest.objects.filter(
|
||||
ignored=False, invite_sent=False
|
||||
ignored=False, invite__isnull=True
|
||||
).count(),
|
||||
"user_stats": user_chart.get_chart(start, end, interval),
|
||||
"status_stats": status_chart.get_chart(start, end, interval),
|
||||
|
|
|
@ -25,14 +25,23 @@ class Federation(View):
|
|||
|
||||
def get(self, request, status="federated"):
|
||||
"""list of servers"""
|
||||
servers = models.FederatedServer.objects.filter(status=status)
|
||||
|
||||
filters = {}
|
||||
if software := request.GET.get("application_type"):
|
||||
filters["application_type"] = software
|
||||
|
||||
servers = models.FederatedServer.objects.filter(status=status, **filters)
|
||||
|
||||
sort = request.GET.get("sort")
|
||||
sort_fields = ["created_date", "application_type", "server_name"]
|
||||
# pylint: disable=consider-using-f-string
|
||||
if not sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
|
||||
sort_fields = [
|
||||
"created_date",
|
||||
"updated_date",
|
||||
"application_type",
|
||||
"server_name",
|
||||
]
|
||||
if not sort in sort_fields + [f"-{f}" for f in sort_fields]:
|
||||
sort = "-created_date"
|
||||
servers = servers.order_by(sort)
|
||||
servers = servers.order_by(sort, "-created_date")
|
||||
|
||||
paginated = Paginator(servers, PAGE_LENGTH)
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
|
@ -49,6 +58,9 @@ class Federation(View):
|
|||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
"sort": sort,
|
||||
"software_options": models.FederatedServer.objects.values_list(
|
||||
"application_type", flat=True
|
||||
).distinct(),
|
||||
"form": forms.ServerForm(),
|
||||
}
|
||||
return TemplateResponse(request, "settings/federation/instance_list.html", data)
|
||||
|
|
|
@ -11,20 +11,24 @@ from bookwyrm import forms, models
|
|||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.connectors import connector_manager
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.views.helpers import is_api_request
|
||||
from bookwyrm.views.helpers import is_api_request, maybe_redirect_local_path
|
||||
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
class Author(View):
|
||||
"""this person wrote a book"""
|
||||
|
||||
def get(self, request, author_id):
|
||||
# pylint: disable=unused-argument
|
||||
def get(self, request, author_id, slug=None):
|
||||
"""landing page for an author"""
|
||||
author = get_object_or_404(models.Author, id=author_id)
|
||||
|
||||
if is_api_request(request):
|
||||
return ActivitypubResponse(author.to_activity())
|
||||
|
||||
if redirect_local_path := maybe_redirect_local_path(request, author):
|
||||
return redirect_local_path
|
||||
|
||||
books = (
|
||||
models.Work.objects.filter(editions__authors=author)
|
||||
.order_by("created_date")
|
||||
|
|
|
@ -15,14 +15,14 @@ from bookwyrm.activitypub import ActivitypubResponse
|
|||
from bookwyrm.connectors import connector_manager, ConnectorException
|
||||
from bookwyrm.connectors.abstract_connector import get_image
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.views.helpers import is_api_request
|
||||
from bookwyrm.views.helpers import is_api_request, maybe_redirect_local_path
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class Book(View):
|
||||
"""a book! this is the stuff"""
|
||||
|
||||
def get(self, request, book_id, user_statuses=False, update_error=False):
|
||||
def get(self, request, book_id, **kwargs):
|
||||
"""info about a book"""
|
||||
if is_api_request(request):
|
||||
book = get_object_or_404(
|
||||
|
@ -30,7 +30,11 @@ class Book(View):
|
|||
)
|
||||
return ActivitypubResponse(book.to_activity())
|
||||
|
||||
user_statuses = user_statuses if request.user.is_authenticated else False
|
||||
user_statuses = (
|
||||
kwargs.get("user_statuses", False)
|
||||
if request.user.is_authenticated
|
||||
else False
|
||||
)
|
||||
|
||||
# it's safe to use this OR because edition and work and subclasses of the same
|
||||
# table, so they never have clashing IDs
|
||||
|
@ -46,6 +50,11 @@ class Book(View):
|
|||
if not book or not book.parent_work:
|
||||
raise Http404()
|
||||
|
||||
if redirect_local_path := not user_statuses and maybe_redirect_local_path(
|
||||
request, book
|
||||
):
|
||||
return redirect_local_path
|
||||
|
||||
# all reviews for all editions of the book
|
||||
reviews = models.Review.privacy_filter(request.user).filter(
|
||||
book__parent_work__editions=book
|
||||
|
@ -80,7 +89,7 @@ class Book(View):
|
|||
else None,
|
||||
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
|
||||
"lists": lists,
|
||||
"update_error": update_error,
|
||||
"update_error": kwargs.get("update_error", False),
|
||||
}
|
||||
|
||||
if request.user.is_authenticated:
|
||||
|
|
|
@ -15,7 +15,7 @@ from bookwyrm.activitypub import ActivitypubResponse
|
|||
from bookwyrm.settings import PAGE_LENGTH, STREAMS
|
||||
from bookwyrm.suggested_users import suggested_users
|
||||
from .helpers import filter_stream_by_status_type, get_user_from_username
|
||||
from .helpers import is_api_request, is_bookwyrm_request
|
||||
from .helpers import is_api_request, is_bookwyrm_request, maybe_redirect_local_path
|
||||
from .annual_summary import get_annual_summary_year
|
||||
|
||||
|
||||
|
@ -113,7 +113,8 @@ class DirectMessage(View):
|
|||
class Status(View):
|
||||
"""get posting"""
|
||||
|
||||
def get(self, request, username, status_id):
|
||||
# pylint: disable=unused-argument
|
||||
def get(self, request, username, status_id, slug=None):
|
||||
"""display a particular status (and replies, etc)"""
|
||||
user = get_user_from_username(request.user, username)
|
||||
status = get_object_or_404(
|
||||
|
@ -130,6 +131,9 @@ class Status(View):
|
|||
status.to_activity(pure=not is_bookwyrm_request(request))
|
||||
)
|
||||
|
||||
if redirect_local_path := maybe_redirect_local_path(request, status):
|
||||
return redirect_local_path
|
||||
|
||||
visible_thread = (
|
||||
models.Status.privacy_filter(request.user)
|
||||
.filter(thread_id=status.thread_id)
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
import urllib.parse
|
||||
import re
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db import IntegrityError
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.models.relationship import clear_cache
|
||||
from .helpers import (
|
||||
get_user_from_username,
|
||||
handle_remote_webfinger,
|
||||
|
@ -22,17 +22,17 @@ def follow(request):
|
|||
"""follow another user, here or abroad"""
|
||||
username = request.POST["user"]
|
||||
to_follow = get_user_from_username(request.user, username)
|
||||
clear_cache(request.user, to_follow)
|
||||
|
||||
try:
|
||||
models.UserFollowRequest.objects.create(
|
||||
user_subject=request.user,
|
||||
user_object=to_follow,
|
||||
)
|
||||
except IntegrityError:
|
||||
pass
|
||||
follow_request, created = models.UserFollowRequest.objects.get_or_create(
|
||||
user_subject=request.user,
|
||||
user_object=to_follow,
|
||||
)
|
||||
|
||||
if request.GET.get("next"):
|
||||
return redirect(request.GET.get("next", "/"))
|
||||
if not created:
|
||||
# this request probably failed to connect with the remote
|
||||
# that means we should save to trigger a re-broadcast
|
||||
follow_request.save()
|
||||
|
||||
return redirect(to_follow.local_path)
|
||||
|
||||
|
@ -49,14 +49,14 @@ def unfollow(request):
|
|||
user_subject=request.user, user_object=to_unfollow
|
||||
).delete()
|
||||
except models.UserFollows.DoesNotExist:
|
||||
pass
|
||||
clear_cache(request.user, to_unfollow)
|
||||
|
||||
try:
|
||||
models.UserFollowRequest.objects.get(
|
||||
user_subject=request.user, user_object=to_unfollow
|
||||
).delete()
|
||||
except models.UserFollowRequest.DoesNotExist:
|
||||
pass
|
||||
clear_cache(request.user, to_unfollow)
|
||||
|
||||
# this is handled with ajax so it shouldn't really matter
|
||||
return redirect(request.headers.get("Referer", "/"))
|
||||
|
|
|
@ -14,17 +14,22 @@ from django.db.models.functions import Greatest
|
|||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.suggested_users import suggested_users
|
||||
from .helpers import get_user_from_username
|
||||
from .helpers import get_user_from_username, maybe_redirect_local_path
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class Group(View):
|
||||
"""group page"""
|
||||
|
||||
def get(self, request, group_id):
|
||||
# pylint: disable=unused-argument
|
||||
def get(self, request, group_id, slug=None):
|
||||
"""display a group"""
|
||||
|
||||
group = get_object_or_404(models.Group, id=group_id)
|
||||
group.raise_visible_to_user(request.user)
|
||||
|
||||
if redirect_local_path := maybe_redirect_local_path(request, group):
|
||||
return redirect_local_path
|
||||
|
||||
lists = (
|
||||
models.List.privacy_filter(request.user)
|
||||
.filter(group=group)
|
||||
|
@ -80,7 +85,8 @@ class Group(View):
|
|||
class UserGroups(View):
|
||||
"""a user's groups page"""
|
||||
|
||||
def get(self, request, username):
|
||||
# pylint: disable=unused-argument
|
||||
def get(self, request, username, slug=None):
|
||||
"""display a group"""
|
||||
user = get_user_from_username(request.user, username)
|
||||
groups = (
|
||||
|
|
|
@ -8,6 +8,7 @@ from dateutil.parser import ParserError
|
|||
from requests import HTTPError
|
||||
from django.db.models import Q
|
||||
from django.conf import settings as django_settings
|
||||
from django.shortcuts import redirect
|
||||
from django.http import Http404
|
||||
from django.utils import translation
|
||||
|
||||
|
@ -201,3 +202,21 @@ def filter_stream_by_status_type(activities, allowed_types=None):
|
|||
)
|
||||
|
||||
return activities
|
||||
|
||||
|
||||
def maybe_redirect_local_path(request, model):
|
||||
"""
|
||||
if the request had an invalid path, return a permanent redirect response to the
|
||||
correct one, including a slug if any.
|
||||
if path is valid, returns False.
|
||||
"""
|
||||
|
||||
# don't redirect empty path for unit tests which currently have this
|
||||
if request.path in ("/", model.local_path):
|
||||
return False
|
||||
|
||||
new_path = model.local_path
|
||||
if len(request.GET) > 0:
|
||||
new_path = f"{model.local_path}?{request.GET.urlencode()}"
|
||||
|
||||
return redirect(new_path, permanent=True)
|
||||
|
|
|
@ -91,9 +91,12 @@ class ConfirmEmailCode(View):
|
|||
def get(self, request, code): # pylint: disable=unused-argument
|
||||
"""you got the code! good work"""
|
||||
settings = models.SiteSettings.get()
|
||||
if request.user.is_authenticated or not settings.require_confirm_email:
|
||||
if request.user.is_authenticated:
|
||||
return redirect("/")
|
||||
|
||||
if not settings.require_confirm_email:
|
||||
return redirect("login")
|
||||
|
||||
# look up the user associated with this code
|
||||
try:
|
||||
user = models.User.objects.get(confirmation_code=code)
|
||||
|
|
|
@ -18,21 +18,27 @@ from django.views.decorators.http import require_POST
|
|||
from bookwyrm import book_search, forms, models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.views.helpers import is_api_request
|
||||
from bookwyrm.views.helpers import is_api_request, maybe_redirect_local_path
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class List(View):
|
||||
"""book list page"""
|
||||
|
||||
def get(self, request, list_id, add_failed=False, add_succeeded=False):
|
||||
def get(self, request, list_id, **kwargs):
|
||||
"""display a book list"""
|
||||
add_failed = kwargs.get("add_failed", False)
|
||||
add_succeeded = kwargs.get("add_succeeded", False)
|
||||
|
||||
book_list = get_object_or_404(models.List, id=list_id)
|
||||
book_list.raise_visible_to_user(request.user)
|
||||
|
||||
if is_api_request(request):
|
||||
return ActivitypubResponse(book_list.to_activity(**request.GET))
|
||||
|
||||
if r := maybe_redirect_local_path(request, book_list):
|
||||
return r
|
||||
|
||||
query = request.GET.get("q")
|
||||
suggestions = None
|
||||
|
||||
|
|
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
|
|
@ -24,6 +24,8 @@ class Search(View):
|
|||
def get(self, request):
|
||||
"""that search bar up top"""
|
||||
query = request.GET.get("q")
|
||||
# check if query is isbn
|
||||
query = isbn_check(query)
|
||||
min_confidence = request.GET.get("min_confidence", 0)
|
||||
search_type = request.GET.get("type")
|
||||
search_remote = (
|
||||
|
@ -123,3 +125,35 @@ def list_search(query, viewer, *_):
|
|||
)
|
||||
.order_by("-similarity")
|
||||
), None
|
||||
|
||||
|
||||
def isbn_check(query):
|
||||
"""isbn10 or isbn13 check, if so remove separators"""
|
||||
if query:
|
||||
su_num = re.sub(r"(?<=\d)\D(?=\d|[xX])", "", query)
|
||||
if len(su_num) == 13 and su_num.isdecimal():
|
||||
# Multiply every other digit by 3
|
||||
# Add these numbers and the other digits
|
||||
product = sum(int(ch) for ch in su_num[::2]) + sum(
|
||||
int(ch) * 3 for ch in su_num[1::2]
|
||||
)
|
||||
if product % 10 == 0:
|
||||
return su_num
|
||||
elif (
|
||||
len(su_num) == 10
|
||||
and su_num[:-1].isdecimal()
|
||||
and (su_num[-1].isdecimal() or su_num[-1].lower() == "x")
|
||||
):
|
||||
product = 0
|
||||
# Iterate through code_string
|
||||
for i in range(9):
|
||||
# for each character, multiply by a different decreasing number: 10 - x
|
||||
product = product + int(su_num[i]) * (10 - i)
|
||||
# Handle last character
|
||||
if su_num[9].lower() == "x":
|
||||
product += 10
|
||||
else:
|
||||
product += int(su_num[9])
|
||||
if product % 11 == 0:
|
||||
return su_num
|
||||
return query
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue