diff --git a/comments/__init__.py b/comments/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/comments/admin.py b/comments/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/comments/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/comments/apps.py b/comments/apps.py new file mode 100644 index 00000000..6aa34832 --- /dev/null +++ b/comments/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CommentsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "comments" diff --git a/comments/migrations/__init__.py b/comments/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/comments/models.py b/comments/models.py new file mode 100644 index 00000000..ebd4a941 --- /dev/null +++ b/comments/models.py @@ -0,0 +1,73 @@ +from django.db import models +from django.conf import settings +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType + +class CommentThread(models.Model): + """ + A generic comment thread that can be attached to any model instance. + Used for tracking discussions on various objects across the platform. + """ + content_type = models.ForeignKey( + ContentType, + on_delete=models.CASCADE, + related_name='comment_threads' + ) + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey('content_type', 'object_id') + + title = models.CharField(max_length=255, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + related_name='created_comment_threads' + ) + is_locked = models.BooleanField(default=False) + is_hidden = models.BooleanField(default=False) + + class Meta: + indexes = [ + models.Index(fields=['content_type', 'object_id']), + ] + ordering = ['-created_at'] + + def __str__(self): + return f"Comment Thread on {self.content_object} - {self.title}" + + +class Comment(models.Model): + """ + Individual comment within a comment thread. + """ + thread = models.ForeignKey( + CommentThread, + on_delete=models.CASCADE, + related_name='comments' + ) + author = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + null=True, + related_name='comments' + ) + content = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + is_edited = models.BooleanField(default=False) + is_hidden = models.BooleanField(default=False) + parent = models.ForeignKey( + 'self', + on_delete=models.CASCADE, + null=True, + blank=True, + related_name='replies' + ) + + class Meta: + ordering = ['created_at'] + + def __str__(self): + return f"Comment by {self.author} on {self.created_at}" diff --git a/comments/tests.py b/comments/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/comments/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/comments/views.py b/comments/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/comments/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/companies/models.py b/companies/models.py index 47ed88a1..ab3ece7e 100644 --- a/companies/models.py +++ b/companies/models.py @@ -2,6 +2,7 @@ from django.db import models from django.utils.text import slugify from django.urls import reverse from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes.fields import GenericRelation from typing import Tuple, Optional, ClassVar, TYPE_CHECKING from history_tracking.models import HistoricalModel, VersionBranch, ChangeSet from history_tracking.signals import get_current_branch, ChangesetContextManager @@ -19,6 +20,10 @@ class Company(HistoricalModel): total_rides = models.IntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + comments = GenericRelation('comments.CommentThread', + related_name='company_threads', + related_query_name='comments_thread' + ) objects: ClassVar[models.Manager['Company']] @@ -101,6 +106,10 @@ class Manufacturer(HistoricalModel): total_roller_coasters = models.IntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + comments = GenericRelation('comments.CommentThread', + related_name='manufacturer_threads', + related_query_name='comments_thread' + ) objects: ClassVar[models.Manager['Manufacturer']] @@ -181,6 +190,10 @@ class Designer(HistoricalModel): total_roller_coasters = models.IntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + comments = GenericRelation('comments.CommentThread', + related_name='designer_threads', + related_query_name='comments_thread' + ) objects: ClassVar[models.Manager['Designer']] diff --git a/history_tracking/htmx_views.py b/history_tracking/htmx_views.py index 43e14cf2..82a7f358 100644 --- a/history_tracking/htmx_views.py +++ b/history_tracking/htmx_views.py @@ -5,7 +5,7 @@ from django.http import HttpRequest, HttpResponse, Http404 from django.template.loader import render_to_string from django.core.exceptions import PermissionDenied -from .models import ChangeSet, CommentThread, Comment +from .models import ChangeSet, HistoricalCommentThread, Comment from .notifications import NotificationDispatcher from .state_machine import ApprovalStateMachine @@ -16,7 +16,7 @@ def get_comments(request: HttpRequest) -> HttpResponse: if not anchor: raise Http404("Anchor parameter is required") - thread = CommentThread.objects.filter(anchor__id=anchor).first() + thread = HistoricalCommentThread.objects.filter(anchor__id=anchor).first() comments = thread.comments.all() if thread else [] return render(request, 'history_tracking/partials/comments_list.html', { @@ -44,7 +44,7 @@ def add_comment(request: HttpRequest) -> HttpResponse: if not content: return HttpResponse("Comment content is required", status=400) - thread, created = CommentThread.objects.get_or_create( + thread, created = HistoricalCommentThread.objects.get_or_create( anchor={'id': anchor}, defaults={'created_by': request.user} ) diff --git a/history_tracking/models.py b/history_tracking/models.py index f31d4aa3..0df73144 100644 --- a/history_tracking/models.py +++ b/history_tracking/models.py @@ -18,7 +18,8 @@ class HistoricalModel(models.Model): id = models.BigAutoField(primary_key=True) history: HistoricalRecords = HistoricalRecords( inherit=True, - bases=(HistoricalChangeMixin,) + bases=(HistoricalChangeMixin,), + excluded_fields=['comments', 'photos', 'reviews'] # Exclude all generic relations ) class Meta: @@ -116,8 +117,8 @@ class VersionTag(models.Model): def __str__(self) -> str: return f"{self.name} ({self.branch.name})" -class CommentThread(models.Model): - """Represents a thread of comments on a historical record""" +class HistoricalCommentThread(models.Model): + """Represents a thread of comments specific to historical records and version control""" content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') @@ -149,7 +150,7 @@ class CommentThread(models.Model): class Comment(models.Model): """Individual comment within a thread""" - thread = models.ForeignKey(CommentThread, on_delete=models.CASCADE, related_name='comments') + thread = models.ForeignKey(HistoricalCommentThread, on_delete=models.CASCADE, related_name='comments') author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) content = models.TextField() created_at = models.DateTimeField(auto_now_add=True) diff --git a/history_tracking/views.py b/history_tracking/views.py index ce08c881..cd4346f4 100644 --- a/history_tracking/views.py +++ b/history_tracking/views.py @@ -9,7 +9,7 @@ from django.views.decorators.http import require_http_methods from django.core.exceptions import PermissionDenied from typing import Dict, Any -from .models import VersionBranch, ChangeSet, VersionTag, CommentThread +from .models import VersionBranch, ChangeSet, VersionTag, HistoricalCommentThread from .managers import ChangeTracker from .comparison import ComparisonEngine from .state_machine import ApprovalStateMachine @@ -42,7 +42,7 @@ def version_comparison(request: HttpRequest) -> HttpResponse: # Add comments to changes for change in diff_result['changes']: anchor_id = change['metadata']['comment_anchor_id'] - change['comments'] = CommentThread.objects.filter( + change['comments'] = HistoricalCommentThread.objects.filter( anchor__contains={'id': anchor_id} ).prefetch_related('comments') diff --git a/parks/models.py b/parks/models.py index a50e144d..05d0e0db 100644 --- a/parks/models.py +++ b/parks/models.py @@ -58,6 +58,10 @@ class Park(HistoricalModel): Company, on_delete=models.SET_NULL, null=True, blank=True, related_name="parks" ) photos = GenericRelation(Photo, related_query_name="park") + comments = GenericRelation('comments.CommentThread', + related_name='park_threads', + related_query_name='comments_thread' + ) areas: models.Manager['ParkArea'] # Type hint for reverse relation rides: models.Manager['Ride'] # Type hint for reverse relation from rides app @@ -164,6 +168,12 @@ class ParkArea(HistoricalModel): opening_date = models.DateField(null=True, blank=True) closing_date = models.DateField(null=True, blank=True) + # Relationships + comments = GenericRelation('comments.CommentThread', + related_name='park_area_threads', + related_query_name='comments_thread' + ) + # Metadata created_at = models.DateTimeField(auto_now_add=True, null=True) updated_at = models.DateTimeField(auto_now=True) diff --git a/pyproject.toml b/pyproject.toml index 2764ca5f..41802ee2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,5 +55,7 @@ dependencies = [ "django-simple-history>=3.5.0", "django-tailwind-cli>=2.21.1", "playwright>=1.41.0", - "pytest-playwright>=0.4.3" + "pytest-playwright>=0.4.3", + "celery>=5.4.0", + "django-redis>=5.4.0", ] diff --git a/requirements.txt b/requirements.txt index fbe75e88..74ddc462 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,7 @@ pyjwt==2.10.1 # Database psycopg2-binary==2.9.10 dj-database-url==2.3.0 +django-redis==5.4.0 # Email requests==2.32.3 # For ForwardEmail.net API diff --git a/reviews/models.py b/reviews/models.py index 9098670a..812a3cf6 100644 --- a/reviews/models.py +++ b/reviews/models.py @@ -1,6 +1,6 @@ from django.db import models from django.urls import reverse -from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.core.validators import MinValueValidator, MaxValueValidator from history_tracking.models import HistoricalModel, VersionBranch, ChangeSet @@ -40,6 +40,12 @@ class Review(HistoricalModel): related_name='moderated_reviews' ) moderated_at = models.DateTimeField(null=True, blank=True) + + # Comments + comments = GenericRelation('comments.CommentThread', + related_name='review_threads', + related_query_name='comments_thread' + ) class Meta: ordering = ['-created_at'] diff --git a/rides/models.py b/rides/models.py index 4622a7c6..f52305bf 100644 --- a/rides/models.py +++ b/rides/models.py @@ -41,6 +41,10 @@ class RideModel(HistoricalModel): ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + comments = GenericRelation('comments.CommentThread', + related_name='ride_model_threads', + related_query_name='comments_thread' + ) class Meta: ordering = ['manufacturer', 'name'] @@ -179,6 +183,10 @@ class Ride(HistoricalModel): updated_at = models.DateTimeField(auto_now=True) photos = GenericRelation('media.Photo') reviews = GenericRelation('reviews.Review') + comments = GenericRelation('comments.CommentThread', + related_name='ride_threads', + related_query_name='comments_thread' + ) class Meta: ordering = ['name'] diff --git a/thrillwiki/settings.py b/thrillwiki/settings.py index 8e32c037..92615915 100644 --- a/thrillwiki/settings.py +++ b/thrillwiki/settings.py @@ -53,6 +53,7 @@ INSTALLED_APPS = [ "designers", "analytics", "location", + "comments", ] MIDDLEWARE = [ @@ -109,14 +110,22 @@ DATABASES = { # Cache settings CACHES = { "default": { - "BACKEND": "django.core.cache.backends.locmem.LocMemCache", - "LOCATION": "unique-snowflake", + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://192.168.86.3:6379/1", + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + "SOCKET_CONNECT_TIMEOUT": 5, + "SOCKET_TIMEOUT": 5, + "RETRY_ON_TIMEOUT": True, + "MAX_CONNECTIONS": 1000, + "PARSER_CLASS": "redis.connection.HiredisParser", + }, + "KEY_PREFIX": "thrillwiki", "TIMEOUT": 300, # 5 minutes - "OPTIONS": {"MAX_ENTRIES": 1000}, } } -CACHE_MIDDLEWARE_SECONDS = 1 # 5 minutes +CACHE_MIDDLEWARE_SECONDS = 300 # 5 minutes CACHE_MIDDLEWARE_KEY_PREFIX = "thrillwiki" # Password validation diff --git a/uv.lock b/uv.lock index 17cd680e..9b2ffa40 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,18 @@ version = 1 requires-python = ">=3.13" +[[package]] +name = "amqp" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]amqp-5.3.1.tar.gz", hash = "sha256:[AWS-SECRET-REMOVED]de74d170a6f10ab044739432", size = 129013 } +wheels = [ + { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]amqp-5.3.1-py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]32d479b98661a95f117880a2", size = 50944 }, +] + [[package]] name = "asgiref" version = "3.8.1" @@ -43,6 +55,15 @@ wheels = [ { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]Automat-24.8.1-py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]0ab63ff808566ce90551e02a", size = 42585 }, ] +[[package]] +name = "billiard" +version = "4.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]billiard-4.2.1.tar.gz", hash = "sha256:[AWS-SECRET-REMOVED]33c7c1fcd66a7e677c4fb36f", size = 155031 } +wheels = [ + { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]billiard-4.2.1-py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]227baffd928c644d15d8f3cb", size = 86766 }, +] + [[package]] name = "black" version = "24.10.0" @@ -63,6 +84,26 @@ wheels = [ { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]black-24.10.0-py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]9853e47a294a3dd963c1dd7d", size = 206898 }, ] +[[package]] +name = "celery" +version = "5.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "billiard" }, + { name = "click" }, + { name = "click-didyoumean" }, + { name = "click-plugins" }, + { name = "click-repl" }, + { name = "kombu" }, + { name = "python-dateutil" }, + { name = "tzdata" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]celery-5.4.0.tar.gz", hash = "sha256:[AWS-SECRET-REMOVED]89d85f94756762d8bca7e706", size = 1575692 } +wheels = [ + { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]celery-5.4.0-py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]37edde2dfc0dacd73ed97f64", size = 425983 }, +] + [[package]] name = "certifi" version = "2024.12.14" @@ -156,6 +197,43 @@ wheels = [ { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]click-8.1.8-py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]d329b7903a866228027263b2", size = 98188 }, ] +[[package]] +name = "click-didyoumean" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]click_didyoumean-0.3.1.tar.gz", hash = "sha256:[AWS-SECRET-REMOVED]05aa09cbfc07c9d7fbb5a463", size = 3089 } +wheels = [ + { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]957e63d0fac41c10e7c3117c", size = 3631 }, +] + +[[package]] +name = "click-plugins" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]click-plugins-1.1.1.tar.gz", hash = "sha256:[AWS-SECRET-REMOVED]df9a3a3742e9ed63645f264b", size = 8164 } +wheels = [ + { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]dc815c06b442aa3c02889fc8", size = 7497 }, +] + +[[package]] +name = "click-repl" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]click-repl-0.3.0.tar.gz", hash = "sha256:[AWS-SECRET-REMOVED]0fe37474f3feebb69ced26a9", size = 10449 } +wheels = [ + { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]click_repl-0.3.0-py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]4c6899382da7feeeeb51b812", size = 10289 }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -328,6 +406,19 @@ wheels = [ { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]django_oauth_toolkit-3.0.1-py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]221bbb1cffcb50b8932e55ed", size = 77299 }, ] +[[package]] +name = "django-redis" +version = "5.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "redis" }, +] +sdist = { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]django-redis-5.4.0.tar.gz", hash = "sha256:[AWS-SECRET-REMOVED]50b2db93602e6cb292818c42", size = 52567 } +wheels = [ + { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]django_redis-5.4.0-py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]319df4c6da6ca9a2942edd5b", size = 31119 }, +] + [[package]] name = "django-simple-history" version = "3.7.0" @@ -483,6 +574,20 @@ wheels = [ { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]jwcrypto-1.5.6-py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]8be71f67f03566692fd55789", size = 92520 }, ] +[[package]] +name = "kombu" +version = "5.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "amqp" }, + { name = "tzdata" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]kombu-5.4.2.tar.gz", hash = "sha256:[AWS-SECRET-REMOVED]ff31e7fba89138cdb406f2cf", size = 442858 } +wheels = [ + { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]kombu-5.4.2-py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]8c49ea866884047d66e14763", size = 201349 }, +] + [[package]] name = "mccabe" version = "0.7.0" @@ -610,6 +715,18 @@ wheels = [ { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]pluggy-1.5.0-py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]4d554740532335b9d75ea669", size = 20556 }, ] +[[package]] +name = "prompt-toolkit" +version = "3.0.50" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]prompt_toolkit-3.0.50.tar.gz", hash = "sha256:[AWS-SECRET-REMOVED]c9728359013f79877fc89bab", size = 429087 } +wheels = [ + { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]89e6d88ebbb1b509d9779198", size = 387816 }, +] + [[package]] name = "psycopg2-binary" version = "2.9.10" @@ -626,6 +743,7 @@ wheels = [ { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:[AWS-SECRET-REMOVED]07c6df12b7737febc40f0909", size = 2822712 }, { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:[AWS-SECRET-REMOVED]860ff3bbe1384130828714b1", size = 2920155 }, { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:[AWS-SECRET-REMOVED]998122abe1dce6428bd86567", size = 2959356 }, + { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:[AWS-SECRET-REMOVED]9b46e6fd07c3eb46e4535142", size = 2569224 }, ] [[package]] @@ -773,6 +891,18 @@ wheels = [ { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]pytest_playwright-0.6.2-py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]1f8b3acf575beed84e7e9043", size = 16436 }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:[AWS-SECRET-REMOVED]f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]6dfe83f850eea9a5f7470427", size = 229892 }, +] + [[package]] name = "python-dotenv" version = "1.0.1" @@ -851,6 +981,15 @@ wheels = [ { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]56c89140852d1120324e8686", size = 9755 }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]six-1.17.0.tar.gz", hash = "sha256:[AWS-SECRET-REMOVED]63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]six-1.17.0-py2.py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]b2e1e8eda2be8970586c3274", size = 11050 }, +] + [[package]] name = "sqlparse" version = "0.5.3" @@ -875,6 +1014,7 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "black" }, + { name = "celery" }, { name = "channels" }, { name = "channels-redis" }, { name = "daphne" }, @@ -887,6 +1027,7 @@ dependencies = [ { name = "django-filter" }, { name = "django-htmx" }, { name = "django-oauth-toolkit" }, + { name = "django-redis" }, { name = "django-simple-history" }, { name = "django-tailwind-cli" }, { name = "django-webpack-loader" }, @@ -908,6 +1049,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "black", specifier = ">=24.1.0" }, + { name = "celery", specifier = ">=5.4.0" }, { name = "channels", specifier = ">=4.2.0" }, { name = "channels-redis", specifier = ">=4.2.1" }, { name = "daphne", specifier = ">=4.1.2" }, @@ -920,6 +1062,7 @@ requires-dist = [ { name = "django-filter", specifier = ">=23.5" }, { name = "django-htmx", specifier = ">=1.17.2" }, { name = "django-oauth-toolkit", specifier = ">=3.0.1" }, + { name = "django-redis", specifier = ">=5.4.0" }, { name = "django-simple-history", specifier = ">=3.5.0" }, { name = "django-tailwind-cli", specifier = ">=2.21.1" }, { name = "django-webpack-loader", specifier = ">=3.1.1" }, @@ -1012,6 +1155,24 @@ wheels = [ { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]urllib3-2.3.0-py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]710050facf0dd6911440e3df", size = 128369 }, ] +[[package]] +name = "vine" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]vine-5.1.0.tar.gz", hash = "sha256:[AWS-SECRET-REMOVED]15bb196ce38aefd6799e61e0", size = 48980 } +wheels = [ + { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]vine-5.1.0-py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]810050a728bd7413811fb1dc", size = 9636 }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]wcwidth-0.2.13.tar.gz", hash = "sha256:[AWS-SECRET-REMOVED]6ce63e9b4e64fc18303972b5", size = 101301 } +wheels = [ + { url = "https://files.pythonhosted.[AWS-SECRET-REMOVED][AWS-SECRET-REMOVED]wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:[AWS-SECRET-REMOVED]6bdb8d6102117aac784f6859", size = 34166 }, +] + [[package]] name = "whitenoise" version = "6.8.2"