Merge pull request #2258 from bookwyrm-social/form-perms

Check permissions automatically on form save
This commit is contained in:
Mouse Reeve 2022-09-19 13:32:41 -07:00 committed by GitHub
commit fdc477afdf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 291 additions and 68 deletions

View file

@ -2,13 +2,14 @@
import datetime import datetime
from django import forms from django import forms
from django.core.exceptions import PermissionDenied
from django.forms import widgets from django.forms import widgets
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_celery_beat.models import IntervalSchedule from django_celery_beat.models import IntervalSchedule
from bookwyrm import models from bookwyrm import models
from .custom_form import CustomForm from .custom_form import CustomForm, StyledForm
# pylint: disable=missing-class-docstring # pylint: disable=missing-class-docstring
@ -130,7 +131,7 @@ class AutoModRuleForm(CustomForm):
fields = ["string_match", "flag_users", "flag_statuses", "created_by"] fields = ["string_match", "flag_users", "flag_statuses", "created_by"]
class IntervalScheduleForm(CustomForm): class IntervalScheduleForm(StyledForm):
class Meta: class Meta:
model = IntervalSchedule model = IntervalSchedule
fields = ["every", "period"] fields = ["every", "period"]
@ -139,3 +140,10 @@ class IntervalScheduleForm(CustomForm):
"every": forms.NumberInput(attrs={"aria-describedby": "desc_every"}), "every": forms.NumberInput(attrs={"aria-describedby": "desc_every"}),
"period": forms.Select(attrs={"aria-describedby": "desc_period"}), "period": forms.Select(attrs={"aria-describedby": "desc_period"}),
} }
# pylint: disable=arguments-differ
def save(self, request, *args, **kwargs):
"""This is an outside model so the perms check works differently"""
if not request.user.has_perm("bookwyrm.moderate_user"):
raise PermissionDenied()
return super().save(*args, **kwargs)

View file

@ -4,7 +4,7 @@ from django.forms import ModelForm
from django.forms.widgets import Textarea from django.forms.widgets import Textarea
class CustomForm(ModelForm): class StyledForm(ModelForm):
"""add css classes to the forms""" """add css classes to the forms"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -16,7 +16,7 @@ class CustomForm(ModelForm):
css_classes["checkbox"] = "checkbox" css_classes["checkbox"] = "checkbox"
css_classes["textarea"] = "textarea" css_classes["textarea"] = "textarea"
# pylint: disable=super-with-arguments # pylint: disable=super-with-arguments
super(CustomForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
for visible in self.visible_fields(): for visible in self.visible_fields():
if hasattr(visible.field.widget, "input_type"): if hasattr(visible.field.widget, "input_type"):
input_type = visible.field.widget.input_type input_type = visible.field.widget.input_type
@ -24,3 +24,13 @@ class CustomForm(ModelForm):
input_type = "textarea" input_type = "textarea"
visible.field.widget.attrs["rows"] = 5 visible.field.widget.attrs["rows"] = 5
visible.field.widget.attrs["class"] = css_classes[input_type] visible.field.widget.attrs["class"] = css_classes[input_type]
class CustomForm(StyledForm):
"""Check permissions on save"""
# pylint: disable=arguments-differ
def save(self, request, *args, **kwargs):
"""Save and check perms"""
self.instance.raise_not_editable(request.user)
return super().save(*args, **kwargs)

View file

@ -0,0 +1,65 @@
# Generated by Django 3.2.15 on 2022-09-19 16:34
import bookwyrm.models.fields
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0157_auto_20220909_2338"),
]
operations = [
migrations.AddField(
model_name="automod",
name="created_date",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="automod",
name="remote_id",
field=bookwyrm.models.fields.RemoteIdField(
max_length=255,
null=True,
validators=[bookwyrm.models.fields.validate_remote_id],
),
),
migrations.AddField(
model_name="automod",
name="updated_date",
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name="emailblocklist",
name="remote_id",
field=bookwyrm.models.fields.RemoteIdField(
max_length=255,
null=True,
validators=[bookwyrm.models.fields.validate_remote_id],
),
),
migrations.AddField(
model_name="emailblocklist",
name="updated_date",
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name="ipblocklist",
name="remote_id",
field=bookwyrm.models.fields.RemoteIdField(
max_length=255,
null=True,
validators=[bookwyrm.models.fields.validate_remote_id],
),
),
migrations.AddField(
model_name="ipblocklist",
name="updated_date",
field=models.DateTimeField(auto_now=True),
),
]

View file

@ -3,18 +3,33 @@ from functools import reduce
import operator import operator
from django.apps import apps from django.apps import apps
from django.core.exceptions import PermissionDenied
from django.db import models, transaction from django.db import models, transaction
from django.db.models import Q from django.db.models import Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from bookwyrm.tasks import app from bookwyrm.tasks import app
from .base_model import BookWyrmModel
from .user import User from .user import User
class EmailBlocklist(models.Model): class AdminModel(BookWyrmModel):
"""Overrides the permissions methods"""
class Meta:
"""this is just here to provide default fields for other models"""
abstract = True
def raise_not_editable(self, viewer):
if viewer.has_perm("bookwyrm.moderate_user"):
return
raise PermissionDenied()
class EmailBlocklist(AdminModel):
"""blocked email addresses""" """blocked email addresses"""
created_date = models.DateTimeField(auto_now_add=True)
domain = models.CharField(max_length=255, unique=True) domain = models.CharField(max_length=255, unique=True)
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
@ -29,10 +44,9 @@ class EmailBlocklist(models.Model):
return User.objects.filter(email__endswith=f"@{self.domain}") return User.objects.filter(email__endswith=f"@{self.domain}")
class IPBlocklist(models.Model): class IPBlocklist(AdminModel):
"""blocked ip addresses""" """blocked ip addresses"""
created_date = models.DateTimeField(auto_now_add=True)
address = models.CharField(max_length=255, unique=True) address = models.CharField(max_length=255, unique=True)
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
@ -42,7 +56,7 @@ class IPBlocklist(models.Model):
ordering = ("-created_date",) ordering = ("-created_date",)
class AutoMod(models.Model): class AutoMod(AdminModel):
"""rules to automatically flag suspicious activity""" """rules to automatically flag suspicious activity"""
string_match = models.CharField(max_length=200, unique=True) string_match = models.CharField(max_length=200, unique=True)

View file

@ -1,5 +1,7 @@
""" flagged for moderation """ """ flagged for moderation """
from django.core.exceptions import PermissionDenied
from django.db import models from django.db import models
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
@ -21,6 +23,12 @@ class Report(BookWyrmModel):
links = models.ManyToManyField("Link", blank=True) links = models.ManyToManyField("Link", blank=True)
resolved = models.BooleanField(default=False) resolved = models.BooleanField(default=False)
def raise_not_editable(self, viewer):
"""instead of user being the owner field, it's reporter"""
if self.reporter == viewer or viewer.has_perm("bookwyrm.moderate_user"):
return
raise PermissionDenied()
def get_remote_id(self): def get_remote_id(self):
return f"https://{DOMAIN}/settings/reports/{self.id}" return f"https://{DOMAIN}/settings/reports/{self.id}"

View file

@ -3,6 +3,7 @@ import datetime
from urllib.parse import urljoin from urllib.parse import urljoin
import uuid import uuid
from django.core.exceptions import PermissionDenied
from django.db import models, IntegrityError from django.db import models, IntegrityError
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
@ -15,7 +16,23 @@ from .user import User
from .fields import get_absolute_url from .fields import get_absolute_url
class SiteSettings(models.Model): 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""" """customized settings for this instance"""
name = models.CharField(default="BookWyrm", max_length=100) name = models.CharField(default="BookWyrm", max_length=100)
@ -115,7 +132,7 @@ class SiteSettings(models.Model):
super().save(*args, **kwargs) super().save(*args, **kwargs)
class Theme(models.Model): class Theme(SiteModel):
"""Theme files""" """Theme files"""
created_date = models.DateTimeField(auto_now_add=True) created_date = models.DateTimeField(auto_now_add=True)
@ -138,6 +155,13 @@ class SiteInvite(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
invitees = models.ManyToManyField(User, related_name="invitees") 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): def valid(self):
"""make sure it hasn't expired or been used""" """make sure it hasn't expired or been used"""
return (self.expiry is None or self.expiry > timezone.now()) and ( return (self.expiry is None or self.expiry > timezone.now()) and (
@ -161,6 +185,12 @@ class InviteRequest(BookWyrmModel):
invite_sent = models.BooleanField(default=False) invite_sent = models.BooleanField(default=False)
ignored = 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): def save(self, *args, **kwargs):
"""don't create a request for a registered email""" """don't create a request for a registered email"""
if not self.id and User.objects.filter(email=self.email).exists(): if not self.id and User.objects.filter(email=self.email).exists():

View file

@ -5,6 +5,7 @@ from urllib.parse import urlparse
from django.apps import apps from django.apps import apps
from django.contrib.auth.models import AbstractUser, Group from django.contrib.auth.models import AbstractUser, Group
from django.contrib.postgres.fields import ArrayField, CICharField from django.contrib.postgres.fields import ArrayField, CICharField
from django.core.exceptions import PermissionDenied
from django.dispatch import receiver from django.dispatch import receiver
from django.db import models, transaction from django.db import models, transaction
from django.utils import timezone from django.utils import timezone
@ -401,6 +402,12 @@ class User(OrderedCollectionPageMixin, AbstractUser):
editable=False, editable=False,
).save(broadcast=False) ).save(broadcast=False)
def raise_not_editable(self, viewer):
"""Who can edit the user object?"""
if self == viewer or viewer.has_perm("bookwyrm.moderate_user"):
return
raise PermissionDenied()
class KeyPair(ActivitypubMixin, BookWyrmModel): class KeyPair(ActivitypubMixin, BookWyrmModel):
"""public and private keys for a user""" """public and private keys for a user"""

View file

@ -54,7 +54,6 @@
method="POST" method="POST"
action="{% url 'settings-themes' %}" action="{% url 'settings-themes' %}"
class="box" class="box"
enctype="multipart/form-data"
> >
<fieldset> <fieldset>
{% csrf_token %} {% csrf_token %}

View file

@ -15,6 +15,7 @@ from bookwyrm.tests.validate_html import validate_html
class AutomodViews(TestCase): class AutomodViews(TestCase):
"""every response to a get request, html or json""" """every response to a get request, html or json"""
# pylint: disable=invalid-name
def setUp(self): def setUp(self):
"""we need basic test data and mocks""" """we need basic test data and mocks"""
self.factory = RequestFactory() self.factory = RequestFactory()

View file

@ -17,6 +17,7 @@ from bookwyrm.tests.validate_html import validate_html
class FederationViews(TestCase): class FederationViews(TestCase):
"""every response to a get request, html or json""" """every response to a get request, html or json"""
# pylint: disable=invalid-name
def setUp(self): def setUp(self):
"""we need basic test data and mocks""" """we need basic test data and mocks"""
self.factory = RequestFactory() self.factory = RequestFactory()

View file

@ -0,0 +1,88 @@
""" test for app action functionality """
from unittest.mock import patch
from django.contrib.auth.models import Group
from django.core.exceptions import PermissionDenied
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import forms, models, views
from bookwyrm.management.commands import initdb
from bookwyrm.tests.validate_html import validate_html
class AdminThemesViews(TestCase):
"""Edit site settings"""
# pylint: disable=invalid-name
def setUp(self):
"""we need basic test data and mocks"""
self.factory = RequestFactory()
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
self.local_user = models.User.objects.create_user(
"mouse@local.com",
"mouse@mouse.mouse",
"password",
local=True,
localname="mouse",
)
self.another_user = models.User.objects.create_user(
"rat@local.com",
"rat@rat.rat",
"password",
local=True,
localname="rat",
)
initdb.init_groups()
initdb.init_permissions()
group = Group.objects.get(name="admin")
self.local_user.groups.set([group])
self.site = models.SiteSettings.objects.create()
def test_themes_get(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.Themes.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_themes_post(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.Themes.as_view()
form = forms.ThemeForm()
form.data["name"] = "test theme"
form.data["path"] = "not/a/path.scss"
request = self.factory.post("", form.data)
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
theme = models.Theme.objects.last()
self.assertEqual(theme.name, "test theme")
self.assertEqual(theme.path, "not/a/path.scss")
def test_themes_post_forbidden(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.Themes.as_view()
form = forms.ThemeForm()
form.data["name"] = "test theme"
form.data["path"] = "not/a/path.scss"
request = self.factory.post("", form.data)
request.user = self.another_user
with self.assertRaises(PermissionDenied):
view(request)

View file

@ -14,6 +14,7 @@ from bookwyrm.tests.validate_html import validate_html
class InviteViews(TestCase): class InviteViews(TestCase):
"""every response to a get request, html or json""" """every response to a get request, html or json"""
# pylint: disable=invalid-name
def setUp(self): def setUp(self):
"""we need basic test data and mocks""" """we need basic test data and mocks"""
self.factory = RequestFactory() self.factory = RequestFactory()
@ -82,6 +83,7 @@ class InviteViews(TestCase):
view = views.InviteRequest.as_view() view = views.InviteRequest.as_view()
request = self.factory.post("", form.data) request = self.factory.post("", form.data)
request.user = self.local_user
result = view(request) result = view(request)
validate_html(result.render()) validate_html(result.render())
@ -96,6 +98,7 @@ class InviteViews(TestCase):
view = views.InviteRequest.as_view() view = views.InviteRequest.as_view()
request = self.factory.post("", form.data) request = self.factory.post("", form.data)
request.user = self.local_user
result = view(request) result = view(request)
validate_html(result.render()) validate_html(result.render())
@ -109,6 +112,7 @@ class InviteViews(TestCase):
request = self.factory.get("") request = self.factory.get("")
request.user = self.local_user request.user = self.local_user
request.user.is_superuser = True request.user.is_superuser = True
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
validate_html(result.render()) validate_html(result.render())

View file

@ -15,6 +15,7 @@ from bookwyrm.tests.validate_html import validate_html
class ListViews(TestCase): class ListViews(TestCase):
"""lists of lists""" """lists of lists"""
# pylint: disable=invalid-name
def setUp(self): def setUp(self):
"""we need basic test data and mocks""" """we need basic test data and mocks"""
self.factory = RequestFactory() self.factory = RequestFactory()

View file

@ -11,6 +11,7 @@ from bookwyrm.tests.validate_html import validate_html
class ReportViews(TestCase): class ReportViews(TestCase):
"""every response to a get request, html or json""" """every response to a get request, html or json"""
# pylint: disable=invalid-name
def setUp(self): def setUp(self):
"""we need basic test data and mocks""" """we need basic test data and mocks"""
self.factory = RequestFactory() self.factory = RequestFactory()

View file

@ -102,7 +102,7 @@ class EditAnnouncement(View):
return TemplateResponse( return TemplateResponse(
request, "settings/announcements/edit_announcement.html", data request, "settings/announcements/edit_announcement.html", data
) )
announcement = form.save() announcement = form.save(request)
return redirect("settings-announcements", announcement.id) return redirect("settings-announcements", announcement.id)

View file

@ -34,7 +34,7 @@ class AutoMod(View):
"""add rule""" """add rule"""
form = forms.AutoModRuleForm(request.POST) form = forms.AutoModRuleForm(request.POST)
if form.is_valid(): if form.is_valid():
form.save() form.save(request)
form = forms.AutoModRuleForm() form = forms.AutoModRuleForm()
data = automod_view_data() data = automod_view_data()
@ -54,7 +54,7 @@ def schedule_automod_task(request):
return TemplateResponse(request, "settings/automod/rules.html", data) return TemplateResponse(request, "settings/automod/rules.html", data)
with transaction.atomic(): with transaction.atomic():
schedule = form.save() schedule = form.save(request)
PeriodicTask.objects.get_or_create( PeriodicTask.objects.get_or_create(
interval=schedule, interval=schedule,
name="automod-task", name="automod-task",

View file

@ -40,7 +40,7 @@ class EmailBlocklist(View):
return TemplateResponse( return TemplateResponse(
request, "settings/email_blocklist/email_blocklist.html", data request, "settings/email_blocklist/email_blocklist.html", data
) )
form.save() form.save(request)
data["form"] = forms.EmailBlocklistForm() data["form"] = forms.EmailBlocklistForm()
return TemplateResponse( return TemplateResponse(

View file

@ -86,7 +86,7 @@ class AddFederatedServer(View):
return TemplateResponse( return TemplateResponse(
request, "settings/federation/edit_instance.html", data request, "settings/federation/edit_instance.html", data
) )
server = form.save() server = form.save(request)
return redirect("settings-federated-server", server.id) return redirect("settings-federated-server", server.id)
@ -156,7 +156,7 @@ class FederatedServer(View):
"""update note""" """update note"""
server = get_object_or_404(models.FederatedServer, id=server) server = get_object_or_404(models.FederatedServer, id=server)
server.notes = request.POST.get("notes") server.notes = request.POST.get("notes")
server.save() server.save(request)
return redirect("settings-federated-server", server.id) return redirect("settings-federated-server", server.id)

View file

@ -52,9 +52,9 @@ class ManageInvites(View):
if not form.is_valid(): if not form.is_valid():
return HttpResponseBadRequest(f"ERRORS: {form.errors}") return HttpResponseBadRequest(f"ERRORS: {form.errors}")
invite = form.save(commit=False) invite = form.save(request, commit=False)
invite.user = request.user invite.user = request.user
invite.save() invite.save(request)
paginated = Paginator( paginated = Paginator(
models.SiteInvite.objects.filter(user=request.user).order_by( models.SiteInvite.objects.filter(user=request.user).order_by(
@ -170,7 +170,7 @@ class InviteRequest(View):
received = False received = False
if form.is_valid(): if form.is_valid():
received = True received = True
form.save() form.save(request)
data = {"request_form": form, "request_received": received} data = {"request_form": form, "request_received": received}
return TemplateResponse(request, "landing/landing.html", data) return TemplateResponse(request, "landing/landing.html", data)

View file

@ -40,7 +40,7 @@ class IPBlocklist(View):
return TemplateResponse( return TemplateResponse(
request, "settings/ip_blocklist/ip_blocklist.html", data request, "settings/ip_blocklist/ip_blocklist.html", data
) )
form.save() form.save(request)
data["form"] = forms.IPBlocklistForm() data["form"] = forms.IPBlocklistForm()
return TemplateResponse( return TemplateResponse(

View file

@ -39,7 +39,7 @@ class LinkDomain(View):
"""Set display name""" """Set display name"""
domain = get_object_or_404(models.LinkDomain, id=domain_id) domain = get_object_or_404(models.LinkDomain, id=domain_id)
form = forms.LinkDomainForm(request.POST, instance=domain) form = forms.LinkDomainForm(request.POST, instance=domain)
form.save() form.save(request)
return redirect("settings-link-domain", status=status) return redirect("settings-link-domain", status=status)

View file

@ -29,7 +29,7 @@ class Site(View):
if not form.is_valid(): if not form.is_valid():
data = {"site_form": form} data = {"site_form": form}
return TemplateResponse(request, "settings/site.html", data) return TemplateResponse(request, "settings/site.html", data)
site = form.save() site = form.save(request)
data = {"site_form": forms.SiteForm(instance=site), "success": True} data = {"site_form": forms.SiteForm(instance=site), "success": True}
return TemplateResponse(request, "settings/site.html", data) return TemplateResponse(request, "settings/site.html", data)

View file

@ -24,9 +24,9 @@ class Themes(View):
def post(self, request): def post(self, request):
"""edit the site settings""" """edit the site settings"""
form = forms.ThemeForm(request.POST, request.FILES) form = forms.ThemeForm(request.POST)
if form.is_valid(): if form.is_valid():
form.save() form.save(request)
data = get_view_data() data = get_view_data()

View file

@ -88,6 +88,6 @@ class UserAdmin(View):
else: else:
form = forms.UserGroupForm(request.POST, instance=user) form = forms.UserGroupForm(request.POST, instance=user)
if form.is_valid(): if form.is_valid():
form.save() form.save(request)
data = {"user": user, "group_form": form} data = {"user": user, "group_form": form}
return TemplateResponse(request, "settings/users/user.html", data) return TemplateResponse(request, "settings/users/user.html", data)

View file

@ -68,7 +68,7 @@ class EditAuthor(View):
if not form.is_valid(): if not form.is_valid():
data = {"author": author, "form": form} data = {"author": author, "form": form}
return TemplateResponse(request, "author/edit_author.html", data) return TemplateResponse(request, "author/edit_author.html", data)
author = form.save() author = form.save(request)
return redirect(f"/author/{author.id}") return redirect(f"/author/{author.id}")

View file

@ -56,7 +56,7 @@ class EditBook(View):
for author_id in remove_authors: for author_id in remove_authors:
book.authors.remove(author_id) book.authors.remove(author_id)
book = form.save(commit=False) book = form.save(request, commit=False)
url = request.POST.get("cover-url") url = request.POST.get("cover-url")
if url: if url:
@ -119,7 +119,7 @@ class CreateBook(View):
return TemplateResponse(request, "book/edit/edit_book.html", data) return TemplateResponse(request, "book/edit/edit_book.html", data)
with transaction.atomic(): with transaction.atomic():
book = form.save() book = form.save(request)
parent_work = get_object_or_404(models.Work, id=parent_work_id) parent_work = get_object_or_404(models.Work, id=parent_work_id)
book.parent_work = parent_work book.parent_work = parent_work
@ -229,7 +229,7 @@ class ConfirmEditBook(View):
with transaction.atomic(): with transaction.atomic():
# save book # save book
book = form.save() book = form.save(request)
# add known authors # add known authors
authors = None authors = None

View file

@ -34,7 +34,7 @@ class BookFileLinks(View):
"""Edit a link""" """Edit a link"""
link = get_object_or_404(models.FileLink, id=link_id, book=book_id) link = get_object_or_404(models.FileLink, id=link_id, book=book_id)
form = forms.FileLinkForm(request.POST, instance=link) form = forms.FileLinkForm(request.POST, instance=link)
form.save() form.save(request)
return self.get(request, book_id) return self.get(request, book_id)
@ -76,7 +76,7 @@ class AddFileLink(View):
request, "book/file_links/file_link_page.html", data request, "book/file_links/file_link_page.html", data
) )
link = form.save() link = form.save(request)
book.file_links.add(link) book.file_links.add(link)
book.last_edited_by = request.user book.last_edited_by = request.user
book.save() book.save()

View file

@ -30,7 +30,7 @@ class Feed(View):
form = forms.FeedStatusTypesForm(request.POST, instance=request.user) form = forms.FeedStatusTypesForm(request.POST, instance=request.user)
if form.is_valid(): if form.is_valid():
# workaround to avoid broadcasting this change # workaround to avoid broadcasting this change
user = form.save(commit=False) user = form.save(request, commit=False)
user.save(broadcast=False, update_fields=["feed_status_types"]) user.save(broadcast=False, update_fields=["feed_status_types"])
filters_applied = True filters_applied = True

View file

@ -38,7 +38,7 @@ class GetStartedProfile(View):
if not form.is_valid(): if not form.is_valid():
data = {"form": form, "next": "get-started-books"} data = {"form": form, "next": "get-started-books"}
return TemplateResponse(request, "get_started/profile.html", data) return TemplateResponse(request, "get_started/profile.html", data)
save_user_form(form) save_user_form(request, form)
return redirect(self.next_view) return redirect(self.next_view)
@ -82,7 +82,6 @@ class GetStartedBooks(View):
for (book_id, shelf_id) in shelve_actions: for (book_id, shelf_id) in shelve_actions:
book = get_object_or_404(models.Edition, id=book_id) book = get_object_or_404(models.Edition, id=book_id)
shelf = get_object_or_404(models.Shelf, id=shelf_id) shelf = get_object_or_404(models.Shelf, id=shelf_id)
shelf.raise_not_editable(request.user)
models.ShelfBook.objects.create(book=book, shelf=shelf, user=request.user) models.ShelfBook.objects.create(book=book, shelf=shelf, user=request.user)
return redirect(self.next_view) return redirect(self.next_view)

View file

@ -48,8 +48,6 @@ class Goal(View):
year = int(year) year = int(year)
user = get_user_from_username(request.user, username) user = get_user_from_username(request.user, username)
goal = models.AnnualGoal.objects.filter(year=year, user=user).first() goal = models.AnnualGoal.objects.filter(year=year, user=user).first()
if goal:
goal.raise_not_editable(request.user)
form = forms.GoalForm(request.POST, instance=goal) form = forms.GoalForm(request.POST, instance=goal)
if not form.is_valid(): if not form.is_valid():
@ -59,7 +57,7 @@ class Goal(View):
"year": year, "year": year,
} }
return TemplateResponse(request, "user/goal.html", data) return TemplateResponse(request, "user/goal.html", data)
goal = form.save() goal = form.save(request)
if request.POST.get("post-status"): if request.POST.get("post-status"):
# create status, if appropriate # create status, if appropriate

View file

@ -52,7 +52,7 @@ class Group(View):
form = forms.GroupForm(request.POST, instance=user_group) form = forms.GroupForm(request.POST, instance=user_group)
if not form.is_valid(): if not form.is_valid():
return redirect("group", user_group.id) return redirect("group", user_group.id)
user_group = form.save() user_group = form.save(request)
# let the other members know something about the group changed # let the other members know something about the group changed
memberships = models.GroupMember.objects.filter(group=user_group) memberships = models.GroupMember.objects.filter(group=user_group)
@ -113,10 +113,8 @@ class UserGroups(View):
if not form.is_valid(): if not form.is_valid():
return redirect(request.user.local_path + "/groups") return redirect(request.user.local_path + "/groups")
group = form.save(commit=False)
group.raise_not_editable(request.user)
with transaction.atomic(): with transaction.atomic():
group.save() group = form.save(request)
# add the creator as a group member # add the creator as a group member
models.GroupMember.objects.create(group=group, user=request.user) models.GroupMember.objects.create(group=group, user=request.user)
return redirect("group", group.id) return redirect("group", group.id)
@ -129,10 +127,13 @@ class FindUsers(View):
# this is mostly borrowed from the Get Started friend finder # this is mostly borrowed from the Get Started friend finder
def get(self, request, group_id): def get(self, request, group_id):
"""basic profile info""" """Search for a user to add the a group, or load suggested users cache"""
user_query = request.GET.get("user_query") user_query = request.GET.get("user_query")
group = get_object_or_404(models.Group, id=group_id) group = get_object_or_404(models.Group, id=group_id)
# only users who can edit can add users
group.raise_not_editable(request.user) group.raise_not_editable(request.user)
lists = ( lists = (
models.List.privacy_filter(request.user) models.List.privacy_filter(request.user)
.filter(group=group) .filter(group=group)

View file

@ -31,7 +31,6 @@ class Curate(View):
def post(self, request, list_id): def post(self, request, list_id):
"""edit a book_list""" """edit a book_list"""
book_list = get_object_or_404(models.List, id=list_id) book_list = get_object_or_404(models.List, id=list_id)
book_list.raise_not_editable(request.user)
suggestion = get_object_or_404(models.ListItem, id=request.POST.get("item")) suggestion = get_object_or_404(models.ListItem, id=request.POST.get("item"))
approved = request.POST.get("approved") == "true" approved = request.POST.get("approved") == "true"

View file

@ -81,13 +81,12 @@ class List(View):
def post(self, request, list_id): def post(self, request, list_id):
"""edit a list""" """edit a list"""
book_list = get_object_or_404(models.List, id=list_id) book_list = get_object_or_404(models.List, id=list_id)
book_list.raise_not_editable(request.user)
form = forms.ListForm(request.POST, instance=book_list) form = forms.ListForm(request.POST, instance=book_list)
if not form.is_valid(): if not form.is_valid():
# this shouldn't happen # this shouldn't happen
raise Exception(form.errors) raise Exception(form.errors)
book_list = form.save() book_list = form.save(request)
if not book_list.curation == "group": if not book_list.curation == "group":
book_list.group = None book_list.group = None
book_list.save(broadcast=False) book_list.save(broadcast=False)
@ -196,7 +195,7 @@ def add_book(request):
if not form.is_valid(): if not form.is_valid():
return List().get(request, book_list.id, add_failed=True) return List().get(request, book_list.id, add_failed=True)
item = form.save(commit=False) item = form.save(request, commit=False)
if book_list.curation == "curated": if book_list.curation == "curated":
# make a pending entry at the end of the list # make a pending entry at the end of the list
@ -242,7 +241,6 @@ def set_book_position(request, list_item_id):
special care with the unique ordering per list. special care with the unique ordering per list.
""" """
list_item = get_object_or_404(models.ListItem, id=list_item_id) list_item = get_object_or_404(models.ListItem, id=list_item_id)
list_item.book_list.raise_not_editable(request.user)
try: try:
int_position = int(request.POST.get("position")) int_position = int(request.POST.get("position"))
except ValueError: except ValueError:

View file

@ -16,10 +16,9 @@ class ListItem(View):
def post(self, request, list_id, list_item): def post(self, request, list_id, list_item):
"""Edit a list item's notes""" """Edit a list item's notes"""
list_item = get_object_or_404(models.ListItem, id=list_item, book_list=list_id) list_item = get_object_or_404(models.ListItem, id=list_item, book_list=list_id)
list_item.raise_not_editable(request.user)
form = forms.ListItemForm(request.POST, instance=list_item) form = forms.ListItemForm(request.POST, instance=list_item)
if form.is_valid(): if form.is_valid():
item = form.save(commit=False) item = form.save(request, commit=False)
item.notes = to_markdown(item.notes) item.notes = to_markdown(item.notes)
item.save() item.save()
else: else:

View file

@ -36,8 +36,7 @@ class Lists(View):
form = forms.ListForm(request.POST) form = forms.ListForm(request.POST)
if not form.is_valid(): if not form.is_valid():
return redirect("lists") return redirect("lists")
book_list = form.save(commit=False) book_list = form.save(request, commit=False)
book_list.raise_not_editable(request.user)
# list should not have a group if it is not group curated # list should not have a group if it is not group curated
if not book_list.curation == "group": if not book_list.curation == "group":

View file

@ -34,14 +34,14 @@ class EditUser(View):
data = {"form": form, "user": request.user} data = {"form": form, "user": request.user}
return TemplateResponse(request, "preferences/edit_user.html", data) return TemplateResponse(request, "preferences/edit_user.html", data)
user = save_user_form(form) user = save_user_form(request, form)
return set_language(user, redirect("user-feed", request.user.localname)) return set_language(user, redirect("user-feed", request.user.localname))
def save_user_form(form): def save_user_form(request, form):
"""special handling for the user form""" """special handling for the user form"""
user = form.save(commit=False) user = form.save(request, commit=False)
if "avatar" in form.files: if "avatar" in form.files:
# crop and resize avatar upload # crop and resize avatar upload

View file

@ -159,7 +159,7 @@ class ReadThrough(View):
models.ReadThrough, id=request.POST.get("id") models.ReadThrough, id=request.POST.get("id")
) )
return TemplateResponse(request, "readthrough/readthrough.html", data) return TemplateResponse(request, "readthrough/readthrough.html", data)
form.save() form.save(request)
return redirect("book", book_id) return redirect("book", book_id)

View file

@ -33,7 +33,7 @@ class Report(View):
if not form.is_valid(): if not form.is_valid():
raise ValueError(form.errors) raise ValueError(form.errors)
report = form.save() report = form.save(request)
if report.links.exists(): if report.links.exists():
# revert the domain to pending # revert the domain to pending
domain = report.links.first().domain domain = report.links.first().domain

View file

@ -113,7 +113,6 @@ class Shelf(View):
"""edit a shelf""" """edit a shelf"""
user = get_user_from_username(request.user, username) user = get_user_from_username(request.user, username)
shelf = get_object_or_404(user.shelf_set, identifier=shelf_identifier) shelf = get_object_or_404(user.shelf_set, identifier=shelf_identifier)
shelf.raise_not_editable(request.user)
# you can't change the name of the default shelves # you can't change the name of the default shelves
if not shelf.editable and request.POST.get("name") != shelf.name: if not shelf.editable and request.POST.get("name") != shelf.name:
@ -122,7 +121,7 @@ class Shelf(View):
form = forms.ShelfForm(request.POST, instance=shelf) form = forms.ShelfForm(request.POST, instance=shelf)
if not form.is_valid(): if not form.is_valid():
return redirect(shelf.local_path) return redirect(shelf.local_path)
shelf = form.save() shelf = form.save(request)
return redirect(shelf.local_path) return redirect(shelf.local_path)

View file

@ -15,9 +15,7 @@ def create_shelf(request):
if not form.is_valid(): if not form.is_valid():
return redirect("user-shelves", request.user.localname) return redirect("user-shelves", request.user.localname)
shelf = form.save(commit=False) shelf = form.save(request)
shelf.raise_not_editable(request.user)
shelf.save()
return redirect(shelf.local_path) return redirect(shelf.local_path)

View file

@ -34,7 +34,6 @@ class EditStatus(View):
status = get_object_or_404( status = get_object_or_404(
models.Status.objects.select_subclasses(), id=status_id models.Status.objects.select_subclasses(), id=status_id
) )
status.raise_not_editable(request.user)
status_type = "reply" if status.reply_parent else status.status_type.lower() status_type = "reply" if status.reply_parent else status.status_type.lower()
data = { data = {
@ -65,7 +64,6 @@ class CreateStatus(View):
existing_status = get_object_or_404( existing_status = get_object_or_404(
models.Status.objects.select_subclasses(), id=existing_status_id models.Status.objects.select_subclasses(), id=existing_status_id
) )
existing_status.raise_not_editable(request.user)
existing_status.edited_date = timezone.now() existing_status.edited_date = timezone.now()
status_type = status_type[0].upper() + status_type[1:] status_type = status_type[0].upper() + status_type[1:]
@ -84,8 +82,7 @@ class CreateStatus(View):
return HttpResponseBadRequest() return HttpResponseBadRequest()
return redirect("/") return redirect("/")
status = form.save(commit=False) status = form.save(request)
status.raise_not_editable(request.user)
# save the plain, unformatted version of the status for future editing # save the plain, unformatted version of the status for future editing
status.raw_content = status.content status.raw_content = status.content
if hasattr(status, "quote"): if hasattr(status, "quote"):
@ -167,7 +164,6 @@ def edit_readthrough(request):
"""can't use the form because the dates are too finnicky""" """can't use the form because the dates are too finnicky"""
# TODO: remove this, it duplicates the code in the ReadThrough view # TODO: remove this, it duplicates the code in the ReadThrough view
readthrough = get_object_or_404(models.ReadThrough, id=request.POST.get("id")) readthrough = get_object_or_404(models.ReadThrough, id=request.POST.get("id"))
readthrough.raise_not_editable(request.user)
readthrough.start_date = load_date_in_user_tz_as_utc( readthrough.start_date = load_date_in_user_tz_as_utc(
request.POST.get("start_date"), request.user request.POST.get("start_date"), request.user