mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-05-20 17:28:55 +00:00
0d621b68e0
Accessing many-to-many relations before saving is no longer allowed. Reorder all operations consistently: 1. Validations 2. Modify own fields 3. Perform save by calling super().save() 4. Modify related objects and clear caches Especially clearing caches should be done after actually saving, otherwise the old data can be re-added immediately by another request before the new data is written.
264 lines
9.4 KiB
Python
264 lines
9.4 KiB
Python
""" the particulars for this instance of BookWyrm """
|
|
import datetime
|
|
from urllib.parse import urljoin
|
|
import uuid
|
|
|
|
import django.contrib.auth.models as auth_models
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.db import models, IntegrityError
|
|
from django.dispatch import receiver
|
|
from django.utils import timezone
|
|
from model_utils import FieldTracker
|
|
|
|
from bookwyrm.connectors.abstract_connector import get_data
|
|
from bookwyrm.preview_images import generate_site_preview_image_task
|
|
from bookwyrm.settings import BASE_URL, ENABLE_PREVIEW_IMAGES, STATIC_FULL_URL
|
|
from bookwyrm.settings import RELEASE_API
|
|
from bookwyrm.tasks import app, MISC
|
|
from .base_model import BookWyrmModel, new_access_code
|
|
from .user import User
|
|
from .fields import get_absolute_url
|
|
|
|
|
|
class SiteModel(models.Model):
|
|
"""we just need edit perms"""
|
|
|
|
class Meta:
|
|
"""this is just here to provide default fields for other models"""
|
|
|
|
abstract = True
|
|
|
|
# pylint: disable=no-self-use
|
|
def raise_not_editable(self, viewer):
|
|
"""Check if the user has the right permissions"""
|
|
if viewer.has_perm("bookwyrm.edit_instance_settings"):
|
|
return
|
|
raise PermissionDenied()
|
|
|
|
|
|
class SiteSettings(SiteModel):
|
|
"""customized settings for this instance"""
|
|
|
|
name = models.CharField(default="BookWyrm", max_length=100)
|
|
instance_tagline = models.CharField(
|
|
max_length=150, default="Social Reading and Reviewing"
|
|
)
|
|
instance_description = models.TextField(default="This instance has no description.")
|
|
instance_short_description = models.CharField(max_length=255, blank=True, null=True)
|
|
default_theme = models.ForeignKey(
|
|
"Theme", null=True, blank=True, on_delete=models.SET_NULL
|
|
)
|
|
available_version = models.CharField(null=True, blank=True, max_length=10)
|
|
|
|
# admin setup options
|
|
install_mode = models.BooleanField(default=False)
|
|
admin_code = models.CharField(max_length=50, default=uuid.uuid4)
|
|
|
|
# about page
|
|
registration_closed_text = models.TextField(
|
|
default="We aren't taking new users at this time. You can find an open "
|
|
'instance at <a href="https://joinbookwyrm.com/instances">'
|
|
"joinbookwyrm.com/instances</a>."
|
|
)
|
|
invite_request_text = models.TextField(
|
|
default="If your request is approved, you will receive an email with a "
|
|
"registration link."
|
|
)
|
|
code_of_conduct = models.TextField(default="Add a code of conduct here.")
|
|
privacy_policy = models.TextField(default="Add a privacy policy here.")
|
|
impressum = models.TextField(default="Add a impressum here.")
|
|
show_impressum = models.BooleanField(default=False)
|
|
|
|
# registration
|
|
allow_registration = models.BooleanField(default=False)
|
|
allow_invite_requests = models.BooleanField(default=True)
|
|
invite_request_question = models.BooleanField(default=False)
|
|
require_confirm_email = models.BooleanField(default=True)
|
|
default_user_auth_group = models.ForeignKey(
|
|
auth_models.Group, null=True, blank=True, on_delete=models.RESTRICT
|
|
)
|
|
|
|
invite_question_text = models.CharField(
|
|
max_length=255, blank=True, default="What is your favourite book?"
|
|
)
|
|
# images
|
|
logo = models.ImageField(upload_to="logos/", null=True, blank=True)
|
|
logo_small = models.ImageField(upload_to="logos/", null=True, blank=True)
|
|
favicon = models.ImageField(upload_to="logos/", null=True, blank=True)
|
|
preview_image = models.ImageField(
|
|
upload_to="previews/logos/", null=True, blank=True
|
|
)
|
|
|
|
# footer
|
|
support_link = models.CharField(max_length=255, null=True, blank=True)
|
|
support_title = models.CharField(max_length=100, null=True, blank=True)
|
|
admin_email = models.EmailField(max_length=255, null=True, blank=True)
|
|
footer_item = models.TextField(null=True, blank=True)
|
|
|
|
# controls
|
|
imports_enabled = models.BooleanField(default=True)
|
|
import_size_limit = models.IntegerField(default=0)
|
|
import_limit_reset = models.IntegerField(default=0)
|
|
user_exports_enabled = models.BooleanField(default=False)
|
|
user_import_time_limit = models.IntegerField(default=48)
|
|
|
|
field_tracker = FieldTracker(fields=["name", "instance_tagline", "logo"])
|
|
|
|
@classmethod
|
|
def get(cls):
|
|
"""gets the site settings db entry or defaults"""
|
|
try:
|
|
return cls.objects.get(id=1)
|
|
except cls.DoesNotExist:
|
|
default_settings = SiteSettings(id=1)
|
|
default_settings.save()
|
|
return default_settings
|
|
|
|
@property
|
|
def logo_url(self):
|
|
"""helper to build the logo url"""
|
|
return self.get_url("logo", "images/logo.png")
|
|
|
|
@property
|
|
def logo_small_url(self):
|
|
"""helper to build the logo url"""
|
|
return self.get_url("logo_small", "images/logo-small.png")
|
|
|
|
@property
|
|
def favicon_url(self):
|
|
"""helper to build the logo url"""
|
|
return self.get_url("favicon", "images/favicon.png")
|
|
|
|
def get_url(self, field, default_path):
|
|
"""get a media url or a default static path"""
|
|
uploaded = getattr(self, field, None)
|
|
if uploaded:
|
|
return get_absolute_url(uploaded)
|
|
return urljoin(STATIC_FULL_URL, default_path)
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""if require_confirm_email is disabled, make sure no users are pending,
|
|
if enabled, make sure invite_question_text is not empty"""
|
|
if not self.invite_question_text:
|
|
self.invite_question_text = "What is your favourite book?"
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
if not self.require_confirm_email:
|
|
User.objects.filter(is_active=False, deactivation_reason="pending").update(
|
|
is_active=True, deactivation_reason=None
|
|
)
|
|
|
|
|
|
class Theme(SiteModel):
|
|
"""Theme files"""
|
|
|
|
created_date = models.DateTimeField(auto_now_add=True)
|
|
name = models.CharField(max_length=50, unique=True)
|
|
path = models.CharField(max_length=50, unique=True)
|
|
loads = models.BooleanField(null=True, blank=True)
|
|
|
|
def __str__(self):
|
|
# pylint: disable=invalid-str-returned
|
|
return self.name
|
|
|
|
|
|
class SiteInvite(models.Model):
|
|
"""gives someone access to create an account on the instance"""
|
|
|
|
created_date = models.DateTimeField(auto_now_add=True)
|
|
code = models.CharField(max_length=32, default=new_access_code)
|
|
expiry = models.DateTimeField(blank=True, null=True)
|
|
use_limit = models.IntegerField(blank=True, null=True)
|
|
times_used = models.IntegerField(default=0)
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
invitees = models.ManyToManyField(User, related_name="invitees")
|
|
|
|
# pylint: disable=no-self-use
|
|
def raise_not_editable(self, viewer):
|
|
"""Admins only"""
|
|
if viewer.has_perm("bookwyrm.create_invites"):
|
|
return
|
|
raise PermissionDenied()
|
|
|
|
def valid(self):
|
|
"""make sure it hasn't expired or been used"""
|
|
return (self.expiry is None or self.expiry > timezone.now()) and (
|
|
self.use_limit is None or self.times_used < self.use_limit
|
|
)
|
|
|
|
@property
|
|
def link(self):
|
|
"""formats the invite link"""
|
|
return f"{BASE_URL}/invite/{self.code}"
|
|
|
|
|
|
class InviteRequest(BookWyrmModel):
|
|
"""prospective users can request an invite"""
|
|
|
|
email = models.EmailField(max_length=255, unique=True)
|
|
invite = models.ForeignKey(
|
|
SiteInvite, on_delete=models.SET_NULL, null=True, blank=True
|
|
)
|
|
answer = models.TextField(max_length=255, unique=False, null=True, blank=True)
|
|
invite_sent = models.BooleanField(default=False)
|
|
ignored = models.BooleanField(default=False)
|
|
|
|
def raise_not_editable(self, viewer):
|
|
"""Only check perms on edit, not create"""
|
|
if not self.id or viewer.has_perm("bookwyrm.create_invites"):
|
|
return
|
|
raise PermissionDenied()
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""don't create a request for a registered email"""
|
|
if not self.id and User.objects.filter(email=self.email).exists():
|
|
raise IntegrityError()
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
def get_password_reset_expiry():
|
|
"""give people a limited time to use the link"""
|
|
now = timezone.now()
|
|
return now + datetime.timedelta(days=1)
|
|
|
|
|
|
class PasswordReset(models.Model):
|
|
"""gives someone access to create an account on the instance"""
|
|
|
|
code = models.CharField(max_length=32, default=new_access_code)
|
|
expiry = models.DateTimeField(default=get_password_reset_expiry)
|
|
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
|
|
|
def valid(self):
|
|
"""make sure it hasn't expired or been used"""
|
|
return self.expiry > timezone.now()
|
|
|
|
@property
|
|
def link(self):
|
|
"""formats the invite link"""
|
|
return f"{BASE_URL}/password-reset/{self.code}"
|
|
|
|
|
|
# pylint: disable=unused-argument
|
|
@receiver(models.signals.post_save, sender=SiteSettings)
|
|
def preview_image(instance, *args, **kwargs):
|
|
"""Update image preview for the default site image"""
|
|
if not ENABLE_PREVIEW_IMAGES:
|
|
return
|
|
changed_fields = instance.field_tracker.changed()
|
|
|
|
if len(changed_fields) > 0:
|
|
generate_site_preview_image_task.delay()
|
|
|
|
|
|
@app.task(queue=MISC)
|
|
def check_for_updates_task():
|
|
"""See if git remote knows about a new version"""
|
|
site = SiteSettings.objects.get()
|
|
release = get_data(RELEASE_API, timeout=3)
|
|
available_version = release.get("tag_name", None)
|
|
if available_version:
|
|
site.available_version = available_version
|
|
site.save(update_fields=["available_version"])
|