""" 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 ' "joinbookwyrm.com/instances." ) 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"])