bookwyrm/bookwyrm/models/site.py

267 lines
9.6 KiB
Python

""" the particulars for this instance of BookWyrm """
import datetime
from typing import Optional, Iterable
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 bookwyrm.utils.db import add_update_fields
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, update_fields: Optional[Iterable[str]] = None, **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?"
update_fields = add_update_fields(update_fields, "invite_question_text")
super().save(*args, update_fields=update_fields, **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"])