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 29653cac..27011ec2 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 f2747247..bb736581 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.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944 }, +] + [[package]] name = "asgiref" version = "3.8.1" @@ -43,6 +55,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/cc/55a32a2c98022d88812b5986d2a92c4ff3ee087e83b712ebc703bba452bf/Automat-24.8.1-py3-none-any.whl", hash = "sha256:bf029a7bc3da1e2c24da2343e7598affaa9f10bf0ab63ff808566ce90551e02a", size = 42585 }, ] +[[package]] +name = "billiard" +version = "4.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/58/1546c970afcd2a2428b1bfafecf2371d8951cc34b46701bea73f4280989e/billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f", size = 155031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/da/43b15f28fe5f9e027b41c539abc5469052e9d48fd75f8ff094ba2a0ae767/billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb", size = 86766 }, +] + [[package]] name = "black" version = "24.10.0" @@ -63,6 +84,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/a7/4b27c50537ebca8bec139b872861f9d2bf501c5ec51fcf897cb924d9e264/black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", 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.org/packages/8a/9c/cf0bce2cc1c8971bf56629d8f180e4ca35612c7e79e6e432e785261a8be4/celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706", size = 1575692 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/c4/6a4d3772e5407622feb93dd25c86ce3c0fee746fa822a777a627d56b4f2a/celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64", size = 425983 }, +] + [[package]] name = "certifi" version = "2024.12.14" @@ -156,6 +197,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, ] +[[package]] +name = "click-didyoumean" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631 }, +] + +[[package]] +name = "click-plugins" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/1d/45434f64ed749540af821fd7e42b8e4d23ac04b1eda7c26613288d6cd8a8/click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b", size = 8164 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/da/824b92d9942f4e472702488857914bdd50f73021efea15b4cad9aca8ecef/click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8", 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.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289 }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -328,6 +406,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/40/e556bc19ba65356fe5f0e48ca01c50e81f7c630042fa7411b6ab428ecf68/django_oauth_toolkit-3.0.1-py3-none-any.whl", hash = "sha256:3ef00b062a284f2031b0732b32dc899e3bbf0eac221bbb1cffcb50b8932e55ed", 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.org/packages/83/9d/2272742fdd9d0a9f0b28cd995b0539430c9467a2192e4de2cea9ea6ad38c/django-redis-5.4.0.tar.gz", hash = "sha256:6a02abaa34b0fea8bf9b707d2c363ab6adc7409950b2db93602e6cb292818c42", size = 52567 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/f1/63caad7c9222c26a62082f4f777de26389233b7574629996098bf6d25a4d/django_redis-5.4.0-py3-none-any.whl", hash = "sha256:ebc88df7da810732e2af9987f7f426c96204bf89319df4c6da6ca9a2942edd5b", size = 31119 }, +] + [[package]] name = "django-simple-history" version = "3.7.0" @@ -483,6 +574,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cd/58/4a1880ea64032185e9ae9f63940c9327c6952d5584ea544a8f66972f2fda/jwcrypto-1.5.6-py3-none-any.whl", hash = "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789", 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.org/packages/38/4d/b93fcb353d279839cc35d0012bee805ed0cf61c07587916bfc35dbfddaf1/kombu-5.4.2.tar.gz", hash = "sha256:eef572dd2fd9fc614b37580e3caeafdd5af46c1eff31e7fba89138cdb406f2cf", size = 442858 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/ec/7811a3cf9fdfee3ee88e54d08fcbc3fabe7c1b6e4059826c59d7b795651c/kombu-5.4.2-py3-none-any.whl", hash = "sha256:14212f5ccf022fc0a70453bb025a1dcc32782a588c49ea866884047d66e14763", size = 201349 }, +] + [[package]] name = "mccabe" version = "0.7.0" @@ -610,6 +715,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] +[[package]] +name = "prompt-toolkit" +version = "3.0.50" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size = 429087 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816 }, +] + [[package]] name = "psycopg2-binary" version = "2.9.10" @@ -626,6 +743,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712 }, { url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155 }, { url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356 }, + { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224 }, ] [[package]] @@ -773,6 +891,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/72/38/e95090d0a26114a650f7d8847425f21757fc740de965df94de47b439fe67/pytest_playwright-0.6.2-py3-none-any.whl", hash = "sha256:0eff73bebe497b0158befed91e2f5fe94cfa17181f8b3acf575beed84e7e9043", 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.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + [[package]] name = "python-dotenv" version = "1.0.1" @@ -851,6 +981,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", 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.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, ] +[[package]] +name = "vine" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636 }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, +] + [[package]] name = "whitenoise" version = "6.8.2"