mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-05-20 09:18:19 +00:00
Compare commits
49 commits
bae28ac425
...
f09c26405e
Author | SHA1 | Date | |
---|---|---|---|
f09c26405e | |||
c4b21ee258 | |||
ad830dd885 | |||
366c647585 | |||
4f58b11330 | |||
609bc15406 | |||
c42db40a63 | |||
3aefbb548e | |||
baea105c18 | |||
c73d1fff6a | |||
3d183a393f | |||
f24fdf73b5 | |||
839ab2fafd | |||
637f19b208 | |||
031223104f | |||
6684d60526 | |||
cca58023ed | |||
bf5c08dbf3 | |||
be872ed672 | |||
98724dfa47 | |||
74e2103e3a | |||
a0d15ccec0 | |||
8dc412c4cb | |||
d5fb21f330 | |||
cb3fd0cfc1 | |||
5340ed35de | |||
1e14d635bc | |||
09d857e6fb | |||
1d8bd2be89 | |||
cd29b44807 | |||
6976cb4876 | |||
63d6486fc9 | |||
5b8e083bcd | |||
e3ca543f8e | |||
975c3ba9aa | |||
a2e41faf69 | |||
3754af7bc5 | |||
7b388ae972 | |||
7a25869e3f | |||
5d09c54e57 | |||
b7ba6f1a36 | |||
e144ce19fa | |||
da4214ad61 | |||
d3d5f1bec6 | |||
339298cb3d | |||
f665aea665 | |||
c142e383c9 | |||
4ae0dbde92 | |||
856737e19c |
16
.env.example
16
.env.example
|
@ -16,6 +16,11 @@ DEFAULT_LANGUAGE="English"
|
||||||
## Leave unset to allow all hosts
|
## Leave unset to allow all hosts
|
||||||
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
|
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
|
||||||
|
|
||||||
|
# Specify when the site is served from a port that is not the default
|
||||||
|
# for the protocol (80 for HTTP or 443 for HTTPS).
|
||||||
|
# Probably only necessary in development.
|
||||||
|
# PORT=1333
|
||||||
|
|
||||||
MEDIA_ROOT=images/
|
MEDIA_ROOT=images/
|
||||||
|
|
||||||
# Database configuration
|
# Database configuration
|
||||||
|
@ -78,10 +83,13 @@ S3_SIGNED_URL_EXPIRY=900
|
||||||
# Commented are example values if you use a non-AWS, S3-compatible service
|
# Commented are example values if you use a non-AWS, S3-compatible service
|
||||||
# AWS S3 should work with only AWS_STORAGE_BUCKET_NAME and AWS_S3_REGION_NAME
|
# AWS S3 should work with only AWS_STORAGE_BUCKET_NAME and AWS_S3_REGION_NAME
|
||||||
# non-AWS S3-compatible services will need AWS_STORAGE_BUCKET_NAME,
|
# non-AWS S3-compatible services will need AWS_STORAGE_BUCKET_NAME,
|
||||||
# along with both AWS_S3_CUSTOM_DOMAIN and AWS_S3_ENDPOINT_URL
|
# along with both AWS_S3_CUSTOM_DOMAIN and AWS_S3_ENDPOINT_URL.
|
||||||
|
# AWS_S3_URL_PROTOCOL must end in ":" and defaults to the same protocol as
|
||||||
|
# the BookWyrm instance ("http:" or "https:", based on USE_SSL).
|
||||||
|
|
||||||
# AWS_STORAGE_BUCKET_NAME= # "example-bucket-name"
|
# AWS_STORAGE_BUCKET_NAME= # "example-bucket-name"
|
||||||
# AWS_S3_CUSTOM_DOMAIN=None # "example-bucket-name.s3.fr-par.scw.cloud"
|
# AWS_S3_CUSTOM_DOMAIN=None # "example-bucket-name.s3.fr-par.scw.cloud"
|
||||||
|
# AWS_S3_URL_PROTOCOL=None # "http:"
|
||||||
# AWS_S3_REGION_NAME=None # "fr-par"
|
# AWS_S3_REGION_NAME=None # "fr-par"
|
||||||
# AWS_S3_ENDPOINT_URL=None # "https://s3.fr-par.scw.cloud"
|
# AWS_S3_ENDPOINT_URL=None # "https://s3.fr-par.scw.cloud"
|
||||||
|
|
||||||
|
@ -136,9 +144,9 @@ HTTP_X_FORWARDED_PROTO=false
|
||||||
TWO_FACTOR_LOGIN_VALIDITY_WINDOW=2
|
TWO_FACTOR_LOGIN_VALIDITY_WINDOW=2
|
||||||
TWO_FACTOR_LOGIN_MAX_SECONDS=60
|
TWO_FACTOR_LOGIN_MAX_SECONDS=60
|
||||||
|
|
||||||
# Additional hosts to allow in the Content-Security-Policy, "self" (should be DOMAIN)
|
# Additional hosts to allow in the Content-Security-Policy, "self" (should be
|
||||||
# and AWS_S3_CUSTOM_DOMAIN (if used) are added by default.
|
# DOMAIN with optionally ":" + PORT) and AWS_S3_CUSTOM_DOMAIN (if used) are
|
||||||
# Value should be a comma-separated list of host names.
|
# added by default. Value should be a comma-separated list of host names.
|
||||||
CSP_ADDITIONAL_HOSTS=
|
CSP_ADDITIONAL_HOSTS=
|
||||||
|
|
||||||
# Time before being logged out (in seconds)
|
# Time before being logged out (in seconds)
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -38,3 +38,6 @@ nginx/default.conf
|
||||||
|
|
||||||
#macOS
|
#macOS
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker-compose.override.yml
|
||||||
|
|
|
@ -118,9 +118,11 @@ def get_connectors() -> Iterator[abstract_connector.AbstractConnector]:
|
||||||
def get_or_create_connector(remote_id: str) -> abstract_connector.AbstractConnector:
|
def get_or_create_connector(remote_id: str) -> abstract_connector.AbstractConnector:
|
||||||
"""get the connector related to the object's server"""
|
"""get the connector related to the object's server"""
|
||||||
url = urlparse(remote_id)
|
url = urlparse(remote_id)
|
||||||
identifier = url.netloc
|
identifier = url.hostname
|
||||||
if not identifier:
|
if not identifier:
|
||||||
raise ValueError("Invalid remote id")
|
raise ValueError(f"Invalid remote id: {remote_id}")
|
||||||
|
|
||||||
|
base_url = f"{url.scheme}://{url.netloc}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
connector_info = models.Connector.objects.get(identifier=identifier)
|
connector_info = models.Connector.objects.get(identifier=identifier)
|
||||||
|
@ -128,10 +130,10 @@ def get_or_create_connector(remote_id: str) -> abstract_connector.AbstractConnec
|
||||||
connector_info = models.Connector.objects.create(
|
connector_info = models.Connector.objects.create(
|
||||||
identifier=identifier,
|
identifier=identifier,
|
||||||
connector_file="bookwyrm_connector",
|
connector_file="bookwyrm_connector",
|
||||||
base_url=f"https://{identifier}",
|
base_url=base_url,
|
||||||
books_url=f"https://{identifier}/book",
|
books_url=f"{base_url}/book",
|
||||||
covers_url=f"https://{identifier}/images/covers",
|
covers_url=f"{base_url}/images/covers",
|
||||||
search_url=f"https://{identifier}/search?q=",
|
search_url=f"{base_url}/search?q=",
|
||||||
priority=2,
|
priority=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -188,8 +190,11 @@ def raise_not_valid_url(url: str) -> None:
|
||||||
if not parsed.scheme in ["http", "https"]:
|
if not parsed.scheme in ["http", "https"]:
|
||||||
raise ConnectorException("Invalid scheme: ", url)
|
raise ConnectorException("Invalid scheme: ", url)
|
||||||
|
|
||||||
|
if not parsed.hostname:
|
||||||
|
raise ConnectorException("Hostname missing: ", url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ipaddress.ip_address(parsed.netloc)
|
ipaddress.ip_address(parsed.hostname)
|
||||||
raise ConnectorException("Provided url is an IP address: ", url)
|
raise ConnectorException("Provided url is an IP address: ", url)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# it's not an IP address, which is good
|
# it's not an IP address, which is good
|
||||||
|
|
|
@ -4,7 +4,7 @@ from django.template.loader import get_template
|
||||||
|
|
||||||
from bookwyrm import models, settings
|
from bookwyrm import models, settings
|
||||||
from bookwyrm.tasks import app, EMAIL
|
from bookwyrm.tasks import app, EMAIL
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN, BASE_URL
|
||||||
|
|
||||||
|
|
||||||
def email_data():
|
def email_data():
|
||||||
|
@ -14,6 +14,7 @@ def email_data():
|
||||||
"site_name": site.name,
|
"site_name": site.name,
|
||||||
"logo": site.logo_small_url,
|
"logo": site.logo_small_url,
|
||||||
"domain": DOMAIN,
|
"domain": DOMAIN,
|
||||||
|
"base_url": BASE_URL,
|
||||||
"user": None,
|
"user": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ class FileLinkForm(CustomForm):
|
||||||
url = cleaned_data.get("url")
|
url = cleaned_data.get("url")
|
||||||
filetype = cleaned_data.get("filetype")
|
filetype = cleaned_data.get("filetype")
|
||||||
book = cleaned_data.get("book")
|
book = cleaned_data.get("book")
|
||||||
domain = urlparse(url).netloc
|
domain = urlparse(url).hostname
|
||||||
if models.LinkDomain.objects.filter(domain=domain).exists():
|
if models.LinkDomain.objects.filter(domain=domain).exists():
|
||||||
status = models.LinkDomain.objects.get(domain=domain).status
|
status = models.LinkDomain.objects.get(domain=domain).status
|
||||||
if status == "blocked":
|
if status == "blocked":
|
||||||
|
|
|
@ -8,7 +8,7 @@ from django.contrib.postgres.indexes import GinIndex
|
||||||
import pgtrigger
|
import pgtrigger
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import BASE_URL
|
||||||
from bookwyrm.utils.db import format_trigger
|
from bookwyrm.utils.db import format_trigger
|
||||||
|
|
||||||
from .book import BookDataModel, MergedAuthor
|
from .book import BookDataModel, MergedAuthor
|
||||||
|
@ -70,7 +70,7 @@ class Author(BookDataModel):
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
"""editions and works both use "book" instead of model_name"""
|
"""editions and works both use "book" instead of model_name"""
|
||||||
return f"https://{DOMAIN}/author/{self.id}"
|
return f"{BASE_URL}/author/{self.id}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""sets up indexes and triggers"""
|
"""sets up indexes and triggers"""
|
||||||
|
|
|
@ -10,7 +10,7 @@ from django.http import Http404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import BASE_URL
|
||||||
from .fields import RemoteIdField
|
from .fields import RemoteIdField
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ class BookWyrmModel(models.Model):
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
"""generate the url that resolves to the local object, without a slug"""
|
"""generate the url that resolves to the local object, without a slug"""
|
||||||
base_path = f"https://{DOMAIN}"
|
base_path = BASE_URL
|
||||||
if hasattr(self, "user"):
|
if hasattr(self, "user"):
|
||||||
base_path = f"{base_path}{self.user.local_path}"
|
base_path = f"{base_path}{self.user.local_path}"
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ class BookWyrmModel(models.Model):
|
||||||
@property
|
@property
|
||||||
def local_path(self):
|
def local_path(self):
|
||||||
"""how to link to this object in the local app, with a slug"""
|
"""how to link to this object in the local app, with a slug"""
|
||||||
local = self.get_remote_id().replace(f"https://{DOMAIN}", "")
|
local = self.get_remote_id().replace(BASE_URL, "")
|
||||||
|
|
||||||
name = None
|
name = None
|
||||||
if hasattr(self, "name_field"):
|
if hasattr(self, "name_field"):
|
||||||
|
|
|
@ -21,7 +21,7 @@ from bookwyrm import activitypub
|
||||||
from bookwyrm.isbn.isbn import hyphenator_singleton as hyphenator
|
from bookwyrm.isbn.isbn import hyphenator_singleton as hyphenator
|
||||||
from bookwyrm.preview_images import generate_edition_preview_image_task
|
from bookwyrm.preview_images import generate_edition_preview_image_task
|
||||||
from bookwyrm.settings import (
|
from bookwyrm.settings import (
|
||||||
DOMAIN,
|
BASE_URL,
|
||||||
DEFAULT_LANGUAGE,
|
DEFAULT_LANGUAGE,
|
||||||
LANGUAGE_ARTICLES,
|
LANGUAGE_ARTICLES,
|
||||||
ENABLE_PREVIEW_IMAGES,
|
ENABLE_PREVIEW_IMAGES,
|
||||||
|
@ -327,7 +327,7 @@ class Book(BookDataModel):
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
"""editions and works both use "book" instead of model_name"""
|
"""editions and works both use "book" instead of model_name"""
|
||||||
return f"https://{DOMAIN}/book/{self.id}"
|
return f"{BASE_URL}/book/{self.id}"
|
||||||
|
|
||||||
def guess_sort_title(self):
|
def guess_sort_title(self):
|
||||||
"""Get a best-guess sort title for the current book"""
|
"""Get a best-guess sort title for the current book"""
|
||||||
|
|
|
@ -11,7 +11,7 @@ ConnectorFiles = models.TextChoices("ConnectorFiles", CONNECTORS)
|
||||||
class Connector(BookWyrmModel):
|
class Connector(BookWyrmModel):
|
||||||
"""book data source connectors"""
|
"""book data source connectors"""
|
||||||
|
|
||||||
identifier = models.CharField(max_length=255, unique=True)
|
identifier = models.CharField(max_length=255, unique=True) # domain
|
||||||
priority = models.IntegerField(default=2)
|
priority = models.IntegerField(default=2)
|
||||||
name = models.CharField(max_length=255, null=True, blank=True)
|
name = models.CharField(max_length=255, null=True, blank=True)
|
||||||
connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices)
|
connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices)
|
||||||
|
|
|
@ -16,7 +16,7 @@ FederationStatus = [
|
||||||
class FederatedServer(BookWyrmModel):
|
class FederatedServer(BookWyrmModel):
|
||||||
"""store which servers we federate with"""
|
"""store which servers we federate with"""
|
||||||
|
|
||||||
server_name = models.CharField(max_length=255, unique=True)
|
server_name = models.CharField(max_length=255, unique=True) # domain
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=255, default="federated", choices=FederationStatus
|
max_length=255, default="federated", choices=FederationStatus
|
||||||
)
|
)
|
||||||
|
@ -64,5 +64,4 @@ class FederatedServer(BookWyrmModel):
|
||||||
def is_blocked(cls, url: str) -> bool:
|
def is_blocked(cls, url: str) -> bool:
|
||||||
"""look up if a domain is blocked"""
|
"""look up if a domain is blocked"""
|
||||||
url = urlparse(url)
|
url = urlparse(url)
|
||||||
domain = url.netloc
|
return cls.objects.filter(server_name=url.hostname, status="blocked").exists()
|
||||||
return cls.objects.filter(server_name=domain, status="blocked").exists()
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
""" do book related things with other users """
|
""" do book related things with other users """
|
||||||
from django.db import models, IntegrityError, transaction
|
from django.db import models, IntegrityError, transaction
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import BASE_URL
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
from . import fields
|
from . import fields
|
||||||
from .relationship import UserBlocks
|
from .relationship import UserBlocks
|
||||||
|
@ -17,7 +17,7 @@ class Group(BookWyrmModel):
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
"""don't want the user to be in there in this case"""
|
"""don't want the user to be in there in this case"""
|
||||||
return f"https://{DOMAIN}/group/{self.id}"
|
return f"{BASE_URL}/group/{self.id}"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def followers_filter(cls, queryset, viewer):
|
def followers_filter(cls, queryset, viewer):
|
||||||
|
|
|
@ -38,7 +38,7 @@ class Link(ActivitypubMixin, BookWyrmModel):
|
||||||
"""create a link"""
|
"""create a link"""
|
||||||
# get or create the associated domain
|
# get or create the associated domain
|
||||||
if not self.domain:
|
if not self.domain:
|
||||||
domain = urlparse(self.url).netloc
|
domain = urlparse(self.url).hostname
|
||||||
self.domain, _ = LinkDomain.objects.get_or_create(domain=domain)
|
self.domain, _ = LinkDomain.objects.get_or_create(domain=domain)
|
||||||
|
|
||||||
# this is never broadcast, the owning model broadcasts an update
|
# this is never broadcast, the owning model broadcasts an update
|
||||||
|
|
|
@ -7,7 +7,7 @@ from django.db.models import Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import BASE_URL
|
||||||
|
|
||||||
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
|
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
|
@ -50,7 +50,7 @@ class List(OrderedCollectionMixin, BookWyrmModel):
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
"""don't want the user to be in there in this case"""
|
"""don't want the user to be in there in this case"""
|
||||||
return f"https://{DOMAIN}/list/{self.id}"
|
return f"{BASE_URL}/list/{self.id}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def collection_queryset(self):
|
def collection_queryset(self):
|
||||||
|
|
|
@ -10,7 +10,7 @@ from .notification import Notification, NotificationType
|
||||||
|
|
||||||
|
|
||||||
class Move(ActivityMixin, BookWyrmModel):
|
class Move(ActivityMixin, BookWyrmModel):
|
||||||
"""migrating an activitypub user account"""
|
"""migrating an activitypub object"""
|
||||||
|
|
||||||
user = fields.ForeignKey(
|
user = fields.ForeignKey(
|
||||||
"User", on_delete=models.PROTECT, activitypub_field="actor"
|
"User", on_delete=models.PROTECT, activitypub_field="actor"
|
||||||
|
|
|
@ -3,7 +3,7 @@ from django.core.exceptions import PermissionDenied
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import BASE_URL
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ class Report(BookWyrmModel):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
return f"https://{DOMAIN}/settings/reports/{self.id}"
|
return f"{BASE_URL}/settings/reports/{self.id}"
|
||||||
|
|
||||||
def comment(self, user, note):
|
def comment(self, user, note):
|
||||||
"""comment on a report"""
|
"""comment on a report"""
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import BASE_URL
|
||||||
from bookwyrm.tasks import BROADCAST
|
from bookwyrm.tasks import BROADCAST
|
||||||
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
|
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
|
@ -71,7 +71,7 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
||||||
@property
|
@property
|
||||||
def local_path(self):
|
def local_path(self):
|
||||||
"""No slugs"""
|
"""No slugs"""
|
||||||
return self.get_remote_id().replace(f"https://{DOMAIN}", "")
|
return self.get_remote_id().replace(BASE_URL, "")
|
||||||
|
|
||||||
def raise_not_deletable(self, viewer):
|
def raise_not_deletable(self, viewer):
|
||||||
"""don't let anyone delete a default shelf"""
|
"""don't let anyone delete a default shelf"""
|
||||||
|
|
|
@ -12,7 +12,7 @@ from model_utils import FieldTracker
|
||||||
|
|
||||||
from bookwyrm.connectors.abstract_connector import get_data
|
from bookwyrm.connectors.abstract_connector import get_data
|
||||||
from bookwyrm.preview_images import generate_site_preview_image_task
|
from bookwyrm.preview_images import generate_site_preview_image_task
|
||||||
from bookwyrm.settings import DOMAIN, ENABLE_PREVIEW_IMAGES, STATIC_FULL_URL
|
from bookwyrm.settings import BASE_URL, ENABLE_PREVIEW_IMAGES, STATIC_FULL_URL
|
||||||
from bookwyrm.settings import RELEASE_API
|
from bookwyrm.settings import RELEASE_API
|
||||||
from bookwyrm.tasks import app, MISC
|
from bookwyrm.tasks import app, MISC
|
||||||
from .base_model import BookWyrmModel, new_access_code
|
from .base_model import BookWyrmModel, new_access_code
|
||||||
|
@ -188,7 +188,7 @@ class SiteInvite(models.Model):
|
||||||
@property
|
@property
|
||||||
def link(self):
|
def link(self):
|
||||||
"""formats the invite link"""
|
"""formats the invite link"""
|
||||||
return f"https://{DOMAIN}/invite/{self.code}"
|
return f"{BASE_URL}/invite/{self.code}"
|
||||||
|
|
||||||
|
|
||||||
class InviteRequest(BookWyrmModel):
|
class InviteRequest(BookWyrmModel):
|
||||||
|
@ -235,7 +235,7 @@ class PasswordReset(models.Model):
|
||||||
@property
|
@property
|
||||||
def link(self):
|
def link(self):
|
||||||
"""formats the invite link"""
|
"""formats the invite link"""
|
||||||
return f"https://{DOMAIN}/password-reset/{self.code}"
|
return f"{BASE_URL}/password-reset/{self.code}"
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
|
|
|
@ -19,7 +19,7 @@ from bookwyrm.connectors import get_data, ConnectorException
|
||||||
from bookwyrm.models.shelf import Shelf
|
from bookwyrm.models.shelf import Shelf
|
||||||
from bookwyrm.models.status import Status
|
from bookwyrm.models.status import Status
|
||||||
from bookwyrm.preview_images import generate_user_preview_image_task
|
from bookwyrm.preview_images import generate_user_preview_image_task
|
||||||
from bookwyrm.settings import DOMAIN, ENABLE_PREVIEW_IMAGES, USE_HTTPS, LANGUAGES
|
from bookwyrm.settings import BASE_URL, ENABLE_PREVIEW_IMAGES, LANGUAGES
|
||||||
from bookwyrm.signatures import create_key_pair
|
from bookwyrm.signatures import create_key_pair
|
||||||
from bookwyrm.tasks import app, MISC
|
from bookwyrm.tasks import app, MISC
|
||||||
from bookwyrm.utils import regex
|
from bookwyrm.utils import regex
|
||||||
|
@ -42,12 +42,6 @@ def get_feed_filter_choices():
|
||||||
return [f[0] for f in FeedFilterChoices]
|
return [f[0] for f in FeedFilterChoices]
|
||||||
|
|
||||||
|
|
||||||
def site_link():
|
|
||||||
"""helper for generating links to the site"""
|
|
||||||
protocol = "https" if USE_HTTPS else "http"
|
|
||||||
return f"{protocol}://{DOMAIN}"
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-public-methods
|
# pylint: disable=too-many-public-methods
|
||||||
class User(OrderedCollectionPageMixin, AbstractUser):
|
class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
"""a user who wants to read books"""
|
"""a user who wants to read books"""
|
||||||
|
@ -214,8 +208,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
@property
|
@property
|
||||||
def confirmation_link(self):
|
def confirmation_link(self):
|
||||||
"""helper for generating confirmation links"""
|
"""helper for generating confirmation links"""
|
||||||
link = site_link()
|
return f"{BASE_URL}/confirm-email/{self.confirmation_code}"
|
||||||
return f"{link}/confirm-email/{self.confirmation_code}"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def following_link(self):
|
def following_link(self):
|
||||||
|
@ -349,7 +342,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
if not self.local and not re.match(regex.FULL_USERNAME, self.username):
|
if not self.local and not re.match(regex.FULL_USERNAME, self.username):
|
||||||
# generate a username that uses the domain (webfinger format)
|
# generate a username that uses the domain (webfinger format)
|
||||||
actor_parts = urlparse(self.remote_id)
|
actor_parts = urlparse(self.remote_id)
|
||||||
self.username = f"{self.username}@{actor_parts.netloc}"
|
self.username = f"{self.username}@{actor_parts.hostname}"
|
||||||
|
|
||||||
# this user already exists, no need to populate fields
|
# this user already exists, no need to populate fields
|
||||||
if not created:
|
if not created:
|
||||||
|
@ -369,11 +362,10 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# populate fields for local users
|
# populate fields for local users
|
||||||
link = site_link()
|
self.remote_id = f"{BASE_URL}/user/{self.localname}"
|
||||||
self.remote_id = f"{link}/user/{self.localname}"
|
|
||||||
self.followers_url = f"{self.remote_id}/followers"
|
self.followers_url = f"{self.remote_id}/followers"
|
||||||
self.inbox = f"{self.remote_id}/inbox"
|
self.inbox = f"{self.remote_id}/inbox"
|
||||||
self.shared_inbox = f"{link}/inbox"
|
self.shared_inbox = f"{BASE_URL}/inbox"
|
||||||
self.outbox = f"{self.remote_id}/outbox"
|
self.outbox = f"{self.remote_id}/outbox"
|
||||||
|
|
||||||
# an id needs to be set before we can proceed with related models
|
# an id needs to be set before we can proceed with related models
|
||||||
|
@ -558,7 +550,7 @@ def set_remote_server(user_id, allow_external_connections=False):
|
||||||
user = User.objects.get(id=user_id)
|
user = User.objects.get(id=user_id)
|
||||||
actor_parts = urlparse(user.remote_id)
|
actor_parts = urlparse(user.remote_id)
|
||||||
federated_server = get_or_create_remote_server(
|
federated_server = get_or_create_remote_server(
|
||||||
actor_parts.netloc, allow_external_connections=allow_external_connections
|
actor_parts.hostname, allow_external_connections=allow_external_connections
|
||||||
)
|
)
|
||||||
# if we were unable to find the server, we need to create a new entry for it
|
# if we were unable to find the server, we need to create a new entry for it
|
||||||
if not federated_server:
|
if not federated_server:
|
||||||
|
|
|
@ -101,6 +101,7 @@ INSTALLED_APPS = [
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"django.contrib.humanize",
|
"django.contrib.humanize",
|
||||||
|
"oauth2_provider",
|
||||||
"file_resubmit",
|
"file_resubmit",
|
||||||
"sass_processor",
|
"sass_processor",
|
||||||
"bookwyrm",
|
"bookwyrm",
|
||||||
|
@ -350,28 +351,31 @@ USE_L10N = True
|
||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
USER_AGENT = f"BookWyrm (BookWyrm/{VERSION}; +https://{DOMAIN}/)"
|
|
||||||
|
|
||||||
# Imagekit generated thumbnails
|
# Imagekit generated thumbnails
|
||||||
ENABLE_THUMBNAIL_GENERATION = env.bool("ENABLE_THUMBNAIL_GENERATION", False)
|
ENABLE_THUMBNAIL_GENERATION = env.bool("ENABLE_THUMBNAIL_GENERATION", False)
|
||||||
IMAGEKIT_CACHEFILE_DIR = "thumbnails"
|
IMAGEKIT_CACHEFILE_DIR = "thumbnails"
|
||||||
IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = "bookwyrm.thumbnail_generation.Strategy"
|
IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = "bookwyrm.thumbnail_generation.Strategy"
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
|
||||||
# https://docs.djangoproject.com/en/3.2/howto/static-files/
|
|
||||||
|
|
||||||
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
|
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
CSP_ADDITIONAL_HOSTS = env.list("CSP_ADDITIONAL_HOSTS", [])
|
CSP_ADDITIONAL_HOSTS = env.list("CSP_ADDITIONAL_HOSTS", [])
|
||||||
|
|
||||||
# Storage
|
|
||||||
|
|
||||||
PROTOCOL = "http"
|
PROTOCOL = "http"
|
||||||
if USE_HTTPS:
|
if USE_HTTPS:
|
||||||
PROTOCOL = "https"
|
PROTOCOL = "https"
|
||||||
SESSION_COOKIE_SECURE = True
|
SESSION_COOKIE_SECURE = True
|
||||||
CSRF_COOKIE_SECURE = True
|
CSRF_COOKIE_SECURE = True
|
||||||
|
|
||||||
|
PORT = env.int("PORT", 443 if USE_HTTPS else 80)
|
||||||
|
if (USE_HTTPS and PORT == 443) or (not USE_HTTPS and PORT == 80):
|
||||||
|
NETLOC = DOMAIN
|
||||||
|
else:
|
||||||
|
NETLOC = f"{DOMAIN}:{PORT}"
|
||||||
|
BASE_URL = f"{PROTOCOL}://{NETLOC}"
|
||||||
|
|
||||||
|
USER_AGENT = f"BookWyrm (BookWyrm/{VERSION}; +{BASE_URL})"
|
||||||
|
|
||||||
|
# Storage
|
||||||
|
|
||||||
USE_S3 = env.bool("USE_S3", False)
|
USE_S3 = env.bool("USE_S3", False)
|
||||||
USE_AZURE = env.bool("USE_AZURE", False)
|
USE_AZURE = env.bool("USE_AZURE", False)
|
||||||
S3_SIGNED_URL_EXPIRY = env.int("S3_SIGNED_URL_EXPIRY", 900)
|
S3_SIGNED_URL_EXPIRY = env.int("S3_SIGNED_URL_EXPIRY", 900)
|
||||||
|
@ -386,21 +390,32 @@ if USE_S3:
|
||||||
AWS_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL", None)
|
AWS_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL", None)
|
||||||
AWS_DEFAULT_ACL = "public-read"
|
AWS_DEFAULT_ACL = "public-read"
|
||||||
AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=86400"}
|
AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=86400"}
|
||||||
|
AWS_S3_URL_PROTOCOL = env("AWS_S3_URL_PROTOCOL", f"{PROTOCOL}:")
|
||||||
# S3 Static settings
|
# S3 Static settings
|
||||||
STATIC_LOCATION = "static"
|
STATIC_LOCATION = "static"
|
||||||
STATIC_URL = f"{PROTOCOL}://{AWS_S3_CUSTOM_DOMAIN}/{STATIC_LOCATION}/"
|
STATIC_URL = f"{AWS_S3_URL_PROTOCOL}//{AWS_S3_CUSTOM_DOMAIN}/{STATIC_LOCATION}/"
|
||||||
STATIC_FULL_URL = STATIC_URL
|
STATIC_FULL_URL = STATIC_URL
|
||||||
STATICFILES_STORAGE = "bookwyrm.storage_backends.StaticStorage"
|
STATICFILES_STORAGE = "bookwyrm.storage_backends.StaticStorage"
|
||||||
# S3 Media settings
|
# S3 Media settings
|
||||||
MEDIA_LOCATION = "images"
|
MEDIA_LOCATION = "images"
|
||||||
MEDIA_URL = f"{PROTOCOL}://{AWS_S3_CUSTOM_DOMAIN}/{MEDIA_LOCATION}/"
|
MEDIA_URL = f"{AWS_S3_URL_PROTOCOL}//{AWS_S3_CUSTOM_DOMAIN}/{MEDIA_LOCATION}/"
|
||||||
MEDIA_FULL_URL = MEDIA_URL
|
MEDIA_FULL_URL = MEDIA_URL
|
||||||
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage"
|
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage"
|
||||||
# S3 Exports settings
|
# S3 Exports settings
|
||||||
EXPORTS_STORAGE = "bookwyrm.storage_backends.ExportsS3Storage"
|
EXPORTS_STORAGE = "bookwyrm.storage_backends.ExportsS3Storage"
|
||||||
# Content Security Policy
|
# Content Security Policy
|
||||||
CSP_DEFAULT_SRC = ["'self'", AWS_S3_CUSTOM_DOMAIN] + CSP_ADDITIONAL_HOSTS
|
CSP_DEFAULT_SRC = [
|
||||||
CSP_SCRIPT_SRC = ["'self'", AWS_S3_CUSTOM_DOMAIN] + CSP_ADDITIONAL_HOSTS
|
"'self'",
|
||||||
|
f"{AWS_S3_URL_PROTOCOL}//{AWS_S3_CUSTOM_DOMAIN}"
|
||||||
|
if AWS_S3_CUSTOM_DOMAIN
|
||||||
|
else None,
|
||||||
|
] + CSP_ADDITIONAL_HOSTS
|
||||||
|
CSP_SCRIPT_SRC = [
|
||||||
|
"'self'",
|
||||||
|
f"{AWS_S3_URL_PROTOCOL}//{AWS_S3_CUSTOM_DOMAIN}"
|
||||||
|
if AWS_S3_CUSTOM_DOMAIN
|
||||||
|
else None,
|
||||||
|
] + CSP_ADDITIONAL_HOSTS
|
||||||
elif USE_AZURE:
|
elif USE_AZURE:
|
||||||
# Azure settings
|
# Azure settings
|
||||||
AZURE_ACCOUNT_NAME = env("AZURE_ACCOUNT_NAME")
|
AZURE_ACCOUNT_NAME = env("AZURE_ACCOUNT_NAME")
|
||||||
|
@ -429,11 +444,11 @@ elif USE_AZURE:
|
||||||
else:
|
else:
|
||||||
# Static settings
|
# Static settings
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = "/static/"
|
||||||
STATIC_FULL_URL = f"{PROTOCOL}://{DOMAIN}{STATIC_URL}"
|
STATIC_FULL_URL = BASE_URL + STATIC_URL
|
||||||
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
|
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
|
||||||
# Media settings
|
# Media settings
|
||||||
MEDIA_URL = "/images/"
|
MEDIA_URL = "/images/"
|
||||||
MEDIA_FULL_URL = f"{PROTOCOL}://{DOMAIN}{MEDIA_URL}"
|
MEDIA_FULL_URL = BASE_URL + MEDIA_URL
|
||||||
DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage"
|
DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage"
|
||||||
# Exports settings
|
# Exports settings
|
||||||
EXPORTS_STORAGE = "bookwyrm.storage_backends.ExportsFileStorage"
|
EXPORTS_STORAGE = "bookwyrm.storage_backends.ExportsFileStorage"
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
<div style="font-family: BlinkMacSystemFont,-apple-system,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Fira Sans','Droid Sans','Helvetica Neue',Helvetica,Arial,sans-serif; border-radius: 6px; background-color: #efefef; max-width: 632px;">
|
<div style="font-family: BlinkMacSystemFont,-apple-system,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Fira Sans','Droid Sans','Helvetica Neue',Helvetica,Arial,sans-serif; border-radius: 6px; background-color: #efefef; max-width: 632px;">
|
||||||
<div style="padding: 1rem; overflow: auto;">
|
<div style="padding: 1rem; overflow: auto;">
|
||||||
<div style="float: left; margin-right: 1rem;">
|
<div style="float: left; margin-right: 1rem;">
|
||||||
<a style="color: #3273dc;" href="https://{{ domain }}" style="text-decoration: none;"><img src="{{ logo }}" alt="logo" loading="lazy" decoding="async"></a>
|
<a style="color: #3273dc;" href="{{ base_url }}" style="text-decoration: none;"><img src="{{ logo }}" alt="logo" loading="lazy" decoding="async"></a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a style="color: black; text-decoration: none" href="https://{{ domain }}" style="text-decoration: none;"><strong>{{ site_name }}</strong><br>
|
<a style="color: black; text-decoration: none" href="{{ base_url }}" style="text-decoration: none;"><strong>{{ site_name }}</strong><br>
|
||||||
{{ domain }}</a>
|
{{ domain }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,9 +18,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="padding: 1rem; font-size: 0.8rem;">
|
<div style="padding: 1rem; font-size: 0.8rem;">
|
||||||
<p style="margin: 0; color: #333;">{% blocktrans %}BookWyrm hosted on <a style="color: #3273dc;" href="https://{{ domain }}">{{ site_name }}</a>{% endblocktrans %}</p>
|
<p style="margin: 0; color: #333;">{% blocktrans %}BookWyrm hosted on <a style="color: #3273dc;" href="{{ base_url }}">{{ site_name }}</a>{% endblocktrans %}</p>
|
||||||
{% if user %}
|
{% if user %}
|
||||||
<p style="margin: 0; color: #333;"><a style="color: #3273dc;" href="https://{{ domain }}{% url 'prefs-profile' %}">{% trans "Email preference" %}</a></p>
|
<p style="margin: 0; color: #333;"><a style="color: #3273dc;" href="{{ base_url }}{% url 'prefs-profile' %}">{% trans "Email preference" %}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,6 +12,6 @@
|
||||||
<p>
|
<p>
|
||||||
{% url 'code-of-conduct' as coc_path %}
|
{% url 'code-of-conduct' as coc_path %}
|
||||||
{% url 'about' as about_path %}
|
{% url 'about' as about_path %}
|
||||||
{% blocktrans %}Learn more <a href="https://{{ domain }}{{ about_path }}">about {{ site_name }}</a>.{% endblocktrans %}
|
{% blocktrans %}Learn more <a href="{{ base_url }}{{ about_path }}">about {{ site_name }}</a>.{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -5,6 +5,6 @@
|
||||||
|
|
||||||
{{ invite_link }}
|
{{ invite_link }}
|
||||||
|
|
||||||
{% blocktrans %}Learn more about {{ site_name }}:{% endblocktrans %} https://{{ domain }}{% url 'about' %}
|
{% blocktrans %}Learn more about {{ site_name }}:{% endblocktrans %} {{ base_url }}{% url 'about' %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -10,6 +10,6 @@
|
||||||
<Image width="16" height="16" type="image/x-icon">{{ image }}</Image>
|
<Image width="16" height="16" type="image/x-icon">{{ image }}</Image>
|
||||||
<Url
|
<Url
|
||||||
type="text/html"
|
type="text/html"
|
||||||
template="https://{{ DOMAIN }}{% url 'search' %}?q={searchTerms}"
|
template="{{ BASE_URL }}{% url 'search' %}?q={searchTerms}"
|
||||||
/>
|
/>
|
||||||
</OpenSearchDescription>
|
</OpenSearchDescription>
|
||||||
|
|
11
bookwyrm/templates/rss/edition.html
Normal file
11
bookwyrm/templates/rss/edition.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% blocktrans trimmed with book_title=obj.title book_author=obj.author_text %}
|
||||||
|
‘{{ book_title }}’{% if book_author %} by {{ book_author }}{% endif %}
|
||||||
|
{% endblocktrans %}
|
||||||
|
{{obj.description|default:""}}
|
||||||
|
{% if obj.isbn_13 %}{% trans "ISBN 13:" %} {{ obj.isbn_13 }}{% endif %}
|
||||||
|
{% if obj.oclc_number %}{% trans "OCLC Number:" %} {{ obj.oclc_number }}{% endif %}
|
||||||
|
{% if obj.asin %}{% trans "ASIN:" %} {{ obj.asin }}{% endif %}
|
||||||
|
{% if obj.aasin %}{% trans "Audible ASIN:" %} {{ obj.aasin }}{% endif %}
|
||||||
|
{% if obj.isfdb %}{% trans "ISFDB ID:" %} {{ obj.isfdb }}{% endif %}
|
||||||
|
{% if obj.goodreads_key %}{% trans "Goodreads:" %} {{ obj.goodreads_key }}{% endif %}
|
|
@ -120,7 +120,7 @@ def id_to_username(user_id):
|
||||||
"""given an arbitrary remote id, return the username"""
|
"""given an arbitrary remote id, return the username"""
|
||||||
if user_id:
|
if user_id:
|
||||||
url = urlparse(user_id)
|
url = urlparse(user_id)
|
||||||
domain = url.netloc
|
domain = url.hostname
|
||||||
parts = url.path.split("/")
|
parts = url.path.split("/")
|
||||||
name = parts[-1]
|
name = parts[-1]
|
||||||
value = f"{name}@{domain}"
|
value = f"{name}@{domain}"
|
||||||
|
|
|
@ -6,7 +6,7 @@ import responses
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.connectors import abstract_connector, ConnectorException
|
from bookwyrm.connectors import abstract_connector, ConnectorException
|
||||||
from bookwyrm.connectors.abstract_connector import Mapping, get_data
|
from bookwyrm.connectors.abstract_connector import Mapping, get_data
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import BASE_URL
|
||||||
|
|
||||||
|
|
||||||
class AbstractConnector(TestCase):
|
class AbstractConnector(TestCase):
|
||||||
|
@ -86,7 +86,7 @@ class AbstractConnector(TestCase):
|
||||||
def test_get_or_create_book_existing(self):
|
def test_get_or_create_book_existing(self):
|
||||||
"""find an existing book by remote/origin id"""
|
"""find an existing book by remote/origin id"""
|
||||||
self.assertEqual(models.Book.objects.count(), 1)
|
self.assertEqual(models.Book.objects.count(), 1)
|
||||||
self.assertEqual(self.book.remote_id, f"https://{DOMAIN}/book/{self.book.id}")
|
self.assertEqual(self.book.remote_id, f"{BASE_URL}/book/{self.book.id}")
|
||||||
self.assertEqual(self.book.origin_id, "https://example.com/book/1234")
|
self.assertEqual(self.book.origin_id, "https://example.com/book/1234")
|
||||||
|
|
||||||
# dedupe by origin id
|
# dedupe by origin id
|
||||||
|
@ -95,9 +95,7 @@ class AbstractConnector(TestCase):
|
||||||
self.assertEqual(result, self.book)
|
self.assertEqual(result, self.book)
|
||||||
|
|
||||||
# dedupe by remote id
|
# dedupe by remote id
|
||||||
result = self.connector.get_or_create_book(
|
result = self.connector.get_or_create_book(f"{BASE_URL}/book/{self.book.id}")
|
||||||
f"https://{DOMAIN}/book/{self.book.id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(models.Book.objects.count(), 1)
|
self.assertEqual(models.Book.objects.count(), 1)
|
||||||
self.assertEqual(result, self.book)
|
self.assertEqual(result, self.book)
|
||||||
|
|
42
bookwyrm/tests/data/ap_user_move.json
Normal file
42
bookwyrm/tests/data/ap_user_move.json
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://w3id.org/security/v1",
|
||||||
|
{
|
||||||
|
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||||
|
"schema": "http://schema.org#",
|
||||||
|
"PropertyValue": "schema:PropertyValue",
|
||||||
|
"value": "schema:value"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "https://example.com/user/mouse",
|
||||||
|
"type": "Person",
|
||||||
|
"preferredUsername": "mouse",
|
||||||
|
"name": "MOUSE?? MOUSE!!",
|
||||||
|
"inbox": "https://example.com/user/mouse/inbox",
|
||||||
|
"outbox": "https://example.com/user/mouse/outbox",
|
||||||
|
"followers": "https://example.com/user/mouse/followers",
|
||||||
|
"following": "https://example.com/user/mouse/following",
|
||||||
|
"summary": "",
|
||||||
|
"publicKey": {
|
||||||
|
"id": "https://example.com/user/mouse/#main-key",
|
||||||
|
"owner": "https://example.com/user/mouse",
|
||||||
|
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6QisDrjOQvkRo/MqNmSYPwqtt\nCxg/8rCW+9jKbFUKvqjTeKVotEE85122v/DCvobCCdfQuYIFdVMk+dB1xJ0iPGPg\nyU79QHY22NdV9mFKA2qtXVVxb5cxpA4PlwOHM6PM/k8B+H09OUrop2aPUAYwy+vg\n+MXyz8bAXrIS1kq6fQIDAQAB\n-----END PUBLIC KEY-----"
|
||||||
|
},
|
||||||
|
"endpoints": {
|
||||||
|
"sharedInbox": "https://example.com/inbox"
|
||||||
|
},
|
||||||
|
"bookwyrmUser": true,
|
||||||
|
"manuallyApprovesFollowers": false,
|
||||||
|
"discoverable": false,
|
||||||
|
"alsoKnownAs": [
|
||||||
|
"https://your.domain.here:4242/user/rat"
|
||||||
|
],
|
||||||
|
"devices": "https://friend.camp/users/tripofmice/collections/devices",
|
||||||
|
"tag": [],
|
||||||
|
"icon": {
|
||||||
|
"type": "Image",
|
||||||
|
"mediaType": "image/png",
|
||||||
|
"url": "https://example.com/images/avatars/AL-2-crop-50.png"
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
|
@ -214,7 +214,7 @@
|
||||||
"attributedTo": "https://www.example.com//user/rat",
|
"attributedTo": "https://www.example.com//user/rat",
|
||||||
"content": "<p>I like it</p>",
|
"content": "<p>I like it</p>",
|
||||||
"to": [
|
"to": [
|
||||||
"https://your.domain.here/user/rat/followers"
|
"https://your.domain.here:4242/user/rat/followers"
|
||||||
],
|
],
|
||||||
"cc": [],
|
"cc": [],
|
||||||
"replies": {
|
"replies": {
|
||||||
|
@ -395,7 +395,7 @@
|
||||||
"https://local.lists/9999"
|
"https://local.lists/9999"
|
||||||
],
|
],
|
||||||
"follows": [
|
"follows": [
|
||||||
"https://your.domain.here/user/rat"
|
"https://your.domain.here:4242/user/rat"
|
||||||
],
|
],
|
||||||
"blocks": ["https://your.domain.here/user/badger"]
|
"blocks": ["https://your.domain.here:4242/user/badger"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django.test import TestCase
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.models import base_model
|
from bookwyrm.models import base_model
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import BASE_URL
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=attribute-defined-outside-init
|
# pylint: disable=attribute-defined-outside-init
|
||||||
|
@ -44,14 +44,14 @@ class BaseModel(TestCase):
|
||||||
"""these should be generated"""
|
"""these should be generated"""
|
||||||
self.test_model.id = 1 # pylint: disable=invalid-name
|
self.test_model.id = 1 # pylint: disable=invalid-name
|
||||||
expected = self.test_model.get_remote_id()
|
expected = self.test_model.get_remote_id()
|
||||||
self.assertEqual(expected, f"https://{DOMAIN}/bookwyrmtestmodel/1")
|
self.assertEqual(expected, f"{BASE_URL}/bookwyrmtestmodel/1")
|
||||||
|
|
||||||
def test_remote_id_with_user(self):
|
def test_remote_id_with_user(self):
|
||||||
"""format of remote id when there's a user object"""
|
"""format of remote id when there's a user object"""
|
||||||
self.test_model.user = self.local_user
|
self.test_model.user = self.local_user
|
||||||
self.test_model.id = 1
|
self.test_model.id = 1
|
||||||
expected = self.test_model.get_remote_id()
|
expected = self.test_model.get_remote_id()
|
||||||
self.assertEqual(expected, f"https://{DOMAIN}/user/mouse/bookwyrmtestmodel/1")
|
self.assertEqual(expected, f"{BASE_URL}/user/mouse/bookwyrmtestmodel/1")
|
||||||
|
|
||||||
def test_set_remote_id(self):
|
def test_set_remote_id(self):
|
||||||
"""this function sets remote ids after creation"""
|
"""this function sets remote ids after creation"""
|
||||||
|
@ -60,7 +60,7 @@ class BaseModel(TestCase):
|
||||||
instance = models.Work.objects.create(title="work title")
|
instance = models.Work.objects.create(title="work title")
|
||||||
instance.remote_id = None
|
instance.remote_id = None
|
||||||
base_model.set_remote_id(None, instance, True)
|
base_model.set_remote_id(None, instance, True)
|
||||||
self.assertEqual(instance.remote_id, f"https://{DOMAIN}/book/{instance.id}")
|
self.assertEqual(instance.remote_id, f"{BASE_URL}/book/{instance.id}")
|
||||||
|
|
||||||
# shouldn't set remote_id if it's not created
|
# shouldn't set remote_id if it's not created
|
||||||
instance.remote_id = None
|
instance.remote_id = None
|
||||||
|
|
|
@ -31,7 +31,7 @@ class Book(TestCase):
|
||||||
|
|
||||||
def test_remote_id(self):
|
def test_remote_id(self):
|
||||||
"""fanciness with remote/origin ids"""
|
"""fanciness with remote/origin ids"""
|
||||||
remote_id = f"https://{settings.DOMAIN}/book/{self.work.id}"
|
remote_id = f"{settings.BASE_URL}/book/{self.work.id}"
|
||||||
self.assertEqual(self.work.get_remote_id(), remote_id)
|
self.assertEqual(self.work.get_remote_id(), remote_id)
|
||||||
self.assertEqual(self.work.remote_id, remote_id)
|
self.assertEqual(self.work.remote_id, remote_id)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ from dataclasses import dataclass
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
from urllib.parse import urlparse
|
||||||
from typing import List
|
from typing import List
|
||||||
from unittest import expectedFailure
|
from unittest import expectedFailure
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
@ -22,7 +22,7 @@ from bookwyrm.activitypub.base_activity import ActivityObject
|
||||||
from bookwyrm.models import fields, User, Status, Edition
|
from bookwyrm.models import fields, User, Status, Edition
|
||||||
from bookwyrm.models.base_model import BookWyrmModel
|
from bookwyrm.models.base_model import BookWyrmModel
|
||||||
from bookwyrm.models.activitypub_mixin import ActivitypubMixin
|
from bookwyrm.models.activitypub_mixin import ActivitypubMixin
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import PROTOCOL, NETLOC
|
||||||
|
|
||||||
# pylint: disable=too-many-public-methods
|
# pylint: disable=too-many-public-methods
|
||||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||||
|
@ -427,12 +427,10 @@ class ModelFields(TestCase):
|
||||||
instance = fields.ImageField()
|
instance = fields.ImageField()
|
||||||
|
|
||||||
output = instance.field_to_activity(user.avatar)
|
output = instance.field_to_activity(user.avatar)
|
||||||
self.assertIsNotNone(
|
parsed_url = urlparse(output.url)
|
||||||
re.match(
|
self.assertEqual(parsed_url.scheme, PROTOCOL)
|
||||||
rf"https:\/\/{DOMAIN}\/.*\.jpg",
|
self.assertEqual(parsed_url.netloc, NETLOC)
|
||||||
output.url,
|
self.assertRegex(parsed_url.path, r"\.jpg$")
|
||||||
)
|
|
||||||
)
|
|
||||||
self.assertEqual(output.name, "")
|
self.assertEqual(output.name, "")
|
||||||
self.assertEqual(output.type, "Image")
|
self.assertEqual(output.type, "Image")
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ class List(TestCase):
|
||||||
def test_remote_id(self, *_):
|
def test_remote_id(self, *_):
|
||||||
"""shelves use custom remote ids"""
|
"""shelves use custom remote ids"""
|
||||||
book_list = models.List.objects.create(name="Test List", user=self.local_user)
|
book_list = models.List.objects.create(name="Test List", user=self.local_user)
|
||||||
expected_id = f"https://{settings.DOMAIN}/list/{book_list.id}"
|
expected_id = f"{settings.BASE_URL}/list/{book_list.id}"
|
||||||
self.assertEqual(book_list.get_remote_id(), expected_id)
|
self.assertEqual(book_list.get_remote_id(), expected_id)
|
||||||
|
|
||||||
def test_to_activity(self, *_):
|
def test_to_activity(self, *_):
|
||||||
|
|
60
bookwyrm/tests/models/test_move.py
Normal file
60
bookwyrm/tests/models/test_move.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
""" testing move models """
|
||||||
|
from unittest.mock import patch
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
|
||||||
|
|
||||||
|
class MoveUser(TestCase):
|
||||||
|
"""move your account to another identity"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
"""we need some users for this"""
|
||||||
|
with patch("bookwyrm.models.user.set_remote_server.delay"):
|
||||||
|
cls.target_user = models.User.objects.create_user(
|
||||||
|
"rat",
|
||||||
|
"rat@rat.com",
|
||||||
|
"ratword",
|
||||||
|
local=False,
|
||||||
|
remote_id="https://example.com/users/rat",
|
||||||
|
inbox="https://example.com/users/rat/inbox",
|
||||||
|
outbox="https://example.com/users/rat/outbox",
|
||||||
|
)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"),
|
||||||
|
patch("bookwyrm.activitystreams.populate_stream_task.delay"),
|
||||||
|
patch("bookwyrm.lists_stream.populate_lists_task.delay"),
|
||||||
|
):
|
||||||
|
cls.origin_user = models.User.objects.create_user(
|
||||||
|
"mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse"
|
||||||
|
)
|
||||||
|
cls.origin_user.remote_id = "http://local.com/user/mouse"
|
||||||
|
cls.origin_user.save(broadcast=False, update_fields=["remote_id"])
|
||||||
|
|
||||||
|
def test_user_move_unauthorized(self):
|
||||||
|
"""attempt a user move without alsoKnownAs set"""
|
||||||
|
|
||||||
|
with self.assertRaises(PermissionDenied):
|
||||||
|
models.MoveUser.objects.create(
|
||||||
|
user=self.origin_user,
|
||||||
|
object=self.origin_user.remote_id,
|
||||||
|
target=self.target_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("bookwyrm.suggested_users.remove_user_task.delay")
|
||||||
|
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
|
||||||
|
def test_user_move(self, *_):
|
||||||
|
"""move user"""
|
||||||
|
|
||||||
|
self.target_user.also_known_as.add(self.origin_user.id)
|
||||||
|
self.target_user.save(broadcast=False)
|
||||||
|
|
||||||
|
models.MoveUser.objects.create(
|
||||||
|
user=self.origin_user,
|
||||||
|
object=self.origin_user.remote_id,
|
||||||
|
target=self.target_user,
|
||||||
|
)
|
||||||
|
self.assertEqual(self.origin_user.moved_to, self.target_user.remote_id)
|
|
@ -35,7 +35,7 @@ class Shelf(TestCase):
|
||||||
shelf = models.Shelf.objects.create(
|
shelf = models.Shelf.objects.create(
|
||||||
name="Test Shelf", identifier="test-shelf", user=self.local_user
|
name="Test Shelf", identifier="test-shelf", user=self.local_user
|
||||||
)
|
)
|
||||||
expected_id = f"https://{settings.DOMAIN}/user/mouse/books/test-shelf"
|
expected_id = f"{settings.BASE_URL}/user/mouse/books/test-shelf"
|
||||||
self.assertEqual(shelf.get_remote_id(), expected_id)
|
self.assertEqual(shelf.get_remote_id(), expected_id)
|
||||||
|
|
||||||
def test_to_activity(self, *_):
|
def test_to_activity(self, *_):
|
||||||
|
|
|
@ -79,7 +79,7 @@ class SiteModels(TestCase):
|
||||||
def test_site_invite_link(self):
|
def test_site_invite_link(self):
|
||||||
"""invite link generator"""
|
"""invite link generator"""
|
||||||
invite = models.SiteInvite.objects.create(user=self.local_user, code="hello")
|
invite = models.SiteInvite.objects.create(user=self.local_user, code="hello")
|
||||||
self.assertEqual(invite.link, f"https://{settings.DOMAIN}/invite/hello")
|
self.assertEqual(invite.link, f"{settings.BASE_URL}/invite/hello")
|
||||||
|
|
||||||
def test_invite_request(self):
|
def test_invite_request(self):
|
||||||
"""someone wants an invite"""
|
"""someone wants an invite"""
|
||||||
|
@ -95,7 +95,7 @@ class SiteModels(TestCase):
|
||||||
"""password reset token"""
|
"""password reset token"""
|
||||||
token = models.PasswordReset.objects.create(user=self.local_user, code="hello")
|
token = models.PasswordReset.objects.create(user=self.local_user, code="hello")
|
||||||
self.assertTrue(token.valid())
|
self.assertTrue(token.valid())
|
||||||
self.assertEqual(token.link, f"https://{settings.DOMAIN}/password-reset/hello")
|
self.assertEqual(token.link, f"{settings.BASE_URL}/password-reset/hello")
|
||||||
|
|
||||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||||
@patch("bookwyrm.suggested_users.remove_user_task.delay")
|
@patch("bookwyrm.suggested_users.remove_user_task.delay")
|
||||||
|
|
|
@ -60,7 +60,7 @@ class Status(TestCase):
|
||||||
def test_status_generated_fields(self, *_):
|
def test_status_generated_fields(self, *_):
|
||||||
"""setting remote id"""
|
"""setting remote id"""
|
||||||
status = models.Status.objects.create(content="bleh", user=self.local_user)
|
status = models.Status.objects.create(content="bleh", user=self.local_user)
|
||||||
expected_id = f"https://{settings.DOMAIN}/user/mouse/status/{status.id}"
|
expected_id = f"{settings.BASE_URL}/user/mouse/status/{status.id}"
|
||||||
self.assertEqual(status.remote_id, expected_id)
|
self.assertEqual(status.remote_id, expected_id)
|
||||||
self.assertEqual(status.privacy, "public")
|
self.assertEqual(status.privacy, "public")
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@ class Status(TestCase):
|
||||||
self.assertEqual(activity["tag"][0]["type"], "Hashtag")
|
self.assertEqual(activity["tag"][0]["type"], "Hashtag")
|
||||||
self.assertEqual(activity["tag"][0]["name"], "#content")
|
self.assertEqual(activity["tag"][0]["name"], "#content")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
activity["tag"][0]["href"], f"https://{settings.DOMAIN}/hashtag/{tag.id}"
|
activity["tag"][0]["href"], f"{settings.BASE_URL}/hashtag/{tag.id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_status_with_mention_to_activity(self, *_):
|
def test_status_with_mention_to_activity(self, *_):
|
||||||
|
@ -227,11 +227,9 @@ class Status(TestCase):
|
||||||
self.assertEqual(activity["sensitive"], False)
|
self.assertEqual(activity["sensitive"], False)
|
||||||
self.assertIsInstance(activity["attachment"], list)
|
self.assertIsInstance(activity["attachment"], list)
|
||||||
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
||||||
self.assertTrue(
|
self.assertRegex(
|
||||||
re.match(
|
activity["attachment"][0]["url"],
|
||||||
r"https:\/\/your.domain.here\/images\/covers\/test(_[A-z0-9]+)?.jpg",
|
rf"^{settings.BASE_URL}/images/covers/test(_[A-z0-9]+)?.jpg$",
|
||||||
activity["attachment"][0]["url"],
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
||||||
|
|
||||||
|
@ -263,12 +261,10 @@ class Status(TestCase):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
||||||
# self.assertTrue(
|
self.assertRegex(
|
||||||
# re.match(
|
activity["attachment"][0]["url"],
|
||||||
# r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
|
rf"^{settings.BASE_URL}/images/covers/test_[A-z0-9]+.jpg$",
|
||||||
# activity["attachment"][0].url,
|
)
|
||||||
# )
|
|
||||||
# )
|
|
||||||
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
||||||
|
|
||||||
def test_quotation_to_activity(self, *_):
|
def test_quotation_to_activity(self, *_):
|
||||||
|
@ -306,11 +302,9 @@ class Status(TestCase):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
||||||
self.assertTrue(
|
self.assertRegex(
|
||||||
re.match(
|
activity["attachment"][0]["url"],
|
||||||
r"https:\/\/your.domain.here\/images\/covers\/test(_[A-z0-9]+)?.jpg",
|
rf"^{settings.BASE_URL}/images/covers/test(_[A-z0-9]+)?.jpg$",
|
||||||
activity["attachment"][0]["url"],
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
||||||
|
|
||||||
|
@ -400,11 +394,9 @@ class Status(TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(activity["content"], "test content")
|
self.assertEqual(activity["content"], "test content")
|
||||||
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
||||||
self.assertTrue(
|
self.assertRegex(
|
||||||
re.match(
|
activity["attachment"][0]["url"],
|
||||||
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
|
rf"^{settings.BASE_URL}/images/covers/test_[A-z0-9]+.jpg$",
|
||||||
activity["attachment"][0]["url"],
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
||||||
|
|
||||||
|
@ -425,11 +417,9 @@ class Status(TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(activity["content"], "test content")
|
self.assertEqual(activity["content"], "test content")
|
||||||
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
||||||
self.assertTrue(
|
self.assertRegex(
|
||||||
re.match(
|
activity["attachment"][0]["url"],
|
||||||
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
|
rf"^{settings.BASE_URL}/images/covers/test_[A-z0-9]+.jpg$",
|
||||||
activity["attachment"][0]["url"],
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
||||||
|
|
||||||
|
@ -448,11 +438,9 @@ class Status(TestCase):
|
||||||
f'rated <em><a href="{self.book.remote_id}">{self.book.title}</a></em>: 3 stars',
|
f'rated <em><a href="{self.book.remote_id}">{self.book.title}</a></em>: 3 stars',
|
||||||
)
|
)
|
||||||
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
||||||
self.assertTrue(
|
self.assertRegex(
|
||||||
re.match(
|
activity["attachment"][0]["url"],
|
||||||
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
|
rf"^{settings.BASE_URL}/images/covers/test_[A-z0-9]+.jpg$",
|
||||||
activity["attachment"][0]["url"],
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
||||||
|
|
||||||
|
|
|
@ -9,15 +9,12 @@ import responses
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.management.commands import initdb
|
from bookwyrm.management.commands import initdb
|
||||||
from bookwyrm.settings import USE_HTTPS, DOMAIN
|
from bookwyrm.settings import DOMAIN, BASE_URL
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=missing-class-docstring
|
# pylint: disable=missing-class-docstring
|
||||||
# pylint: disable=missing-function-docstring
|
# pylint: disable=missing-function-docstring
|
||||||
class User(TestCase):
|
class User(TestCase):
|
||||||
|
|
||||||
protocol = "https://" if USE_HTTPS else "http://"
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
with (
|
with (
|
||||||
|
@ -49,11 +46,11 @@ class User(TestCase):
|
||||||
|
|
||||||
def test_computed_fields(self):
|
def test_computed_fields(self):
|
||||||
"""username instead of id here"""
|
"""username instead of id here"""
|
||||||
expected_id = f"{self.protocol}{DOMAIN}/user/mouse"
|
expected_id = f"{BASE_URL}/user/mouse"
|
||||||
self.assertEqual(self.user.remote_id, expected_id)
|
self.assertEqual(self.user.remote_id, expected_id)
|
||||||
self.assertEqual(self.user.username, f"mouse@{DOMAIN}")
|
self.assertEqual(self.user.username, f"mouse@{DOMAIN}")
|
||||||
self.assertEqual(self.user.localname, "mouse")
|
self.assertEqual(self.user.localname, "mouse")
|
||||||
self.assertEqual(self.user.shared_inbox, f"{self.protocol}{DOMAIN}/inbox")
|
self.assertEqual(self.user.shared_inbox, f"{BASE_URL}/inbox")
|
||||||
self.assertEqual(self.user.inbox, f"{expected_id}/inbox")
|
self.assertEqual(self.user.inbox, f"{expected_id}/inbox")
|
||||||
self.assertEqual(self.user.outbox, f"{expected_id}/outbox")
|
self.assertEqual(self.user.outbox, f"{expected_id}/outbox")
|
||||||
self.assertEqual(self.user.followers_url, f"{expected_id}/followers")
|
self.assertEqual(self.user.followers_url, f"{expected_id}/followers")
|
||||||
|
@ -130,7 +127,7 @@ class User(TestCase):
|
||||||
patch("bookwyrm.lists_stream.populate_lists_task.delay"),
|
patch("bookwyrm.lists_stream.populate_lists_task.delay"),
|
||||||
):
|
):
|
||||||
user = models.User.objects.create_user(
|
user = models.User.objects.create_user(
|
||||||
f"test2{DOMAIN}",
|
"test2",
|
||||||
"test2@bookwyrm.test",
|
"test2@bookwyrm.test",
|
||||||
localname="test2",
|
localname="test2",
|
||||||
**user_attrs,
|
**user_attrs,
|
||||||
|
@ -145,7 +142,7 @@ class User(TestCase):
|
||||||
patch("bookwyrm.lists_stream.populate_lists_task.delay"),
|
patch("bookwyrm.lists_stream.populate_lists_task.delay"),
|
||||||
):
|
):
|
||||||
user = models.User.objects.create_user(
|
user = models.User.objects.create_user(
|
||||||
f"test1{DOMAIN}",
|
"test1",
|
||||||
"test1@bookwyrm.test",
|
"test1@bookwyrm.test",
|
||||||
localname="test1",
|
localname="test1",
|
||||||
**user_attrs,
|
**user_attrs,
|
||||||
|
|
|
@ -15,7 +15,7 @@ from django.utils.http import http_date
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.activitypub import Follow
|
from bookwyrm.activitypub import Follow
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN, NETLOC
|
||||||
from bookwyrm.signatures import create_key_pair, make_signature, make_digest
|
from bookwyrm.signatures import create_key_pair, make_signature, make_digest
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ class Signature(TestCase):
|
||||||
"HTTP_SIGNATURE": signature,
|
"HTTP_SIGNATURE": signature,
|
||||||
"HTTP_DIGEST": digest,
|
"HTTP_DIGEST": digest,
|
||||||
"HTTP_CONTENT_TYPE": "application/activity+json; charset=utf-8",
|
"HTTP_CONTENT_TYPE": "application/activity+json; charset=utf-8",
|
||||||
"HTTP_HOST": DOMAIN,
|
"HTTP_HOST": NETLOC,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import re
|
import re
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from bookwyrm.settings import BASE_URL
|
||||||
from bookwyrm.utils import regex
|
from bookwyrm.utils import regex
|
||||||
from bookwyrm.utils.validate import validate_url_domain
|
from bookwyrm.utils.validate import validate_url_domain
|
||||||
|
|
||||||
|
@ -15,17 +16,11 @@ class TestUtils(TestCase):
|
||||||
|
|
||||||
def test_valid_url_domain(self):
|
def test_valid_url_domain(self):
|
||||||
"""Check with a valid URL"""
|
"""Check with a valid URL"""
|
||||||
self.assertEqual(
|
legit = f"{BASE_URL}/legit-book-url/"
|
||||||
validate_url_domain("https://your.domain.here/legit-book-url/"),
|
self.assertEqual(validate_url_domain(legit), legit)
|
||||||
"https://your.domain.here/legit-book-url/",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_invalid_url_domain(self):
|
def test_invalid_url_domain(self):
|
||||||
"""Check with an invalid URL"""
|
"""Check with an invalid URL"""
|
||||||
self.assertIsNone(
|
self.assertIsNone(
|
||||||
validate_url_domain("https://up-to-no-good.tld/bad-actor.exe")
|
validate_url_domain("https://up-to-no-good.tld/bad-actor.exe")
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_default_url_domain(self):
|
|
||||||
"""Check with a default URL"""
|
|
||||||
self.assertEqual(validate_url_domain("/"), "/")
|
|
||||||
|
|
114
bookwyrm/tests/views/preferences/test_move.py
Normal file
114
bookwyrm/tests/views/preferences/test_move.py
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
""" test move functionality """
|
||||||
|
import json
|
||||||
|
from unittest.mock import patch
|
||||||
|
import pathlib
|
||||||
|
from django.contrib.sessions.middleware import SessionMiddleware
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
import responses
|
||||||
|
|
||||||
|
from bookwyrm import forms, models, views
|
||||||
|
|
||||||
|
|
||||||
|
@patch("bookwyrm.activitystreams.add_status_task.delay")
|
||||||
|
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||||
|
@patch("bookwyrm.activitystreams.populate_stream_task.delay")
|
||||||
|
@patch("bookwyrm.suggested_users.rerank_user_task.delay")
|
||||||
|
class ViewsHelpers(TestCase):
|
||||||
|
"""viewing and creating statuses"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
"""we need basic test data and mocks"""
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"),
|
||||||
|
patch("bookwyrm.activitystreams.populate_stream_task.delay"),
|
||||||
|
patch("bookwyrm.lists_stream.populate_lists_task.delay"),
|
||||||
|
patch("bookwyrm.suggested_users.rerank_user_task.delay"),
|
||||||
|
):
|
||||||
|
cls.local_user = models.User.objects.create_user(
|
||||||
|
"rat",
|
||||||
|
"rat@rat.com",
|
||||||
|
"ratword",
|
||||||
|
local=True,
|
||||||
|
discoverable=True,
|
||||||
|
localname="rat",
|
||||||
|
)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("bookwyrm.models.user.set_remote_server.delay"),
|
||||||
|
patch("bookwyrm.suggested_users.rerank_user_task.delay"),
|
||||||
|
):
|
||||||
|
cls.remote_user = models.User.objects.create_user(
|
||||||
|
"mouse@example.com",
|
||||||
|
"mouse@mouse.com",
|
||||||
|
"mouseword",
|
||||||
|
local=False,
|
||||||
|
remote_id="https://example.com/user/mouse",
|
||||||
|
)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""individual test setup"""
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
datafile = pathlib.Path(__file__).parent.joinpath(
|
||||||
|
"../../data/ap_user_move.json"
|
||||||
|
)
|
||||||
|
self.userdata = json.loads(datafile.read_bytes())
|
||||||
|
del self.userdata["icon"]
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
@patch("bookwyrm.models.user.set_remote_server.delay")
|
||||||
|
@patch("bookwyrm.suggested_users.remove_user_task.delay")
|
||||||
|
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
|
||||||
|
def test_move_user_view(self, *_):
|
||||||
|
"""move user"""
|
||||||
|
|
||||||
|
self.assertEqual(self.remote_user.remote_id, "https://example.com/user/mouse")
|
||||||
|
self.assertIsNone(self.local_user.moved_to)
|
||||||
|
self.assertIsNone(self.remote_user.moved_to)
|
||||||
|
self.assertIsNone(self.local_user.also_known_as.first())
|
||||||
|
self.assertIsNone(self.remote_user.also_known_as.first())
|
||||||
|
|
||||||
|
username = "mouse@example.com"
|
||||||
|
wellknown = {
|
||||||
|
"subject": "acct:mouse@example.com",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"rel": "self",
|
||||||
|
"type": "application/activity+json",
|
||||||
|
"href": "https://example.com/user/mouse",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
f"https://example.com/.well-known/webfinger?resource=acct:{username}",
|
||||||
|
json=wellknown,
|
||||||
|
status=200,
|
||||||
|
)
|
||||||
|
responses.add(
|
||||||
|
responses.GET,
|
||||||
|
"https://example.com/user/mouse",
|
||||||
|
json=self.userdata,
|
||||||
|
status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
view = views.MoveUser.as_view()
|
||||||
|
form = forms.MoveUserForm()
|
||||||
|
form.data["target"] = "mouse@example.com"
|
||||||
|
form.data["password"] = "ratword"
|
||||||
|
|
||||||
|
request = self.factory.post("", form.data)
|
||||||
|
request.user = self.local_user
|
||||||
|
middleware = SessionMiddleware()
|
||||||
|
middleware.process_request(request)
|
||||||
|
request.session.save()
|
||||||
|
|
||||||
|
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
|
||||||
|
view(request)
|
||||||
|
self.local_user.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(self.local_user.also_known_as.first(), self.remote_user)
|
||||||
|
self.assertEqual(self.remote_user.also_known_as.first(), self.local_user)
|
||||||
|
self.assertEqual(self.local_user.moved_to, "https://example.com/user/mouse")
|
|
@ -8,7 +8,7 @@ from django.test.client import RequestFactory
|
||||||
import responses
|
import responses
|
||||||
|
|
||||||
from bookwyrm import models, views
|
from bookwyrm import models, views
|
||||||
from bookwyrm.settings import USER_AGENT, DOMAIN
|
from bookwyrm.settings import USER_AGENT, BASE_URL
|
||||||
|
|
||||||
|
|
||||||
@patch("bookwyrm.activitystreams.add_status_task.delay")
|
@patch("bookwyrm.activitystreams.add_status_task.delay")
|
||||||
|
@ -288,13 +288,13 @@ class ViewsHelpers(TestCase): # pylint: disable=too-many-public-methods
|
||||||
def test_redirect_to_referer_valid_domain(self, *_):
|
def test_redirect_to_referer_valid_domain(self, *_):
|
||||||
"""redirect to within the app"""
|
"""redirect to within the app"""
|
||||||
request = self.factory.get("/path")
|
request = self.factory.get("/path")
|
||||||
request.META = {"HTTP_REFERER": f"https://{DOMAIN}/and/a/path"}
|
request.META = {"HTTP_REFERER": f"{BASE_URL}/and/a/path"}
|
||||||
result = views.helpers.redirect_to_referer(request)
|
result = views.helpers.redirect_to_referer(request)
|
||||||
self.assertEqual(result.url, f"https://{DOMAIN}/and/a/path")
|
self.assertEqual(result.url, f"{BASE_URL}/and/a/path")
|
||||||
|
|
||||||
def test_redirect_to_referer_with_get_args(self, *_):
|
def test_redirect_to_referer_with_get_args(self, *_):
|
||||||
"""if the path has get params (like sort) they are preserved"""
|
"""if the path has get params (like sort) they are preserved"""
|
||||||
request = self.factory.get("/path")
|
request = self.factory.get("/path")
|
||||||
request.META = {"HTTP_REFERER": f"https://{DOMAIN}/and/a/path?sort=hello"}
|
request.META = {"HTTP_REFERER": f"{BASE_URL}/and/a/path?sort=hello"}
|
||||||
result = views.helpers.redirect_to_referer(request)
|
result = views.helpers.redirect_to_referer(request)
|
||||||
self.assertEqual(result.url, f"https://{DOMAIN}/and/a/path?sort=hello")
|
self.assertEqual(result.url, f"{BASE_URL}/and/a/path?sort=hello")
|
||||||
|
|
|
@ -8,7 +8,7 @@ from django.test.client import RequestFactory
|
||||||
|
|
||||||
from bookwyrm import models, views
|
from bookwyrm import models, views
|
||||||
from bookwyrm.tests.validate_html import validate_html
|
from bookwyrm.tests.validate_html import validate_html
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import BASE_URL
|
||||||
|
|
||||||
|
|
||||||
class IsbnViews(TestCase):
|
class IsbnViews(TestCase):
|
||||||
|
@ -55,7 +55,7 @@ class IsbnViews(TestCase):
|
||||||
data = json.loads(response.content)
|
data = json.loads(response.content)
|
||||||
self.assertEqual(len(data), 1)
|
self.assertEqual(len(data), 1)
|
||||||
self.assertEqual(data[0]["title"], "Test Book")
|
self.assertEqual(data[0]["title"], "Test Book")
|
||||||
self.assertEqual(data[0]["key"], f"https://{DOMAIN}/book/{self.book.id}")
|
self.assertEqual(data[0]["key"], f"{BASE_URL}/book/{self.book.id}")
|
||||||
|
|
||||||
def test_isbn_html_response(self):
|
def test_isbn_html_response(self):
|
||||||
"""searches local data only and returns book data in json format"""
|
"""searches local data only and returns book data in json format"""
|
||||||
|
|
|
@ -132,3 +132,27 @@ class RssFeedView(TestCase):
|
||||||
self.assertEqual(result.status_code, 200)
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
self.assertIn(b"a sickening sense", result.content)
|
self.assertIn(b"a sickening sense", result.content)
|
||||||
|
|
||||||
|
def test_rss_shelf(self, *_):
|
||||||
|
"""load the rss feed of a shelf"""
|
||||||
|
with patch(
|
||||||
|
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
|
||||||
|
), patch("bookwyrm.activitystreams.add_book_statuses_task.delay"):
|
||||||
|
# make the shelf
|
||||||
|
shelf = models.Shelf.objects.create(
|
||||||
|
name="Test Shelf", identifier="test-shelf", user=self.local_user
|
||||||
|
)
|
||||||
|
# put the shelf on the book
|
||||||
|
models.ShelfBook.objects.create(
|
||||||
|
book=self.book,
|
||||||
|
shelf=shelf,
|
||||||
|
user=self.local_user,
|
||||||
|
)
|
||||||
|
view = rss_feed.RssShelfFeed()
|
||||||
|
request = self.factory.get("/user/books/test-shelf/rss")
|
||||||
|
request.user = self.local_user
|
||||||
|
result = view(
|
||||||
|
request, username=self.local_user.username, shelf_identifier="test-shelf"
|
||||||
|
)
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
self.assertIn(b"Example Edition", result.content)
|
||||||
|
|
|
@ -10,7 +10,7 @@ from django.test.client import RequestFactory
|
||||||
|
|
||||||
from bookwyrm import models, views
|
from bookwyrm import models, views
|
||||||
from bookwyrm.book_search import SearchResult
|
from bookwyrm.book_search import SearchResult
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import BASE_URL
|
||||||
from bookwyrm.tests.validate_html import validate_html
|
from bookwyrm.tests.validate_html import validate_html
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ class Views(TestCase):
|
||||||
data = json.loads(response.content)
|
data = json.loads(response.content)
|
||||||
self.assertEqual(len(data), 1)
|
self.assertEqual(len(data), 1)
|
||||||
self.assertEqual(data[0]["title"], "Test Book")
|
self.assertEqual(data[0]["title"], "Test Book")
|
||||||
self.assertEqual(data[0]["key"], f"https://{DOMAIN}/book/{self.book.id}")
|
self.assertEqual(data[0]["key"], f"{BASE_URL}/book/{self.book.id}")
|
||||||
|
|
||||||
def test_search_no_query(self):
|
def test_search_no_query(self):
|
||||||
"""just the search page"""
|
"""just the search page"""
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||||
from django.urls import path, re_path
|
from django.urls import path, re_path, include
|
||||||
from django.views.generic.base import TemplateView
|
from django.views.generic.base import TemplateView
|
||||||
|
|
||||||
from bookwyrm import settings, views
|
from bookwyrm import settings, views
|
||||||
|
@ -577,11 +577,21 @@ urlpatterns = [
|
||||||
views.Shelf.as_view(),
|
views.Shelf.as_view(),
|
||||||
name="shelf",
|
name="shelf",
|
||||||
),
|
),
|
||||||
|
re_path(
|
||||||
|
rf"^{USER_PATH}/(shelf|books)/(?P<shelf_identifier>[\w-]+)/rss/?$",
|
||||||
|
views.rss_feed.RssShelfFeed(),
|
||||||
|
name="shelf-rss",
|
||||||
|
),
|
||||||
re_path(
|
re_path(
|
||||||
rf"^{LOCAL_USER_PATH}/(books|shelf)/(?P<shelf_identifier>[\w-]+)(.json)?/?$",
|
rf"^{LOCAL_USER_PATH}/(books|shelf)/(?P<shelf_identifier>[\w-]+)(.json)?/?$",
|
||||||
views.Shelf.as_view(),
|
views.Shelf.as_view(),
|
||||||
name="shelf",
|
name="shelf",
|
||||||
),
|
),
|
||||||
|
re_path(
|
||||||
|
rf"^{LOCAL_USER_PATH}/(books|shelf)/(?P<shelf_identifier>[\w-]+)/rss/?$",
|
||||||
|
views.rss_feed.RssShelfFeed(),
|
||||||
|
name="shelf-rss",
|
||||||
|
),
|
||||||
re_path(r"^create-shelf/?$", views.create_shelf, name="shelf-create"),
|
re_path(r"^create-shelf/?$", views.create_shelf, name="shelf-create"),
|
||||||
re_path(r"^delete-shelf/(?P<shelf_id>\d+)/?$", views.delete_shelf),
|
re_path(r"^delete-shelf/(?P<shelf_id>\d+)/?$", views.delete_shelf),
|
||||||
re_path(r"^shelve/?$", views.shelve),
|
re_path(r"^shelve/?$", views.shelve),
|
||||||
|
@ -829,6 +839,7 @@ urlpatterns = [
|
||||||
r"^summary_revoke_key/?$", views.summary_revoke_key, name="summary-revoke-key"
|
r"^summary_revoke_key/?$", views.summary_revoke_key, name="summary-revoke-key"
|
||||||
),
|
),
|
||||||
path("guided-tour/<tour>", views.toggle_guided_tour),
|
path("guided-tour/<tour>", views.toggle_guided_tour),
|
||||||
|
re_path(r"^o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
# Serves /static when DEBUG is true.
|
# Serves /static when DEBUG is true.
|
||||||
|
|
|
@ -1,21 +1,15 @@
|
||||||
"""Validations"""
|
"""Validations"""
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from bookwyrm.settings import DOMAIN, USE_HTTPS
|
from bookwyrm.settings import BASE_URL
|
||||||
|
|
||||||
|
|
||||||
def validate_url_domain(url: str) -> Optional[str]:
|
def validate_url_domain(url: Optional[str]) -> Optional[str]:
|
||||||
"""Basic check that the URL starts with the instance domain name"""
|
"""Basic check that the URL starts with the instance domain name"""
|
||||||
if not url:
|
if url is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if url == "/":
|
if not url.startswith(BASE_URL):
|
||||||
return url
|
return None
|
||||||
|
|
||||||
protocol = "https://" if USE_HTTPS else "http://"
|
return url
|
||||||
origin = f"{protocol}{DOMAIN}"
|
|
||||||
|
|
||||||
if url.startswith(origin):
|
|
||||||
return url
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ def is_bookwyrm_request(request):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def handle_remote_webfinger(query, unknown_only=False):
|
def handle_remote_webfinger(query, unknown_only=False, refresh=False):
|
||||||
"""webfingerin' other servers"""
|
"""webfingerin' other servers"""
|
||||||
user = None
|
user = None
|
||||||
|
|
||||||
|
@ -76,6 +76,11 @@ def handle_remote_webfinger(query, unknown_only=False):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
|
if refresh:
|
||||||
|
# Always fetch the remote info - don't even bother checking the DB
|
||||||
|
raise models.User.DoesNotExist("remote_only is set to True")
|
||||||
|
|
||||||
user = models.User.objects.get(username__iexact=query)
|
user = models.User.objects.get(username__iexact=query)
|
||||||
|
|
||||||
if unknown_only:
|
if unknown_only:
|
||||||
|
@ -93,7 +98,7 @@ def handle_remote_webfinger(query, unknown_only=False):
|
||||||
if link.get("rel") == "self":
|
if link.get("rel") == "self":
|
||||||
try:
|
try:
|
||||||
user = activitypub.resolve_remote_id(
|
user = activitypub.resolve_remote_id(
|
||||||
link["href"], model=models.User
|
link["href"], model=models.User, refresh=refresh
|
||||||
)
|
)
|
||||||
except (KeyError, activitypub.ActivitySerializerError):
|
except (KeyError, activitypub.ActivitySerializerError):
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -32,7 +32,7 @@ class MoveUser(View):
|
||||||
|
|
||||||
if form.is_valid() and user.check_password(form.cleaned_data["password"]):
|
if form.is_valid() and user.check_password(form.cleaned_data["password"]):
|
||||||
username = form.cleaned_data["target"]
|
username = form.cleaned_data["target"]
|
||||||
target = handle_remote_webfinger(username)
|
target = handle_remote_webfinger(username, refresh=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
models.MoveUser.objects.create(
|
models.MoveUser.objects.create(
|
||||||
|
@ -81,6 +81,7 @@ class AliasUser(View):
|
||||||
return TemplateResponse(request, "preferences/alias_user.html", data)
|
return TemplateResponse(request, "preferences/alias_user.html", data)
|
||||||
|
|
||||||
user.also_known_as.add(remote_user.id)
|
user.also_known_as.add(remote_user.id)
|
||||||
|
user.save(broadcast=True) # broadcast the alias
|
||||||
|
|
||||||
return redirect("prefs-alias")
|
return redirect("prefs-alias")
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
from django.contrib.syndication.views import Feed
|
from django.contrib.syndication.views import Feed
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from ..models import Review, Quotation, Comment
|
from ..models import Review, Quotation, Comment
|
||||||
|
|
||||||
from .helpers import get_user_from_username
|
from .helpers import get_user_from_username
|
||||||
|
@ -177,3 +178,61 @@ class RssCommentsOnlyFeed(Feed):
|
||||||
def item_pubdate(self, item):
|
def item_pubdate(self, item):
|
||||||
"""publication date of the item"""
|
"""publication date of the item"""
|
||||||
return item.published_date
|
return item.published_date
|
||||||
|
|
||||||
|
|
||||||
|
class RssShelfFeed(Feed):
|
||||||
|
"""serialize a shelf activity in rss"""
|
||||||
|
|
||||||
|
description_template = "rss/edition.html"
|
||||||
|
|
||||||
|
def item_title(self, item):
|
||||||
|
"""render the item title"""
|
||||||
|
authors = item.authors
|
||||||
|
if item.author_text:
|
||||||
|
authors.display_name = f"{item.author_text}:"
|
||||||
|
else:
|
||||||
|
authors.description = ""
|
||||||
|
template = get_template("rss/title.html")
|
||||||
|
return template.render({"user": authors, "item_title": item.title}).strip()
|
||||||
|
|
||||||
|
def get_object(
|
||||||
|
self, request, shelf_identifier, username
|
||||||
|
): # pylint: disable=arguments-differ
|
||||||
|
"""the shelf that gets serialized"""
|
||||||
|
user = get_user_from_username(request.user, username)
|
||||||
|
# always get privacy, don't support rss over anything private
|
||||||
|
# get the SHELF of the object
|
||||||
|
shelf = get_object_or_404(
|
||||||
|
user.shelf_set,
|
||||||
|
identifier=shelf_identifier,
|
||||||
|
privacy__in=["public", "unlisted"],
|
||||||
|
)
|
||||||
|
shelf.raise_visible_to_user(request.user)
|
||||||
|
return shelf
|
||||||
|
|
||||||
|
def link(self, obj):
|
||||||
|
"""link to the shelf"""
|
||||||
|
return obj.local_path
|
||||||
|
|
||||||
|
def title(self, obj):
|
||||||
|
"""title of the rss feed entry"""
|
||||||
|
return _(f"{obj.user.display_name}’s {obj.name} shelf")
|
||||||
|
|
||||||
|
def items(self, obj):
|
||||||
|
"""the user's activity feed"""
|
||||||
|
return obj.books.order_by("-shelfbook__shelved_date")[:10]
|
||||||
|
|
||||||
|
def item_link(self, item):
|
||||||
|
"""link to the status"""
|
||||||
|
return item.local_path
|
||||||
|
|
||||||
|
def item_pubdate(self, item):
|
||||||
|
"""publication date of the item"""
|
||||||
|
return item.published_date
|
||||||
|
|
||||||
|
def description(self, obj):
|
||||||
|
"""description of the shelf including the shelf name and user."""
|
||||||
|
# if there's a description, lets add it. Not everyone puts a description in.
|
||||||
|
if desc := obj.description:
|
||||||
|
return _(f"{obj.user.display_name}’s {obj.name} shelf: {desc}")
|
||||||
|
return _(f"Books added to {obj.user.name}’s {obj.name} shelf")
|
||||||
|
|
|
@ -9,7 +9,7 @@ from django.utils import timezone
|
||||||
from django.views.decorators.http import require_GET
|
from django.views.decorators.http import require_GET
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.settings import DOMAIN, VERSION, LANGUAGE_CODE
|
from bookwyrm.settings import BASE_URL, DOMAIN, VERSION, LANGUAGE_CODE
|
||||||
|
|
||||||
|
|
||||||
@require_GET
|
@require_GET
|
||||||
|
@ -34,7 +34,7 @@ def webfinger(request):
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rel": "http://ostatus.org/schema/1.0/subscribe",
|
"rel": "http://ostatus.org/schema/1.0/subscribe",
|
||||||
"template": f"https://{DOMAIN}/ostatus_subscribe?acct={{uri}}",
|
"template": f"{BASE_URL}/ostatus_subscribe?acct={{uri}}",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,19 +11,20 @@ env =
|
||||||
DEBUG = false
|
DEBUG = false
|
||||||
USE_HTTPS = true
|
USE_HTTPS = true
|
||||||
DOMAIN = your.domain.here
|
DOMAIN = your.domain.here
|
||||||
|
PORT = 4242
|
||||||
ALLOWED_HOSTS = your.domain.here
|
ALLOWED_HOSTS = your.domain.here
|
||||||
BOOKWYRM_DATABASE_BACKEND = postgres
|
BOOKWYRM_DATABASE_BACKEND = postgres
|
||||||
MEDIA_ROOT = images/
|
MEDIA_ROOT = images/
|
||||||
CELERY_BROKER = ""
|
CELERY_BROKER =
|
||||||
REDIS_BROKER_PORT = 6379
|
REDIS_BROKER_PORT = 6379
|
||||||
REDIS_BROKER_PASSWORD = beep
|
REDIS_BROKER_PASSWORD = beep
|
||||||
REDIS_ACTIVITY_PORT = 6379
|
REDIS_ACTIVITY_PORT = 6379
|
||||||
REDIS_ACTIVITY_PASSWORD = beep
|
REDIS_ACTIVITY_PASSWORD = beep
|
||||||
USE_DUMMY_CACHE = true
|
USE_DUMMY_CACHE = true
|
||||||
FLOWER_PORT = 8888
|
FLOWER_PORT = 8888
|
||||||
EMAIL_HOST = "smtp.mailgun.org"
|
EMAIL_HOST = smtp.mailgun.org
|
||||||
EMAIL_PORT = 587
|
EMAIL_PORT = 587
|
||||||
EMAIL_HOST_USER = ""
|
EMAIL_HOST_USER =
|
||||||
EMAIL_HOST_PASSWORD = ""
|
EMAIL_HOST_PASSWORD =
|
||||||
EMAIL_USE_TLS = true
|
EMAIL_USE_TLS = true
|
||||||
ENABLE_PREVIEW_IMAGES = false
|
ENABLE_PREVIEW_IMAGES = false
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
aiohttp==3.9.2
|
aiohttp==3.9.4
|
||||||
bleach==5.0.1
|
bleach==5.0.1
|
||||||
boto3==1.26.57
|
boto3==1.26.57
|
||||||
bw-file-resubmit==0.6.0rc2
|
bw-file-resubmit==0.6.0rc2
|
||||||
|
@ -10,6 +10,7 @@ django-compressor==4.4
|
||||||
django-csp==3.7
|
django-csp==3.7
|
||||||
django-imagekit==4.1.0
|
django-imagekit==4.1.0
|
||||||
django-model-utils==4.3.1
|
django-model-utils==4.3.1
|
||||||
|
django-oauth-toolkit==2.3.0
|
||||||
django-pgtrigger==4.11.0
|
django-pgtrigger==4.11.0
|
||||||
django-redis==5.2.0
|
django-redis==5.2.0
|
||||||
django-sass-processor==1.2.2
|
django-sass-processor==1.2.2
|
||||||
|
|
Loading…
Reference in a new issue