Merge branch 'main' into notifications

This commit is contained in:
Mouse Reeve 2022-07-06 14:41:15 -07:00
commit 5a6ef7ccd8
29 changed files with 441 additions and 96 deletions

View file

@ -45,7 +45,8 @@ def moderation_report_email(report):
"""a report was created"""
data = email_data()
data["reporter"] = report.reporter.localname or report.reporter.username
data["reportee"] = report.user.localname or report.user.username
if report.user:
data["reportee"] = report.user.localname or report.user.username
data["report_link"] = report.remote_id
for admin in models.User.objects.filter(

View file

@ -10,3 +10,4 @@ from .landing import *
from .links import *
from .lists import *
from .status import *
from .user_admin import *

View file

@ -4,12 +4,6 @@ from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class UserGroupForm(CustomForm):
class Meta:
model = models.User
fields = ["groups"]
class GroupForm(CustomForm):
class Meta:
model = models.Group

View file

@ -0,0 +1,10 @@
""" using django model forms """
from bookwyrm import models
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class UserGroupForm(CustomForm):
class Meta:
model = models.User
fields = ["groups"]

View file

@ -0,0 +1,24 @@
# Generated by Django 3.2.13 on 2022-07-05 23:54
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0150_readthrough_stopped_date"),
]
operations = [
migrations.AlterField(
model_name="report",
name="user",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
),
),
]

View file

@ -0,0 +1,25 @@
# Generated by Django 3.2.13 on 2022-07-06 19:16
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0151_alter_report_user"),
]
operations = [
migrations.AlterField(
model_name="report",
name="user",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
),
),
]

View file

@ -11,7 +11,7 @@ class Report(BookWyrmModel):
"User", related_name="reporter", on_delete=models.PROTECT
)
note = models.TextField(null=True, blank=True)
user = models.ForeignKey("User", on_delete=models.PROTECT)
user = models.ForeignKey("User", on_delete=models.PROTECT, null=True, blank=True)
status = models.ForeignKey(
"Status",
null=True,

View file

@ -42,7 +42,11 @@
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer">{{ link.url }}</a>
</td>
<td>
{% if link.added_by %}
<a href="{% url 'user-feed' link.added_by.id %}">{{ link.added_by.display_name }}</a>
{% else %}
<em>{% trans "Unknown user" %}</em>
{% endif %}
</td>
<td>
{{ link.filelink.filetype }}
@ -50,7 +54,7 @@
<td>
{{ link.domain.name }}
<p>
<a href="{% url 'report-link' link.added_by.id link.id %}">{% trans "Report spam" %}</a>
<a href="{% url 'report-link' link.id %}">{% trans "Report spam" %}</a>
</p>
</td>
<td>

View file

@ -19,7 +19,7 @@ Is that where you'd like to go?
{% block modal-footer %}
{% if request.user.is_authenticated %}
<div class="is-flex-grow-1">
<a href="{% url 'report-link' link.added_by.id link.id %}">{% trans "Report spam" %}</a>
<a href="{% url 'report-link' link.id %}">{% trans "Report spam" %}</a>
</div>
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>

View file

@ -3,7 +3,19 @@
{% block content %}
<p>
{% blocktrans %}@{{ reporter }} has flagged behavior by @{{ reportee }} for moderation. {% endblocktrans %}
{% if report_link %}
{% blocktrans trimmed %}
@{{ reporter }} has flagged a link domain for moderation.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
@{{ reporter }} has flagged behavior by @{{ reportee }} for moderation.
{% endblocktrans %}
{% endif %}
</p>
{% trans "View report" as text %}

View file

@ -2,7 +2,15 @@
{% load i18n %}
{% block content %}
{% blocktrans %}@{{ reporter}} has flagged behavior by @{{ reportee }} for moderation. {% endblocktrans %}
{% if report_link %}
{% blocktrans trimmed %}
@{{ reporter }} has flagged a link domain for moderation.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
@{{ reporter }} has flagged behavior by @{{ reportee }} for moderation.
{% endblocktrans %}
{% endif %}
{% trans "View report" %}
{{ report_link }}

View file

@ -15,7 +15,11 @@
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer">{{ link.url }}</a>
</td>
<td>
{% if link.added_by %}
<a href="{% url 'settings-user' link.added_by.id %}">@{{ link.added_by|username }}</a>
{% else %}
<em>{% trans "Unknown user" %}</em>
{% endif %}
</td>
<td>
{% if link.filelink.filetype %}

View file

@ -55,9 +55,11 @@
</div>
{% endif %}
{% if report.user %}
{% include 'settings/users/user_info.html' with user=report.user %}
{% include 'settings/users/user_moderation_actions.html' with user=report.user %}
{% endif %}
<div class="block">
<h3 class="title is-4">{% trans "Moderator Comments" %}</h3>

View file

@ -9,9 +9,15 @@ Report #{{ report_id }}: Status posted by @{{ username }}
{% elif report.links.exists %}
{% blocktrans trimmed with report_id=report.id username=report.user|username %}
Report #{{ report_id }}: Link added by @{{ username }}
{% endblocktrans %}
{% if report.user %}
{% blocktrans trimmed with report_id=report.id username=report.user|username %}
Report #{{ report_id }}: Link added by @{{ username }}
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with report_id=report.id %}
Report #{{ report_id }}: Link domain
{% endblocktrans %}
{% endif %}
{% else %}

View file

@ -72,6 +72,12 @@
<span class="icon icon-check"></span>
</span>
{% trans "Active" %}
{% elif user.deactivation_reason == "moderator_deletion" or user.deactivation_reason == "self_deletion" %}
<span class="tag is-danger" aria-hidden="true">
<span class="icon icon-x"></span>
</span>
{% trans "Deleted" %}
<span class="help">({{ user.get_deactivation_reason_display }})</span>
{% else %}
<span class="tag is-warning" aria-hidden="true">
<span class="icon icon-x"></span>

View file

@ -53,7 +53,7 @@ def comparison_bool(str1, str2, reverse=False):
@register.filter(is_safe=True)
def truncatepath(value, arg):
"""Truncate a path by removing all directories except the first and truncating ."""
"""Truncate a path by removing all directories except the first and truncating"""
path = os.path.normpath(value.name)
path_list = path.split(os.sep)
try:

View file

@ -1,4 +1,5 @@
""" style fixes and lookups for templates """
from collections import namedtuple
import re
from unittest.mock import patch
@ -61,3 +62,18 @@ class UtilitiesTags(TestCase):
self.assertEqual(utilities.get_title(self.book), "Test Book")
book = models.Edition.objects.create(title="Oh", subtitle="oh my")
self.assertEqual(utilities.get_title(book), "Oh: oh my")
def test_comparison_bool(self, *_):
"""just a simple comparison"""
self.assertTrue(utilities.comparison_bool("a", "a"))
self.assertFalse(utilities.comparison_bool("a", "b"))
self.assertFalse(utilities.comparison_bool("a", "a", reverse=True))
self.assertTrue(utilities.comparison_bool("a", "b", reverse=True))
def test_truncatepath(self, *_):
"""truncate a path"""
ValueMock = namedtuple("Value", ("name"))
value = ValueMock("home/one/two/three/four")
self.assertEqual(utilities.truncatepath(value, 2), "home/…ur")
self.assertEqual(utilities.truncatepath(value, "a"), "four")

View file

@ -4,6 +4,7 @@ from unittest.mock import patch
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from django_celery_beat.models import PeriodicTask, IntervalSchedule
from bookwyrm import forms, models, views
from bookwyrm.tests.validate_html import validate_html
@ -28,6 +29,43 @@ class AutomodViews(TestCase):
models.SiteSettings.objects.create()
def test_automod_rules_get(self):
"""there are so many views, this just makes sure it LOADS"""
schedule = IntervalSchedule.objects.create(every=1, period="days")
PeriodicTask.objects.create(
interval=schedule,
name="automod-task",
task="bookwyrm.models.antispam.automod_task",
)
models.AutoMod.objects.create(created_by=self.local_user, string_match="hello")
view = views.AutoMod.as_view()
request = self.factory.get("")
request.user = self.local_user
request.user.is_superuser = True
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_automod_rules_get_empty_with_schedule(self):
"""there are so many views, this just makes sure it LOADS"""
schedule = IntervalSchedule.objects.create(every=1, period="days")
PeriodicTask.objects.create(
interval=schedule,
name="automod-task",
task="bookwyrm.models.antispam.automod_task",
)
view = views.AutoMod.as_view()
request = self.factory.get("")
request.user = self.local_user
request.user.is_superuser = True
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_automod_rules_get_empty_without_schedule(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.AutoMod.as_view()
request = self.factory.get("")
@ -45,7 +83,7 @@ class AutomodViews(TestCase):
form.data["string_match"] = "hello"
form.data["flag_users"] = True
form.data["flag_statuses"] = False
form.data["created_by"] = self.local_user
form.data["created_by"] = self.local_user.id
view = views.AutoMod.as_view()
request = self.factory.post("", form.data)
@ -53,6 +91,27 @@ class AutomodViews(TestCase):
request.user.is_superuser = True
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
rule = models.AutoMod.objects.get()
self.assertTrue(rule.flag_users)
self.assertFalse(rule.flag_statuses)
def test_schedule_automod_task(self):
"""Schedule the task"""
self.assertFalse(IntervalSchedule.objects.exists())
form = forms.IntervalScheduleForm()
form.data["every"] = 1
form.data["period"] = "days"
request = self.factory.post("", form.data)
request.user = self.local_user
request.user.is_superuser = True
response = views.schedule_automod_task(request)
self.assertEqual(response.status_code, 302)
self.assertTrue(IntervalSchedule.objects.exists())

View file

@ -6,7 +6,7 @@ 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 import models, views
from bookwyrm.tests.validate_html import validate_html
@ -89,52 +89,6 @@ class ReportViews(TestCase):
self.assertEqual(comment.note, "hi")
self.assertEqual(comment.report, report)
def test_report_modal_view(self):
"""a user reports another user"""
request = self.factory.get("")
request.user = self.local_user
result = views.Report.as_view()(request, self.local_user.id)
validate_html(result.render())
def test_make_report(self):
"""a user reports another user"""
form = forms.ReportForm()
form.data["reporter"] = self.local_user.id
form.data["user"] = self.rat.id
request = self.factory.post("", form.data)
request.user = self.local_user
views.Report.as_view()(request)
report = models.Report.objects.get()
self.assertEqual(report.reporter, self.local_user)
self.assertEqual(report.user, self.rat)
def test_report_link(self):
"""a user reports a link as spam"""
book = models.Edition.objects.create(title="hi")
link = models.FileLink.objects.create(
book=book, added_by=self.local_user, url="https://skdjfs.sdf"
)
domain = link.domain
domain.status = "approved"
domain.save()
form = forms.ReportForm()
form.data["reporter"] = self.local_user.id
form.data["user"] = self.rat.id
form.data["links"] = link.id
request = self.factory.post("", form.data)
request.user = self.local_user
views.Report.as_view()(request)
report = models.Report.objects.get()
domain.refresh_from_db()
self.assertEqual(report.links.first().id, link.id)
self.assertEqual(domain.status, "pending")
def test_resolve_report(self):
"""toggle report resolution status"""
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)

View file

@ -48,7 +48,7 @@ class EditBookViews(TestCase):
models.SiteSettings.objects.create()
def test_edit_book_page(self):
def test_edit_book_get(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.EditBook.as_view()
request = self.factory.get("")
@ -59,18 +59,7 @@ class EditBookViews(TestCase):
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_edit_book_create_page(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.CreateBook.as_view()
request = self.factory.get("")
request.user = self.local_user
request.user.is_superuser = True
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_edit_book(self):
def test_edit_book_post(self):
"""lets a user edit a book"""
view = views.EditBook.as_view()
self.local_user.groups.add(self.group)
@ -86,6 +75,23 @@ class EditBookViews(TestCase):
self.book.refresh_from_db()
self.assertEqual(self.book.title, "New Title")
def test_edit_book_post_invalid(self):
"""book form is invalid"""
view = views.EditBook.as_view()
self.local_user.groups.add(self.group)
form = forms.EditionForm(instance=self.book)
form.data["title"] = ""
form.data["last_edited_by"] = self.local_user.id
request = self.factory.post("", form.data)
request.user = self.local_user
result = view(request, self.book.id)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
# Title is unchanged
self.book.refresh_from_db()
self.assertEqual(self.book.title, "Example Edition")
def test_edit_book_add_author(self):
"""lets a user edit a book with new authors"""
view = views.EditBook.as_view()
@ -234,3 +240,49 @@ class EditBookViews(TestCase):
self.assertEqual(len(result["author_matches"]), 2)
self.assertEqual(result["author_matches"][0]["name"], "Sappho")
self.assertEqual(result["author_matches"][1]["name"], "Some Guy")
def test_create_book_get(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.CreateBook.as_view()
request = self.factory.get("")
request.user = self.local_user
request.user.is_superuser = True
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_create_book_post_existing_work(self):
"""Adding an edition to an existing work"""
author = models.Author.objects.create(name="Sappho")
view = views.CreateBook.as_view()
form = forms.EditionForm()
form.data["title"] = "A Title"
form.data["parent_work"] = self.work.id
form.data["authors"] = [author.id]
form.data["last_edited_by"] = self.local_user.id
request = self.factory.post("", form.data)
request.user = self.local_user
request.user.is_superuser = True
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
result = view(request)
self.assertEqual(result.status_code, 302)
new_edition = models.Edition.objects.get(title="A Title")
self.assertEqual(new_edition.parent_work, self.work)
self.assertEqual(new_edition.authors.first(), author)
def test_create_book_post_invalid(self):
"""book form is invalid"""
view = views.CreateBook.as_view()
self.local_user.groups.add(self.group)
form = forms.EditionForm(instance=self.book)
form.data["title"] = ""
form.data["last_edited_by"] = self.local_user.id
request = self.factory.post("", form.data)
request.user = self.local_user
result = view(request)
validate_html(result.render())
self.assertEqual(result.status_code, 200)

View file

@ -112,7 +112,17 @@ class ViewsHelpers(TestCase):
request = self.factory.get("", {"q": "Test Book"}, HTTP_USER_AGENT=USER_AGENT)
self.assertTrue(views.helpers.is_bookwyrm_request(request))
def test_existing_user(self, *_):
def test_handle_remote_webfinger_invalid(self, *_):
"""Various ways you can send a bad query"""
# if there's no query, there's no result
result = views.helpers.handle_remote_webfinger(None)
self.assertIsNone(result)
# malformed user
result = views.helpers.handle_remote_webfinger("noatsymbol")
self.assertIsNone(result)
def test_handle_remote_webfinger_existing_user(self, *_):
"""simple database lookup by username"""
result = views.helpers.handle_remote_webfinger("@mouse@local.com")
self.assertEqual(result, self.local_user)
@ -124,7 +134,19 @@ class ViewsHelpers(TestCase):
self.assertEqual(result, self.local_user)
@responses.activate
def test_load_user(self, *_):
def test_handle_remote_webfinger_load_user_invalid_result(self, *_):
"""find a remote user using webfinger, but fail"""
username = "mouse@example.com"
responses.add(
responses.GET,
f"https://example.com/.well-known/webfinger?resource=acct:{username}",
status=500,
)
result = views.helpers.handle_remote_webfinger("@mouse@example.com")
self.assertIsNone(result)
@responses.activate
def test_handle_remote_webfinger_load_user(self, *_):
"""find a remote user using webfinger"""
username = "mouse@example.com"
wellknown = {
@ -154,7 +176,7 @@ class ViewsHelpers(TestCase):
self.assertIsInstance(result, models.User)
self.assertEqual(result.username, "mouse@example.com")
def test_user_on_blocked_server(self, *_):
def test_handler_remote_webfinger_user_on_blocked_server(self, *_):
"""find a remote user using webfinger"""
models.FederatedServer.objects.create(
server_name="example.com", status="blocked"
@ -163,6 +185,38 @@ class ViewsHelpers(TestCase):
result = views.helpers.handle_remote_webfinger("@mouse@example.com")
self.assertIsNone(result)
@responses.activate
def test_subscribe_remote_webfinger(self, *_):
"""remote subscribe templates"""
query = "mouse@example.com"
response = {
"subject": f"acct:{query}",
"links": [
{
"rel": "self",
"type": "application/activity+json",
"href": "https://example.com/user/mouse",
"template": "hi",
},
{
"rel": "http://ostatus.org/schema/1.0/subscribe",
"type": "application/activity+json",
"href": "https://example.com/user/mouse",
"template": "hello",
},
],
}
responses.add(
responses.GET,
f"https://example.com/.well-known/webfinger?resource=acct:{query}",
json=response,
status=200,
)
template = views.helpers.subscribe_remote_webfinger(query)
self.assertEqual(template, "hello")
template = views.helpers.subscribe_remote_webfinger(f"@{query}")
self.assertEqual(template, "hello")
def test_handle_reading_status_to_read(self, *_):
"""posts shelve activities"""
shelf = self.local_user.shelf_set.get(identifier="to-read")

View file

@ -0,0 +1,109 @@
""" test for app action functionality """
from unittest.mock import patch
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import forms, models, views
from bookwyrm.tests.validate_html import validate_html
class ReportViews(TestCase):
"""every response to a get request, html or json"""
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.rat = models.User.objects.create_user(
"rat@local.com",
"rat@mouse.mouse",
"password",
local=True,
localname="rat",
)
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
), patch("bookwyrm.activitystreams.add_status_task.delay"):
self.status = models.Status.objects.create(
user=self.local_user,
content="Test status",
)
models.SiteSettings.objects.create()
def test_report_modal_view(self):
"""a user reports another user"""
request = self.factory.get("")
request.user = self.local_user
result = views.Report.as_view()(request, self.local_user.id)
validate_html(result.render())
def test_report_modal_view_with_status(self):
"""a user reports another user"""
request = self.factory.get("")
request.user = self.local_user
result = views.Report.as_view()(
request, user_id=self.local_user.id, status_id=self.status.id
)
validate_html(result.render())
def test_report_modal_view_with_link_domain(self):
"""a user reports another user"""
link = models.Link.objects.create(
url="http://example.com/hi",
added_by=self.local_user,
)
request = self.factory.get("")
request.user = self.local_user
result = views.Report.as_view()(request, link_id=link.id)
validate_html(result.render())
def test_make_report(self):
"""a user reports another user"""
form = forms.ReportForm()
form.data["reporter"] = self.local_user.id
form.data["user"] = self.rat.id
request = self.factory.post("", form.data)
request.user = self.local_user
views.Report.as_view()(request)
report = models.Report.objects.get()
self.assertEqual(report.reporter, self.local_user)
self.assertEqual(report.user, self.rat)
def test_report_link(self):
"""a user reports a link as spam"""
book = models.Edition.objects.create(title="hi")
link = models.FileLink.objects.create(
book=book, added_by=self.local_user, url="https://skdjfs.sdf"
)
domain = link.domain
domain.status = "approved"
domain.save()
form = forms.ReportForm()
form.data["reporter"] = self.local_user.id
form.data["user"] = self.rat.id
form.data["links"] = link.id
request = self.factory.post("", form.data)
request.user = self.local_user
views.Report.as_view()(request)
report = models.Report.objects.get()
domain.refresh_from_db()
self.assertEqual(report.links.first().id, link.id)
self.assertEqual(domain.status, "pending")

View file

@ -287,7 +287,7 @@ urlpatterns = [
name="report-status",
),
re_path(
r"^report/(?P<user_id>\d+)/link/(?P<link_id>\d+)?$",
r"^report/link/(?P<link_id>\d+)?$",
views.Report.as_view(),
name="report-link",
),

View file

@ -33,8 +33,7 @@ class AutoMod(View):
def post(self, request):
"""add rule"""
form = forms.AutoModRuleForm(request.POST)
success = form.is_valid()
if success:
if form.is_valid():
form.save()
form = forms.AutoModRuleForm()

View file

@ -77,8 +77,13 @@ class UserAdmin(View):
def post(self, request, user):
"""update user group"""
user = get_object_or_404(models.User, id=user)
form = forms.UserGroupForm(request.POST, instance=user)
if form.is_valid():
form.save()
if request.POST.get("groups") == "":
user.groups.set([])
form = forms.UserGroupForm(instance=user)
else:
form = forms.UserGroupForm(request.POST, instance=user)
if form.is_valid():
form.save()
data = {"user": user, "group_form": form}
return TemplateResponse(request, "settings/users/user.html", data)

View file

@ -148,13 +148,6 @@ def handle_reading_status(user, shelf, book, privacy):
status.save()
def is_blocked(viewer, user):
"""is this viewer blocked by the user?"""
if viewer.is_authenticated and viewer in user.blocks.all():
return True
return False
def load_date_in_user_tz_as_utc(date_str: str, user: models.User) -> datetime:
"""ensures that data is stored consistently in the UTC timezone"""
if not date_str:

View file

@ -13,9 +13,13 @@ from bookwyrm import emailing, forms, models
class Report(View):
"""Make reports"""
def get(self, request, user_id, status_id=None, link_id=None):
def get(self, request, user_id=None, status_id=None, link_id=None):
"""static view of report modal"""
data = {"user": get_object_or_404(models.User, id=user_id)}
data = {"user": None}
if user_id:
# but normally we should have an error if the user is not found
data["user"] = get_object_or_404(models.User, id=user_id)
if status_id:
data["status"] = status_id
if link_id:

3
bw-dev
View file

@ -103,6 +103,9 @@ case "$CMD" in
pytest)
runweb pytest --no-cov-on-fail "$@"
;;
pytest_coverage_report)
runweb pytest -n 3 --cov-report term-missing "$@"
;;
collectstatic)
runweb python manage.py collectstatic --no-input
;;

View file

@ -2,7 +2,7 @@ aiohttp==3.8.1
bleach==5.0.1
celery==5.2.2
colorthief==0.2.1
Django==3.2.13
Django==3.2.14
django-celery-beat==2.2.1
django-compressor==2.4.1
django-imagekit==4.1.0