diff --git a/lego/api/urls.py b/lego/api/urls.py index 535fdbf6f..7261386f8 100644 --- a/lego/api/urls.py +++ b/lego/api/urls.py @@ -5,7 +5,7 @@ from django.views.decorators.csrf import csrf_exempt from django.views.generic import RedirectView -from .v1 import router as v1 +from .v1 import urlpatterns as v1_urlpatterns @csrf_exempt @@ -22,7 +22,7 @@ def version_redirect(request, path): app_name = "api" urlpatterns = [ - re_path(r"^v1/", include((v1.urls, "v1"), namespace="v1")), + re_path(r"^v1/", include((v1_urlpatterns, "v1"), namespace="v1")), re_path( r"^$", RedirectView.as_view(url=f"/api/{settings.API_VERSION}/"), name="default" ), diff --git a/lego/api/v1.py b/lego/api/v1.py index 6571bd741..a34af4be0 100644 --- a/lego/api/v1.py +++ b/lego/api/v1.py @@ -1,3 +1,4 @@ +from django.urls import include, path from rest_framework import routers from lego.apps.articles.views import ArticlesViewSet @@ -33,6 +34,8 @@ FollowEventViewSet, FollowUserViewSet, ) +from lego.apps.forums.urls import urlpatterns as forums_urls +from lego.apps.forums.views import ForumsViewSet, ThreadViewSet from lego.apps.frontpage.views import FrontpageViewSet from lego.apps.gallery.views import GalleryPictureViewSet, GalleryViewSet from lego.apps.ical.viewsets import ICalTokenViewset, ICalViewset @@ -133,6 +136,7 @@ router.register(r"followers-company", FollowCompanyViewSet) router.register(r"followers-event", FollowEventViewSet) router.register(r"followers-user", FollowUserViewSet) +router.register(r"forums", ForumsViewSet) router.register(r"frontpage", FrontpageViewSet, basename="frontpage") router.register(r"galleries", GalleryViewSet) router.register(r"galleries/(?P\d+)/pictures", GalleryPictureViewSet) @@ -197,6 +201,7 @@ r"surveys/(?P\d+)/submissions", SubmissionViewSet, basename="submission" ) router.register(r"tags", TagViewSet) +router.register(r"threads", ThreadViewSet) router.register(r"user-delete", UserDeleteViewSet, basename="user-delete") router.register(r"users", UsersViewSet) router.register( @@ -206,3 +211,8 @@ ) router.register(r"oidc", OIDCViewSet, basename="oidc") router.register(r"webhooks-stripe", StripeWebhook, basename="webhooks-stripe") + +urlpatterns = [ + path("", include(router.urls)), + path("forums/", include((forums_urls, "forums"))), +] diff --git a/lego/apps/companies/constants.py b/lego/apps/companies/constants.py index 1ba1780cb..73014131f 100644 --- a/lego/apps/companies/constants.py +++ b/lego/apps/companies/constants.py @@ -45,12 +45,14 @@ README = "readme" ITDAGENE = "itdagene" LABAMBA_SPONSOR = "labamba_sponsor" +SOCIAL_MEDIA = "social_media" OTHER_OFFERS = ( (COLLABORATION, COLLABORATION), (README, README), (ITDAGENE, ITDAGENE), (LABAMBA_SPONSOR, LABAMBA_SPONSOR), + (SOCIAL_MEDIA, SOCIAL_MEDIA), ) TRANSLATED_OTHER_OFFERS = { @@ -58,6 +60,7 @@ README: "Annonsering i readme", ITDAGENE: "Stand på itDAGENE", LABAMBA_SPONSOR: "Sponsing av LaBamba", + SOCIAL_MEDIA: "Profilering på sosiale medier", } COLLABORATION_ONLINE = "collaboration_online" diff --git a/lego/apps/companies/migrations/0028_alter_companyinterest_other_offers.py b/lego/apps/companies/migrations/0028_alter_companyinterest_other_offers.py new file mode 100644 index 000000000..73ebc9a6f --- /dev/null +++ b/lego/apps/companies/migrations/0028_alter_companyinterest_other_offers.py @@ -0,0 +1,32 @@ +# Generated by Django 4.0.10 on 2024-02-02 14:51 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("companies", "0027_companyinterest_bedex_comment_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="companyinterest", + name="other_offers", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=[ + ("collaboration", "collaboration"), + ("readme", "readme"), + ("itdagene", "itdagene"), + ("labamba_sponsor", "labamba_sponsor"), + ("social_media", "social_media"), + ], + max_length=64, + ), + blank=True, + null=True, + size=None, + ), + ), + ] diff --git a/lego/apps/contact/serializers.py b/lego/apps/contact/serializers.py index b6b3bbdc6..4277f2c37 100644 --- a/lego/apps/contact/serializers.py +++ b/lego/apps/contact/serializers.py @@ -1,6 +1,7 @@ +from django.db.models import Q from rest_framework import exceptions, serializers -from lego.apps.users.constants import GROUP_COMMITTEE +from lego.apps.users.constants import GROUP_BOARD, GROUP_COMMITTEE from lego.apps.users.models import AbakusGroup from lego.utils.fields import PrimaryKeyRelatedFieldNoPKOpt from lego.utils.functions import verify_captcha @@ -12,7 +13,10 @@ class ContactFormSerializer(serializers.Serializer): anonymous = serializers.BooleanField() captcha_response = serializers.CharField() recipient_group = PrimaryKeyRelatedFieldNoPKOpt( - allow_null=True, queryset=AbakusGroup.objects.all().filter(type=GROUP_COMMITTEE) + allow_null=True, + queryset=AbakusGroup.objects.all().filter( + Q(type=GROUP_COMMITTEE) | Q(type=GROUP_BOARD) + ), ) def validate_captcha_response(self, captcha_response): diff --git a/lego/apps/events/fixtures/test_events.yaml b/lego/apps/events/fixtures/test_events.yaml index 536ba3d48..f595488e8 100644 --- a/lego/apps/events/fixtures/test_events.yaml +++ b/lego/apps/events/fixtures/test_events.yaml @@ -151,7 +151,6 @@ end_time: "2118-09-01T15:20:30+03:00" created_by: 1 require_auth: False - use_contact_tracing: True - model: events.Event pk: 11 @@ -198,7 +197,8 @@ end_time: "2012-09-01T15:20:30+03:00" created_by: 1 require_auth: False - + responsible_group: 25 + - model: events.Pool pk: 1 fields: @@ -287,9 +287,9 @@ pk: 9 fields: name: Abakusmember - capacity: 1 + capacity: 1 event: 13 - counter: 1 + counter: 1 activation_date: "2019-09-01T13:20:30+03:00" permission_groups: - - Abakus @@ -361,3 +361,12 @@ pool: 9 registration_date: "2011-09-01T13:20:30+03:00" status: "SUCCESS_REGISTER" + +- model: events.Registration + pk: 8 + fields: + user: 10 + event: 1 + pool: null + registration_date: "2011-09-01T13:20:30+03:00" + status: "SUCCESS_REGISTER" diff --git a/lego/apps/events/migrations/0038_event_is_foreign_language.py b/lego/apps/events/migrations/0038_event_is_foreign_language.py new file mode 100644 index 000000000..473691cb5 --- /dev/null +++ b/lego/apps/events/migrations/0038_event_is_foreign_language.py @@ -0,0 +1,17 @@ +# Generated by Django 4.0.10 on 2023-10-31 17:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("events", "0037_event_responsible_users"), + ] + + operations = [ + migrations.AddField( + model_name="event", + name="is_foreign_language", + field=models.BooleanField(default=False), + ), + ] diff --git a/lego/apps/events/migrations/0039_remove_event_use_contact_tracing.py b/lego/apps/events/migrations/0039_remove_event_use_contact_tracing.py new file mode 100644 index 000000000..5fed078fc --- /dev/null +++ b/lego/apps/events/migrations/0039_remove_event_use_contact_tracing.py @@ -0,0 +1,16 @@ +# Generated by Django 4.0.10 on 2024-02-20 19:26 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("events", "0038_event_is_foreign_language"), + ] + + operations = [ + migrations.RemoveField( + model_name="event", + name="use_contact_tracing", + ), + ] diff --git a/lego/apps/events/models.py b/lego/apps/events/models.py index e86b7c466..9400fa54b 100644 --- a/lego/apps/events/models.py +++ b/lego/apps/events/models.py @@ -16,7 +16,6 @@ from lego.apps.events.exceptions import ( EventHasClosed, EventNotReady, - NoPhoneNumber, NoSuchPool, NoSuchRegistration, NotRegisteredPhotoConsents, @@ -32,7 +31,7 @@ from lego.apps.followers.models import FollowEvent from lego.apps.permissions.models import ObjectPermissionsModel from lego.apps.users.constants import AUTUMN, SPRING -from lego.apps.users.models import AbakusGroup, Penalty, User +from lego.apps.users.models import AbakusGroup, Membership, Penalty, User from lego.utils.models import BasisModel from lego.utils.youtube_validator import youtube_validator @@ -99,10 +98,10 @@ def unregistration_close_time(self) -> date: youtube_url = CharField( max_length=200, default="", validators=[youtube_validator], blank=True ) - use_contact_tracing = models.BooleanField(default=False) legacy_registration_count = models.PositiveIntegerField(default=0) mazemap_poi = models.PositiveIntegerField(null=True) responsible_users = ManyToManyField(User) + is_foreign_language = models.BooleanField(default=False, blank=False, null=False) class Meta: permission_handler = EventPermissionHandler() @@ -134,6 +133,15 @@ def user_should_see_regs(self, user: User) -> bool: or (self.created_by is not None and self.created_by.id == user.id) ) + def user_should_see_allergies(self, user: User) -> bool: + memberships = Membership.objects.filter(user=user) + in_responsible_group = self.responsible_group in [ + mem.abakus_group for mem in memberships + ] + created_by_self = user == self.created_by + + return created_by_self or in_responsible_group + def admin_register( self, admin_user: User, @@ -302,9 +310,6 @@ def register(self, registration: Registration) -> Registration: if self.registration_close_time < current_time: raise EventHasClosed() - if self.use_contact_tracing and user.phone_number is None: - raise NoPhoneNumber() - current_semester = AUTUMN if self.start_time.month > 7 else SPRING if self.use_consent and not user.has_registered_photo_consents_for_semester( self.start_time.year, @@ -771,10 +776,14 @@ def restricted_lookup(self) -> tuple[list[User], list]: ) return [registration.user for registration in registrations], [] - def announcement_lookup(self) -> list[User]: + def announcement_lookup(self, exclude_waiting_list: bool) -> list[User]: registrations: QuerySet[Registration] = self.registrations.filter( status=constants.SUCCESS_REGISTER ) + + if exclude_waiting_list: + registrations = registrations.exclude(pool=None) + return [registration.user for registration in registrations] def add_legacy_registration(self) -> None: diff --git a/lego/apps/events/serializers/events.py b/lego/apps/events/serializers/events.py index aef83001e..8e11dc299 100644 --- a/lego/apps/events/serializers/events.py +++ b/lego/apps/events/serializers/events.py @@ -19,7 +19,6 @@ from lego.apps.events.models import Event, Pool, Registration from lego.apps.events.serializers.pools import ( PoolAdministrateAllergiesSerializer, - PoolAdministrateExportSerializer, PoolAdministrateSerializer, PoolCreateAndUpdateSerializer, PoolReadAuthSerializer, @@ -27,7 +26,6 @@ ) from lego.apps.events.serializers.registrations import ( RegistrationReadDetailedAllergiesSerializer, - RegistrationReadDetailedExportSerializer, RegistrationReadDetailedSerializer, RegistrationReadSerializer, ) @@ -113,6 +111,7 @@ class Meta: "survey", "is_priced", "responsible_users", + "is_foreign_language", ) + ObjectPermissionsSerializerMixin.Meta.fields read_only = True @@ -187,9 +186,9 @@ class Meta: "survey", "use_consent", "youtube_url", - "use_contact_tracing", "mazemap_poi", "responsible_users", + "is_foreign_language", ) read_only = True @@ -323,10 +322,13 @@ def get_unanswered_surveys(self, obj): return request.user.unanswered_surveys() -class EventAdministrateSerializer(EventReadSerializer): +class EventAdministrateSerializer(EventReadSerializer, EventReadDetailedSerializer): pools = PoolAdministrateSerializer(many=True) unregistered = RegistrationReadDetailedSerializer(many=True) waiting_registrations = RegistrationReadDetailedSerializer(many=True) + responsible_group = AbakusGroupField( + queryset=AbakusGroup.objects.all(), required=False, allow_null=True + ) class Meta(EventReadSerializer.Meta): fields = EventReadSerializer.Meta.fields + ( # type: ignore @@ -334,18 +336,12 @@ class Meta(EventReadSerializer.Meta): "unregistered", "waiting_registrations", "use_consent", - "use_contact_tracing", "created_by", "feedback_required", + "responsible_group", ) -class EventAdministrateExportSerializer(EventAdministrateSerializer): - pools = PoolAdministrateExportSerializer(many=True) - unregistered = RegistrationReadDetailedExportSerializer(many=True) - waiting_registrations = RegistrationReadDetailedExportSerializer(many=True) - - class EventAdministrateAllergiesSerializer(EventAdministrateSerializer): pools = PoolAdministrateAllergiesSerializer(many=True) waiting_registrations = RegistrationReadDetailedAllergiesSerializer(many=True) @@ -406,9 +402,9 @@ class Meta: "registration_close_time", "unregistration_close_time", "youtube_url", - "use_contact_tracing", "mazemap_poi", "responsible_users", + "is_foreign_language", ) + ObjectPermissionsSerializerMixin.Meta.fields def validate(self, data): @@ -422,18 +418,6 @@ def validate(self, data): "end_time": "User does not have the required permissions for time travel" } ) - if ( - self.instance is not None - and "use_contact_tracing" in data - and data["use_contact_tracing"] != self.instance.use_contact_tracing - and self.instance.registrations.exists() - ): - raise serializers.ValidationError( - { - "use_contact_tracing": "Cannot change this field after registration has started" - } - ) - return data def create(self, validated_data): @@ -445,7 +429,6 @@ def create(self, validated_data): validated_data["require_auth"] = require_auth if event_status_type == constants.TBA: pools = [] - validated_data["location"] = "TBA" elif event_status_type == constants.OPEN: pools = [] elif event_status_type == constants.INFINITE: @@ -466,7 +449,6 @@ def update(self, instance, validated_data): ) if event_status_type == constants.TBA: pools = [] - validated_data["location"] = "TBA" elif event_status_type == constants.OPEN: pools = [] elif event_status_type == constants.INFINITE: diff --git a/lego/apps/events/tests/test_events_api.py b/lego/apps/events/tests/test_events_api.py index 8c73ad3bd..096844286 100644 --- a/lego/apps/events/tests/test_events_api.py +++ b/lego/apps/events/tests/test_events_api.py @@ -46,6 +46,7 @@ } ], "responsibleUsers": [1], + "isForeignLanguage": True, }, { "title": "Event2", @@ -74,6 +75,7 @@ }, ], "responsibleUsers": [1], + "isForeignLanguage": False, }, { "title": "Event3", @@ -96,6 +98,7 @@ } ], "responsibleUsers": [1], + "isForeignLanguage": True, }, { "title": "Event4", @@ -118,6 +121,7 @@ } ], "responsibleUsers": [1], + "isForeignLanguage": True, }, { "title": "Event5", @@ -140,6 +144,7 @@ } ], "responsibleUsers": [1], + "isForeignLanguage": True, }, { "title": "Event6", @@ -161,6 +166,7 @@ } ], "responsibleUsers": [1], + "isForeignLanguage": True, }, { "title": "Event7", @@ -173,6 +179,7 @@ "endTime": "2015-09-01T13:20:30Z", "mergeTime": "2016-01-01T13:20:30Z", "responsibleUsers": [1], + "isForeignLanguage": True, }, { "title": "Event8", @@ -194,6 +201,7 @@ } ], "responsibleUsers": [1], + "isForeignLanguage": True, }, ] @@ -557,7 +565,6 @@ def test_event_creation_tba(self): ) created_event = Event.objects.get(id=event_id) self.assertEqual(created_event.event_status_type, "TBA") - self.assertEqual(created_event.location, "TBA") self.assertEqual(len(created_event.pools.all()), 0) def test_event_creation_open(self): @@ -1355,7 +1362,7 @@ def test_without_group_permission(self): self.assertEqual(event_response.status_code, status.HTTP_403_FORBIDDEN) -class ExportInfoTestCase(BaseAPITestCase): +class AllergiesTestCase(BaseAPITestCase): fixtures = [ "test_abakus_groups.yaml", "test_companies.yaml", @@ -1365,73 +1372,38 @@ class ExportInfoTestCase(BaseAPITestCase): def setUp(self): self.abakus_user = User.objects.get(pk=1) - self.event = Event.objects.get(title="EXPORT_INFO_EVENT") + self.allergies_user = User.objects.get(pk=11) + self.event = Event.objects.get(title="ALLERGIES_EVENT") self.event.start_time = timezone.now() - timedelta(hours=7) self.event.end_time = timezone.now() - timedelta(hours=3) self.event.save() - def test_with_export_permission(self): + def test_with_allergies_permission_author(self): # Need to apply to the actual event (The one in the db) - AbakusGroup.objects.get(name="Bedkom").add_user(self.abakus_user) + AbakusGroup.objects.get(name="Abakom").add_user(self.abakus_user) self.client.force_authenticate(self.abakus_user) - event_response = self.client.get( - f"{_get_detail_url(self.event.id)}administrate/" - ) + event_response = self.client.get(f"{_get_detail_url(self.event.id)}allergies/") - attendee_email = self.event.pools.first().registrations.first().user.email + attendee_allergies = ( + self.event.pools.first().registrations.first().user.allergies + ) - self.assertEqual(self.event.use_contact_tracing, True) self.assertEqual(event_response.status_code, status.HTTP_200_OK) self.assertEqual( event_response.json() .get("pools")[0] .get("registrations")[0] .get("user") - .get("email"), - attendee_email, + .get("allergies"), + attendee_allergies, ) - def test_without_export_permission(self): + def test_with_allergies_permission_responsible_group(self): user = User.objects.get(pk=2) - AbakusGroup.objects.get(name="Webkom").add_user(user) + AbakusGroup.objects.get(pk=25).add_user(user) self.client.force_authenticate(user) - event_response = self.client.get( - f"{_get_detail_url(self.event.id)}administrate/" - ) - - self.assertIsNone( - event_response.json() - .get("pools")[0] - .get("registrations")[0] - .get("user") - .get("email") - ) - - -class AllergiesTestCase(BaseAPITestCase): - fixtures = [ - "test_abakus_groups.yaml", - "test_companies.yaml", - "test_users.yaml", - "test_events.yaml", - ] - - def setUp(self): - self.abakus_user = User.objects.get(pk=1) - self.allergies_user = User.objects.get(pk=11) - self.event = Event.objects.get(title="ALLERGIES_EVENT") - self.event.start_time = timezone.now() - timedelta(hours=7) - self.event.end_time = timezone.now() - timedelta(hours=3) - self.event.save() - - def test_with_allergies_permission(self): - # Need to apply to the actual event (The one in the db) - AbakusGroup.objects.get(name="Abakom").add_user(self.abakus_user) - self.client.force_authenticate(self.abakus_user) - event_response = self.client.get(f"{_get_detail_url(self.event.id)}allergies/") - attendee_allergies = ( self.event.pools.first().registrations.first().user.allergies ) diff --git a/lego/apps/events/tests/test_registrations.py b/lego/apps/events/tests/test_registrations.py index 4a26fb5e0..05ad932f0 100644 --- a/lego/apps/events/tests/test_registrations.py +++ b/lego/apps/events/tests/test_registrations.py @@ -413,13 +413,13 @@ def test_register_to_waiting_list_after_unregister(self): registration = Registration.objects.get_or_create(event=event, user=user)[0] event.register(registration) - self.assertEqual(event.waiting_registrations.count(), 1) + self.assertEqual(event.waiting_registrations.count(), 2) event.unregister(registration) - self.assertEqual(event.waiting_registrations.count(), 0) + self.assertEqual(event.waiting_registrations.count(), 1) event.register(registration) - self.assertEqual(event.waiting_registrations.count(), 1) + self.assertEqual(event.waiting_registrations.count(), 2) def test_unregistering_non_existing_user(self): """Test that non existing user trying to unregister raises error""" diff --git a/lego/apps/events/views.py b/lego/apps/events/views.py index 059c3e4d3..2baa44441 100644 --- a/lego/apps/events/views.py +++ b/lego/apps/events/views.py @@ -36,7 +36,6 @@ from lego.apps.events.permissions import EventTypePermission from lego.apps.events.serializers.events import ( EventAdministrateAllergiesSerializer, - EventAdministrateExportSerializer, EventAdministrateSerializer, EventCreateAndUpdateSerializer, EventReadAuthUserDetailedSerializer, @@ -236,12 +235,6 @@ def administrate(self, request, *args, **kwargs): ), ) event = queryset.first() - if ( - event.use_contact_tracing - and request.user == event.created_by - and timezone.now() <= event.end_time + timezone.timedelta(days=14) - ): - serializer = EventAdministrateExportSerializer event_data = serializer(event).data event_data = populate_event_registration_users_with_grade(event_data) return Response(event_data) @@ -251,8 +244,7 @@ def allergies(self, request, *args, **kwargs): event_id = self.kwargs.get("pk", None) serializer = EventAdministrateSerializer event = Event.objects.get(pk=event_id) - - if request.user == event.created_by: + if event.user_should_see_allergies(request.user): serializer = EventAdministrateAllergiesSerializer event_data = serializer(event).data diff --git a/lego/apps/forums/__init__.py b/lego/apps/forums/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lego/apps/forums/admin.py b/lego/apps/forums/admin.py new file mode 100644 index 000000000..e69de29bb diff --git a/lego/apps/forums/migrations/0001_initial.py b/lego/apps/forums/migrations/0001_initial.py new file mode 100644 index 000000000..e5a86c59f --- /dev/null +++ b/lego/apps/forums/migrations/0001_initial.py @@ -0,0 +1,201 @@ +# Generated by Django 4.0.10 on 2024-02-26 16:56 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + +import lego.apps.content.fields + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("users", "0041_user_linkedin_id_alter_user_github_username"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("tags", "0004_auto_20200324_1859"), + ] + + operations = [ + migrations.CreateModel( + name="Forum", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created_at", + models.DateTimeField( + db_index=True, default=django.utils.timezone.now, editable=False + ), + ), + ( + "updated_at", + models.DateTimeField( + default=django.utils.timezone.now, editable=False + ), + ), + ( + "deleted", + models.BooleanField(db_index=True, default=False, editable=False), + ), + ("require_auth", models.BooleanField(default=True)), + ("slug", models.SlugField(null=True, unique=True)), + ("title", models.CharField(max_length=255)), + ("description", models.TextField()), + ("text", lego.apps.content.fields.ContentField(allow_images=False)), + ("pinned", models.BooleanField(default=False)), + ( + "can_edit_groups", + models.ManyToManyField( + blank=True, + related_name="can_edit_%(class)s", + to="users.abakusgroup", + ), + ), + ( + "can_edit_users", + models.ManyToManyField( + blank=True, + related_name="can_edit_%(class)s", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "can_view_groups", + models.ManyToManyField( + blank=True, + related_name="can_view_%(class)s", + to="users.abakusgroup", + ), + ), + ( + "created_by", + models.ForeignKey( + default=None, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created", + to=settings.AUTH_USER_MODEL, + ), + ), + ("tags", models.ManyToManyField(blank=True, to="tags.tag")), + ( + "updated_by", + models.ForeignKey( + default=None, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="Thread", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created_at", + models.DateTimeField( + db_index=True, default=django.utils.timezone.now, editable=False + ), + ), + ( + "updated_at", + models.DateTimeField( + default=django.utils.timezone.now, editable=False + ), + ), + ( + "deleted", + models.BooleanField(db_index=True, default=False, editable=False), + ), + ("require_auth", models.BooleanField(default=True)), + ("slug", models.SlugField(null=True, unique=True)), + ("title", models.CharField(max_length=255)), + ("description", models.TextField()), + ("text", lego.apps.content.fields.ContentField(allow_images=False)), + ("pinned", models.BooleanField(default=False)), + ( + "can_edit_groups", + models.ManyToManyField( + blank=True, + related_name="can_edit_%(class)s", + to="users.abakusgroup", + ), + ), + ( + "can_edit_users", + models.ManyToManyField( + blank=True, + related_name="can_edit_%(class)s", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "can_view_groups", + models.ManyToManyField( + blank=True, + related_name="can_view_%(class)s", + to="users.abakusgroup", + ), + ), + ( + "created_by", + models.ForeignKey( + default=None, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "forum", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="threads", + to="forums.forum", + ), + ), + ("tags", models.ManyToManyField(blank=True, to="tags.tag")), + ( + "updated_by", + models.ForeignKey( + default=None, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/lego/apps/forums/migrations/__init__.py b/lego/apps/forums/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lego/apps/forums/models.py b/lego/apps/forums/models.py new file mode 100644 index 000000000..b8e8e2fdd --- /dev/null +++ b/lego/apps/forums/models.py @@ -0,0 +1,20 @@ +from django.db.models import CASCADE, ForeignKey + +from lego.apps.content.models import Content +from lego.apps.forums.permissions import ForumPermissionHandler, ThreadPermissionHandler +from lego.apps.permissions.models import ObjectPermissionsModel +from lego.utils.models import BasisModel + + +class Forum(Content, BasisModel, ObjectPermissionsModel): + class Meta: + abstract = False + permission_handler = ForumPermissionHandler() + + +class Thread(Content, BasisModel, ObjectPermissionsModel): + forum = ForeignKey(Forum, on_delete=CASCADE, related_name="threads") + + class Meta: + abstract = False + permission_handler = ThreadPermissionHandler() diff --git a/lego/apps/forums/permissions.py b/lego/apps/forums/permissions.py new file mode 100644 index 000000000..a4875bc6c --- /dev/null +++ b/lego/apps/forums/permissions.py @@ -0,0 +1,94 @@ +from lego.apps.permissions.constants import CREATE, DELETE, EDIT, LIST, VIEW +from lego.apps.permissions.permissions import PermissionHandler + + +class ForumPermissionHandler(PermissionHandler): + force_object_permission_check = True + authentication_map = {LIST: False, VIEW: False} + default_keyword_permission = "/sudo/admin/forums/{perm}/" + default_require_auth = True + + def has_perm( + self, + user, + perm, + obj=None, + queryset=None, + check_keyword_permissions=True, + **kwargs + ): + if perm == LIST: + return True + + if not user.is_authenticated: + return False + + # Check object permissions before keywork perms + if obj is not None: + if self.has_object_permissions(user, perm, obj): + return True + + if perm == EDIT and self.created_by(user, obj): + return True + + has_perm = super().has_perm( + user, perm, obj, queryset, check_keyword_permissions, **kwargs + ) + + if has_perm: + return True + + return False + + def has_object_permissions(self, user, perm, obj): + return not (perm == DELETE or perm == EDIT) + + +class ThreadPermissionHandler(PermissionHandler): + force_object_permission_check = True + authentication_map = {LIST: False, VIEW: False} + default_keyword_permission = "/sudo/admin/threads/{perm}/" + default_require_auth = True + + def has_perm( + self, + user, + perm, + obj=None, + queryset=None, + check_keyword_permissions=True, + **kwargs + ): + if perm == LIST: + return True + if not user.is_authenticated: + return False + + # Check object permissions before keywork perms + if obj is not None: + if self.has_object_permissions(user, perm, obj): + return True + + if perm == EDIT and self.created_by(user, obj): + return True + + if perm == CREATE: + return True + + has_perm = super().has_perm( + user, perm, obj, queryset, check_keyword_permissions, **kwargs + ) + + if has_perm: + return True + + return False + + def has_object_permissions(self, user, perm, obj): + if perm == DELETE: + return False + if perm == EDIT and obj.created_by == user: + return True + if perm == CREATE: + return True + return not (perm == DELETE or perm == EDIT) diff --git a/lego/apps/forums/serializers.py b/lego/apps/forums/serializers.py new file mode 100644 index 000000000..d7b537ec7 --- /dev/null +++ b/lego/apps/forums/serializers.py @@ -0,0 +1,92 @@ +from rest_framework import serializers +from rest_framework.fields import CharField + +from lego.apps.comments.serializers import CommentSerializer +from lego.apps.content.fields import ContentSerializerField +from lego.apps.forums.models import Forum, Thread +from lego.apps.users.serializers.users import PublicUserSerializer +from lego.utils.serializers import ( + BasisModelSerializer, + ObjectPermissionsSerializerMixin, +) + + +class DetailedThreadSerializer(BasisModelSerializer): + forum = serializers.PrimaryKeyRelatedField(queryset=Forum.objects.all()) + comments = CommentSerializer(read_only=True, many=True) + created_by = PublicUserSerializer(read_only=True) + content_target = CharField(read_only=True) + content = ContentSerializerField(source="text", required=True) + + class Meta: + model = Thread + fields = ( + "id", + "title", + "content", + "comments", + "created_at", + "forum", + "created_by", + "content_target", + ) + + +class DetailedAdminThreadSerializer( + ObjectPermissionsSerializerMixin, DetailedThreadSerializer +): + class Meta: + model = Thread + fields = ( + DetailedThreadSerializer.Meta.fields + + ObjectPermissionsSerializerMixin.Meta.fields + ) + + +class PublicThreadSerializer(BasisModelSerializer): + forum = serializers.PrimaryKeyRelatedField(queryset=Forum.objects.all()) + content = ContentSerializerField(source="text") + + class Meta: + model = Thread + fields = ("id", "title", "content", "created_at", "forum") + + +class DetailedForumSerializer(BasisModelSerializer): + threads = PublicThreadSerializer(many=True, read_only=True) + created_by = PublicUserSerializer(read_only=True) + content_target = CharField(read_only=True) + + class Meta: + model = Forum + fields = ( + "id", + "title", + "description", + "created_at", + "threads", + "created_by", + "content_target", + ) + + +class DetailedAdminForumSerializer( + ObjectPermissionsSerializerMixin, DetailedForumSerializer +): + class Meta: + model = Forum + fields = ( + DetailedForumSerializer.Meta.fields + + ObjectPermissionsSerializerMixin.Meta.fields + ) + + +class PublicForumSerializer(BasisModelSerializer): + class Meta: + model = Forum + fields = ( + "id", + "title", + "description", + "created_at", + ) diff --git a/lego/apps/forums/tests.py b/lego/apps/forums/tests.py new file mode 100644 index 000000000..891f6e23f --- /dev/null +++ b/lego/apps/forums/tests.py @@ -0,0 +1,168 @@ +from django.urls import reverse +from rest_framework import status + +from lego.apps.comments.models import Comment +from lego.apps.forums.models import Forum, Thread +from lego.apps.users.models import AbakusGroup, User +from lego.utils.test_utils import BaseAPITestCase + + +# Helper Functions +def get_forum_list_url(): + return reverse("api:v1:forum-list") + + +def get_forum_detail_url(pk): + return reverse("api:v1:forum-detail", kwargs={"pk": pk}) + + +def create_forum(**kwargs): + return Forum.objects.create( + title="Test Forum", description="Discussion forum", **kwargs + ) + + +def get_comment_list_url(): + return reverse("api:v1:comment-list") + + +def create_comment(thread, text="Test comment"): + return Comment.objects.create(content_object=thread, text=text) + + +def create_thread(forum, **kwargs): + return Thread.objects.create( + title="Sample Thread", + description="A thread in a forum", + forum=forum, + **kwargs, + ) + + +# Test Cases +class ForumTestCase(BaseAPITestCase): + fixtures = ["test_abakus_groups.yaml", "test_users.yaml", "test_articles.yaml"] + + def setUp(self): + self.all_comments = Comment.objects.all() + self.all_users = User.objects.all() + self.with_permission = self.all_users.get(username="useradmin_test") + self.comments_test_group = AbakusGroup.objects.get(name="CommentTest") + self.comments_test_group.add_user(self.with_permission) + self.without_permission = self.all_users.exclude( + pk=self.with_permission.pk + ).first() + self.forum = create_forum() + + def test_forum_creation(self): + self.client.force_authenticate(self.with_permission) + self.assertEqual(Forum.objects.count(), 1) + self.assertEqual(Forum.objects.first(), self.forum) + + def test_forum_list_view(self): + response = self.client.get(get_forum_list_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_forum_detail_view_unauthenticated(self): + self.client.force_authenticate(self.without_permission) + response = self.client.get(get_forum_detail_url(self.forum.pk)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_forum_detail_view_authenticated(self): + self.client.force_authenticate(user=self.with_permission) + response = self.client.get(get_forum_detail_url(self.forum.pk)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["id"], self.forum.pk) + + +class ThreadTestCase(BaseAPITestCase): + fixtures = ["test_abakus_groups.yaml", "test_users.yaml", "test_articles.yaml"] + + def setUp(self): + self.all_users = User.objects.all() + self.with_permission = self.all_users.get(username="useradmin_test") + self.comments_test_group = AbakusGroup.objects.get(name="CommentTest") + self.comments_test_group.add_user(self.with_permission) + self.forum = create_forum() + + def test_thread_creation(self): + self.client.force_authenticate(self.with_permission) + thread = create_thread(self.forum) + self.assertEqual(Thread.objects.count(), 1) + self.assertEqual(Thread.objects.first(), thread) + + +class CommentTestCase(BaseAPITestCase): + fixtures = ["test_abakus_groups.yaml", "test_users.yaml", "test_articles.yaml"] + + def setUp(self): + self.all_users = User.objects.all() + + self.with_permission = self.all_users.get(username="useradmin_test") + self.comments_test_group = AbakusGroup.objects.get(name="CommentTest") + self.comments_test_group.add_user(self.with_permission) + self.forum = create_forum() + self.thread = create_thread(self.forum) + + def test_post_comment_to_thread(self): + self.client.force_authenticate(user=self.with_permission) + comment_data = { + "text": "Test comment", + "content_target": f"forums.thread-{self.thread.id}", + } + response = self.client.post(get_comment_list_url(), comment_data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Comment.objects.count(), 1) + self.assertEqual(Comment.objects.first().text, "Test comment") + + +class AccessAndPermissionTestCase(BaseAPITestCase): + fixtures = ["test_abakus_groups.yaml", "test_users.yaml", "test_articles.yaml"] + + def setUp(self): + self.all_users = User.objects.all() + + self.with_permission = self.all_users.get(username="useradmin_test") + self.comments_test_group = AbakusGroup.objects.get(name="CommentTest") + self.comments_test_group.add_user(self.with_permission) + self.without_permission = self.all_users.exclude( + pk=self.with_permission.pk + ).first() + self.forum = create_forum() + self.thread = create_thread(self.forum) + self.comment = create_comment(thread=self.thread) + + def test_forum_access_unauthenticated_user(self): + response = self.client.get(get_forum_list_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_forum_access_authenticated_user(self): + self.client.force_authenticate(user=self.with_permission) + response = self.client.get(get_forum_list_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_thread_access_unauthenticated_user(self): + response = self.client.get(get_forum_detail_url(self.forum.id)) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_thread_access_authenticated_user(self): + self.client.force_authenticate(user=self.with_permission) + response = self.client.get(get_forum_detail_url(self.forum.id)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_post_comment_unauthenticated_user(self): + comment_data = { + "text": "Test comment", + "content_target": f"forums.thread-{self.thread.id}", + } + response = self.client.post(get_comment_list_url(), comment_data) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_post_comment_authenticated_user(self): + self.client.force_authenticate(user=self.with_permission) + comment_data = { + "text": "Test comment", + "content_target": f"forums.thread-{self.thread.id}", + } + response = self.client.post(get_comment_list_url(), comment_data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) diff --git a/lego/apps/forums/urls.py b/lego/apps/forums/urls.py new file mode 100644 index 000000000..12eec4864 --- /dev/null +++ b/lego/apps/forums/urls.py @@ -0,0 +1,25 @@ +# forums/urls.py + +from django.urls import path + +from .views import ThreadViewSet + +urlpatterns = [ + path( + "/threads/", + ThreadViewSet.as_view({"get": "list", "post": "create"}), + name="forum-threads-list", + ), + path( + "/threads//", + ThreadViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="forum-thread-detail", + ), +] diff --git a/lego/apps/forums/views.py b/lego/apps/forums/views.py new file mode 100644 index 000000000..d550e48c1 --- /dev/null +++ b/lego/apps/forums/views.py @@ -0,0 +1,90 @@ +from django.http import Http404 +from django.shortcuts import get_object_or_404 +from rest_framework import viewsets +from rest_framework.exceptions import NotAuthenticated, PermissionDenied + +from lego.apps.forums.models import Forum, Thread +from lego.apps.forums.serializers import ( + DetailedAdminForumSerializer, + DetailedAdminThreadSerializer, + DetailedForumSerializer, + DetailedThreadSerializer, + PublicForumSerializer, + PublicThreadSerializer, +) +from lego.apps.permissions.api.views import AllowedPermissionsMixin +from lego.apps.permissions.constants import OBJECT_PERMISSIONS_FIELDS + + +class ForumsViewSet(AllowedPermissionsMixin, viewsets.ModelViewSet): + queryset = Forum.objects.all() + ordering = "-created_at" + serializer_class = DetailedForumSerializer + + def get_serializer_class(self): + if self.action == "list": + return PublicForumSerializer + if self.request and self.request.user.is_authenticated: + return DetailedAdminForumSerializer + return super().get_serializer_class() + + def get_object(self) -> Forum: + queryset = self.get_queryset() + pk = self.kwargs.get("pk") + + try: + obj = queryset.get(id=pk) + except (TypeError, OverflowError, Forum.DoesNotExist, ValueError): + obj = get_object_or_404(queryset, slug=pk) + + try: + self.check_object_permissions(self.request, obj) + except NotAuthenticated as e: + raise Http404 from e + except PermissionDenied as e: + raise PermissionDenied from e + return obj + + +class ThreadViewSet(AllowedPermissionsMixin, viewsets.ModelViewSet): + queryset = Thread.objects.all() + ordering = "-created_at" + serializer_class = DetailedThreadSerializer + + def get_queryset(self): + queryset = super().get_queryset().select_related("created_by") + forum_id = self.kwargs.get("forum_id", None) + if forum_id: + queryset = queryset.filter(forum_id=forum_id) + + if self.action == "list": + return queryset + + return queryset.prefetch_related( + "comments", "comments__created_by", *OBJECT_PERMISSIONS_FIELDS + ) + + def get_serializer_class(self): + if self.action == "list": + return PublicThreadSerializer + if self.request and self.request.user.is_authenticated: + return DetailedAdminThreadSerializer + + return super().get_serializer_class() + + def get_object(self) -> Thread: + queryset = self.get_queryset() + pk = self.kwargs.get("pk") + + try: + obj = queryset.get(id=pk) + except (TypeError, OverflowError, Thread.DoesNotExist, ValueError): + obj = get_object_or_404(queryset, slug=pk) + + try: + self.check_object_permissions(self.request, obj) + except NotAuthenticated as e: + raise Http404 from e + except PermissionDenied as e: + raise PermissionDenied from e + return obj diff --git a/lego/apps/joblistings/migrations/0010_alter_joblisting_visible_from.py b/lego/apps/joblistings/migrations/0010_alter_joblisting_visible_from.py new file mode 100644 index 000000000..001eb87e0 --- /dev/null +++ b/lego/apps/joblistings/migrations/0010_alter_joblisting_visible_from.py @@ -0,0 +1,17 @@ +# Generated by Django 4.0.10 on 2023-11-21 19:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("joblistings", "0009_alter_joblisting_created_by_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="joblisting", + name="visible_from", + field=models.DateTimeField(), + ), + ] diff --git a/lego/apps/joblistings/models.py b/lego/apps/joblistings/models.py index dd54251f3..e31637adb 100644 --- a/lego/apps/joblistings/models.py +++ b/lego/apps/joblistings/models.py @@ -22,7 +22,7 @@ class Joblisting(Content, BasisModel): ) contact_mail = models.EmailField(blank=True) deadline = models.DateTimeField(null=True) - visible_from = models.DateTimeField(auto_now_add=True) + visible_from = models.DateTimeField() visible_to = models.DateTimeField() job_type = models.CharField(max_length=20, choices=JOB_TYPE_CHOICES) workplaces = models.ManyToManyField(Workplace) diff --git a/lego/apps/joblistings/tests/test_joblistings_api.py b/lego/apps/joblistings/tests/test_joblistings_api.py index 814d9710c..3844373a6 100644 --- a/lego/apps/joblistings/tests/test_joblistings_api.py +++ b/lego/apps/joblistings/tests/test_joblistings_api.py @@ -51,6 +51,20 @@ "application_url": "http://www.vg.no", "workplaces": [{"town": "Oslo"}, {"town": "Trondheim"}], }, + { + "title": "Itera", + "company": 1, + "description": "En bedrift.", + "text": "Text3", + "deadline": "2025-11-03T02:00:00+00:00", + "visible_from": "2050-09-30T16:15:00+00:00", + "visible_to": "2055-09-30T16:15:00+00:00", + "job_type": "summer_job", + "from_year": 3, + "to_year": 5, + "application_url": "http://www.vg.no", + "workplaces": [{"town": "Oslo"}, {"town": "Trondheim"}], + }, ] @@ -101,6 +115,13 @@ def test_list_after_visible_to(self): self.assertEqual(joblisting_response.status_code, status.HTTP_200_OK) self.assertEqual(len(joblisting_response.json()["results"]), 3) + def test_before_visible(self): + joblisting = Joblisting.objects.all().first() + joblisting.visible_from = timezone.now() + timedelta(days=2) + joblisting.save() + joblisting_response = self.client.get(_get_detail_url(1)) + self.assertEqual(joblisting_response.status_code, status.HTTP_404_NOT_FOUND) + class RetrieveJoblistingsTestCase(BaseAPITestCase): fixtures = [ diff --git a/lego/apps/joblistings/views.py b/lego/apps/joblistings/views.py index b592681a7..3aa041626 100644 --- a/lego/apps/joblistings/views.py +++ b/lego/apps/joblistings/views.py @@ -31,6 +31,8 @@ def get_object(self) -> Joblisting: self.check_object_permissions(self.request, obj) except PermissionError: raise Http404 from None + if obj.visible_from > timezone.now() and self.request.user != obj.created_by: + raise Http404 from None return obj def get_serializer_class(self): diff --git a/lego/apps/lending/managers.py b/lego/apps/lending/managers.py index 375865db2..f131aa6cf 100644 --- a/lego/apps/lending/managers.py +++ b/lego/apps/lending/managers.py @@ -27,6 +27,20 @@ def create(self, *args, **kwargs): return lending_instance + #TODO: We probably need another template for accepting instances + def update(self, *args, **kwargs): + from lego.apps.lending.notifications import LendingInstanceNotification + + lending_instance = super().update(*args, **kwargs) + notification = LendingInstanceNotification( + lending_instance=lending_instance, + user=lending_instance.created_by, + ) + notification.notify() + + return lending_instance + + def get_queryset(self): return super().get_queryset().select_related("created_by") diff --git a/lego/apps/lending/models.py b/lego/apps/lending/models.py index d08ead6d3..4867851f8 100644 --- a/lego/apps/lending/models.py +++ b/lego/apps/lending/models.py @@ -3,8 +3,10 @@ from django.db import models from lego.apps.files.fields import ImageField from lego.apps.files.models import FileField +from lego.apps.lending.permissions import LendingInstancePermissionHandler from lego.apps.lending.validators import responsible_roles_validator from django.contrib.postgres.fields import ArrayField +from lego.apps.permissions.models import ObjectPermissionsModel from lego.apps.users import constants from lego.apps.lending.managers import LendingInstanceManager from lego.apps.users.models import User @@ -16,7 +18,7 @@ # Create your models here. -class LendableObject(BasisModel): +class LendableObject(BasisModel, ObjectPermissionsModel): title = models.CharField(max_length=128, null=False, blank=False) description = models.TextField(null=False, blank=True) has_contract = models.BooleanField(default=False, null=False, blank=False) @@ -39,8 +41,11 @@ class LendableObject(BasisModel): def get_furthest_booking_date(self): return timezone.now() + timedelta(days=14) + class Meta: + abstract = False -class LendingInstance(BasisModel): + +class LendingInstance(BasisModel, ObjectPermissionsModel): lendable_object = models.ForeignKey(LendableObject, on_delete=models.CASCADE) start_date = models.DateTimeField(null=True) end_date = models.DateTimeField(null=True) @@ -52,3 +57,7 @@ class LendingInstance(BasisModel): @property def active(self): return timezone.now() < self.end_date and timezone.now() > self.start_date + + class Meta: + abstract = False + permission_handler = LendingInstancePermissionHandler() diff --git a/lego/apps/lending/permissions.py b/lego/apps/lending/permissions.py new file mode 100644 index 000000000..7bf9e0cef --- /dev/null +++ b/lego/apps/lending/permissions.py @@ -0,0 +1,60 @@ +from lego.apps.permissions.constants import CREATE, DELETE, EDIT, LIST, VIEW +from lego.apps.permissions.permissions import PermissionHandler +from django.db.models import Q + + +class LendingInstancePermissionHandler(PermissionHandler): + force_object_permission_check = True + authentication_map = {LIST: False, VIEW: False} + default_require_auth = True + + def has_perm( + self, + user, + perm, + obj=None, + queryset=None, + check_keyword_permissions=True, + **kwargs + ): + if perm == LIST: + return True + if not user.is_authenticated: + return False + + # Check object permissions before keywork perms + if obj is not None: + if self.has_object_permissions(user, perm, obj): + return True + + if perm == EDIT and self.created_by(user, obj): + return True + + if perm == CREATE: + return True + + has_perm = super().has_perm( + user, perm, obj, queryset, check_keyword_permissions, **kwargs + ) + + if has_perm: + return True + + return False + + def has_object_permissions(self, user, perm, obj): + if perm == DELETE: + return False + if perm == EDIT and obj.created_by == user: + return True + if perm == CREATE: + return True + return not (perm == DELETE or perm == EDIT) + + def filter_queryset(self, user, queryset, **kwargs): + if user.is_authenticated: + return queryset.filter( + Q(created_by=user) | + Q(lendable_object__responsible_groups__in=user.abakus_groups) + ).distinct() + return queryset.none() diff --git a/lego/apps/lending/serializers.py b/lego/apps/lending/serializers.py index eeb167360..922deb493 100644 --- a/lego/apps/lending/serializers.py +++ b/lego/apps/lending/serializers.py @@ -4,17 +4,27 @@ from lego.apps.lending.models import LendableObject, LendingInstance from lego.apps.users.serializers.users import PublicUserSerializer -from lego.utils.serializers import BasisModelSerializer +from lego.utils.serializers import BasisModelSerializer, ObjectPermissionsSerializerMixin -class LendableObjectSerializer(serializers.ModelSerializer): +class DetailedLendableObjectSerializer(BasisModelSerializer): image = ImageField(required=False, options={"height": 500}) class Meta: model = LendableObject fields = "__all__" +class DetailedAdminLendableObjectSerializer( + ObjectPermissionsSerializerMixin, DetailedLendableObjectSerializer +): + class Meta: + model = LendableObject + fields = ( + DetailedLendableObjectSerializer.Meta.fields + + ObjectPermissionsSerializerMixin.Meta.fields + ) + -class LendingInstanceSerializer(BasisModelSerializer): +class DetailedLendingInstanceSerializer(BasisModelSerializer): author = PublicUserSerializer(read_only=True, source="created_by") class Meta: @@ -52,3 +62,13 @@ def validate(self, data): ) return data + +class DetailedAdminLendingInstanceSerializer( + ObjectPermissionsSerializerMixin, DetailedLendingInstanceSerializer +): + class Meta: + model = LendingInstance + fields = ( + DetailedLendingInstanceSerializer.Meta.fields + + ObjectPermissionsSerializerMixin.Meta.fields + ) diff --git a/lego/apps/lending/views.py b/lego/apps/lending/views.py index 8675e4548..c1f0df2c9 100644 --- a/lego/apps/lending/views.py +++ b/lego/apps/lending/views.py @@ -13,33 +13,56 @@ from lego.apps.lending.filters import LendableObjectFilterSet, LendingInstanceFilterSet from lego.apps.lending.models import LendableObject, LendingInstance from lego.apps.lending.serializers import ( - LendableObjectSerializer, - LendingInstanceSerializer, + DetailedLendableObjectSerializer, + DetailedLendingInstanceSerializer, + DetailedAdminLendableObjectSerializer, + DetailedAdminLendingInstanceSerializer ) from lego.apps.permissions.api.views import AllowedPermissionsMixin +from lego.apps.permissions.utils import get_permission_handler class LendableObjectViewSet(AllowedPermissionsMixin, viewsets.ModelViewSet): queryset = LendableObject.objects.all() - serializer_class = LendableObjectSerializer + serializer_class = DetailedLendableObjectSerializer filterset_class = LendableObjectFilterSet http_method_names = ["get", "post", "patch", "delete", "head", "options"] permission_classes = [ permissions.IsAuthenticated, ] + def get_serializer_class(self): + if self.request and self.request.user.is_authenticated: + return DetailedAdminLendableObjectSerializer + return super().get_serializer_class() + class LendingInstanceViewSet(AllowedPermissionsMixin, viewsets.ModelViewSet): queryset = LendingInstance.objects.all() - serializer_class = LendingInstanceSerializer + serializer_class = DetailedLendingInstanceSerializer filterset_class = LendingInstanceFilterSet http_method_names = ["get", "post", "patch", "delete", "head", "options"] permission_classes = [ permissions.IsAuthenticated, ] + + def get_queryset(self): + if self.request is None: + return LendingInstance.objects.none() + + permission_handler = get_permission_handler(LendingInstance) + return permission_handler.filter_queryset( + self.request.user, + ) + + def get_serializer_class(self): + if self.request and self.request.user.is_authenticated: + return DetailedAdminLendingInstanceSerializer + return super().get_serializer_class() + def create(self, request): - serializer = LendingInstanceSerializer( + serializer = DetailedLendingInstanceSerializer( data=request.data, context={"request": request} ) if serializer.is_valid(raise_exception=True): diff --git a/lego/apps/meetings/fixtures/test_meetings.yaml b/lego/apps/meetings/fixtures/test_meetings.yaml index 3c086409b..b62853d9f 100644 --- a/lego/apps/meetings/fixtures/test_meetings.yaml +++ b/lego/apps/meetings/fixtures/test_meetings.yaml @@ -27,3 +27,26 @@ end_time: "2011-02-01T23:59:00+00:00" report: "tomp" report_author: 3 + +- model: meetings.Meeting + pk: 4 + fields: + title: Enda et tomt møte + location: "null" + start_time: "2024-02-01T17:15:00+00:00" + end_time: "2024-02-01T23:59:00+00:00" + report: "hei" + report_author: 3 + +- model: meetings.MeetingInvitation + pk: 1 + fields: + user: 8 + meeting: 4 + status: "ATTENDING" + +- model: meetings.MeetingInvitation + pk: 2 + fields: + user: 9 + meeting: 4 diff --git a/lego/apps/meetings/models.py b/lego/apps/meetings/models.py index 13ad18285..795953e45 100644 --- a/lego/apps/meetings/models.py +++ b/lego/apps/meetings/models.py @@ -105,8 +105,17 @@ def restricted_lookup(self): """ return self.invited_users, [] - def announcement_lookup(self): - return self.invited_users + def announcement_lookup(self, meeting_invitation_status) -> list[User]: + meeting_invitations = self.invitations + + if meeting_invitation_status: + meeting_invitations = meeting_invitations.filter( + status=meeting_invitation_status + ) + + return User.objects.filter( + id__in=meeting_invitations.values_list("user", flat=True) + ) @property def content_target(self): diff --git a/lego/apps/meetings/serializers.py b/lego/apps/meetings/serializers.py index c0c82159a..83c9794bb 100644 --- a/lego/apps/meetings/serializers.py +++ b/lego/apps/meetings/serializers.py @@ -5,6 +5,7 @@ from lego.apps.content.fields import ContentSerializerField from lego.apps.meetings import constants from lego.apps.meetings.models import Meeting, MeetingInvitation +from lego.apps.reactions.models import Reaction from lego.apps.users.fields import PublicUserField from lego.apps.users.models import AbakusGroup, User from lego.apps.users.serializers.users import PublicUserSerializer @@ -36,6 +37,15 @@ class Meta: fields = ("status",) +class ReactionsSerializer(serializers.ModelSerializer): + author = PublicUserSerializer(read_only=True, source="created_by") + + class Meta: + model = Reaction + fields = ("id", "emoji", "author") + read_only = True + + class MeetingGroupInvite(serializers.Serializer): group = PrimaryKeyRelatedFieldNoPKOpt(queryset=AbakusGroup.objects.all()) @@ -63,6 +73,7 @@ class MeetingDetailSerializer(BasisModelSerializer): comments = CommentSerializer(read_only=True, many=True) content_target = CharField(read_only=True) reactions_grouped = serializers.SerializerMethodField() + reactions = ReactionsSerializer(many=True, read_only=True) def get_reactions_grouped(self, obj): user = self.context["request"].user @@ -85,6 +96,7 @@ class Meta: "content_target", "mazemap_poi", "reactions_grouped", + "reactions", ) read_only = True diff --git a/lego/apps/notifications/fixtures/test_announcements.yaml b/lego/apps/notifications/fixtures/test_announcements.yaml index 8e7a76cea..585400a77 100644 --- a/lego/apps/notifications/fixtures/test_announcements.yaml +++ b/lego/apps/notifications/fixtures/test_announcements.yaml @@ -2,7 +2,7 @@ pk: 1 fields: message: abakom - sent: '2019-02-10T14:38:49.164657Z' + sent: "2019-02-10T14:38:49.164657Z" groups: [3] created_by: 4 @@ -11,7 +11,7 @@ fields: message: event_from_arrkom from_group: 4 - sent: '2019-02-10T14:38:49.164657Z' + sent: "2019-02-10T14:38:49.164657Z" events: [1] created_by: 4 @@ -19,7 +19,7 @@ pk: 3 fields: message: event_abakom - sent: '2019-02-10T14:38:49.164657Z' + sent: "2019-02-10T14:38:49.164657Z" groups: [3] events: [1] created_by: 4 @@ -29,7 +29,7 @@ fields: message: user_from_webkom from_group: 12 - users: [1, 2,3] + users: [1, 2, 3] created_by: 4 - model: notifications.Announcement @@ -46,3 +46,34 @@ message: test_from_user_webkom groups: [1] created_by: 3 + +- model: notifications.Announcement + pk: 7 + fields: + message: another_event_from_arrkom + events: [1] + exclude_waiting_list: false + created_by: 9 + +- model: notifications.Announcement + pk: 8 + fields: + message: yet_another_event_from_arrkom + events: [1] + exclude_waiting_list: true + created_by: 9 + +- model: notifications.Announcement + pk: 9 + fields: + message: meeting + meetings: [4] + created_by: 1 + +- model: notifications.Announcement + pk: 10 + fields: + message: meeting + meetings: [4] + meeting_invitation_status: "ATTENDING" + created_by: 1 diff --git a/lego/apps/notifications/migrations/0015_announcement_exclude_waiting_list_and_more.py b/lego/apps/notifications/migrations/0015_announcement_exclude_waiting_list_and_more.py new file mode 100644 index 000000000..f85251d9d --- /dev/null +++ b/lego/apps/notifications/migrations/0015_announcement_exclude_waiting_list_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.0.10 on 2024-03-07 14:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("notifications", "0014_alter_notificationsetting_notification_type"), + ] + + operations = [ + migrations.AddField( + model_name="announcement", + name="exclude_waiting_list", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="announcement", + name="meeting_invitation_status", + field=models.CharField( + blank=True, + choices=[ + ("NO_ANSWER", "NO_ANSWER"), + ("ATTENDING", "ATTENDING"), + ("NOT_ATTENDING", "NOT_ATTENDING"), + ], + max_length=50, + ), + ), + ] diff --git a/lego/apps/notifications/models.py b/lego/apps/notifications/models.py index 62dff51b6..87e7598df 100644 --- a/lego/apps/notifications/models.py +++ b/lego/apps/notifications/models.py @@ -3,6 +3,7 @@ from django.utils import timezone from lego.apps.action_handlers.events import handle_event +from lego.apps.meetings import constants from lego.utils.models import BasisModel from .constants import ( @@ -76,12 +77,16 @@ class Announcement(BasisModel): message = models.TextField() sent = models.DateTimeField(null=True, default=None) - MANY_TO_MANY_RELATIONS = ["users", "groups", "events", "meetings"] + MANY_TO_MANY_RELATIONS = ["users", "groups"] users = models.ManyToManyField("users.User", blank=True) groups = models.ManyToManyField("users.AbakusGroup", blank=True) events = models.ManyToManyField("events.Event", blank=True) + exclude_waiting_list = models.BooleanField(default=False) meetings = models.ManyToManyField("meetings.Meeting", blank=True) + meeting_invitation_status = models.CharField( + max_length=50, choices=constants.INVITATION_STATUS_TYPES, blank=True + ) from_group = models.ForeignKey( "users.AbakusGroup", on_delete=models.PROTECT, @@ -104,6 +109,12 @@ def lookup_recipients(self): users = announcement_lookup() all_users += list(users) + for event in self.events.all(): + all_users += event.announcement_lookup(self.exclude_waiting_list) + + for meeting in self.meetings.all(): + all_users += meeting.announcement_lookup(self.meeting_invitation_status) + return list(set(all_users)) def send(self): diff --git a/lego/apps/notifications/serializers.py b/lego/apps/notifications/serializers.py index 059cbda7f..c56dbf23d 100644 --- a/lego/apps/notifications/serializers.py +++ b/lego/apps/notifications/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from lego.apps.events.serializers.events import EventReadSerializer -from lego.apps.meetings.serializers import MeetingDetailSerializer +from lego.apps.meetings.serializers import MeetingListSerializer from lego.apps.users.serializers.abakus_groups import PublicAbakusGroupSerializer from lego.apps.users.serializers.users import PublicUserSerializer from lego.utils.serializers import BasisModelSerializer @@ -30,7 +30,7 @@ class AnnouncementListSerializer(BasisModelSerializer): users = PublicUserSerializer(many=True, read_only=True) groups = PublicAbakusGroupSerializer(many=True, read_only=True) events = EventReadSerializer(many=True, read_only=True) - meetings = MeetingDetailSerializer(many=True, read_only=True) + meetings = MeetingListSerializer(many=True, read_only=True) from_group = PublicAbakusGroupSerializer(read_only=True) class Meta: @@ -43,7 +43,9 @@ class Meta: "users", "groups", "events", + "exclude_waiting_list", "meetings", + "meeting_invitation_status", ) read_only_fields = ("sent",) @@ -59,6 +61,8 @@ class Meta(AnnouncementListSerializer.Meta): "users", "groups", "events", + "exclude_waiting_list", "meetings", + "meeting_invitation_status", ) read_only_fields = ("sent",) diff --git a/lego/apps/notifications/tests/test_views.py b/lego/apps/notifications/tests/test_views.py index 06f2d189b..10dc15e48 100644 --- a/lego/apps/notifications/tests/test_views.py +++ b/lego/apps/notifications/tests/test_views.py @@ -81,6 +81,7 @@ class AnnouncementViewSetTestCase(BaseAPITestCase): "test_events.yaml", "test_companies.yaml", "test_announcements.yaml", + "test_meetings.yaml", ] def setUp(self): @@ -238,3 +239,59 @@ def test_send_announcement_unauthorized(self): self.client.force_authenticate(self.unauthorized_user) response = self.client.post(f"{self.url}{self.unsent_announcement.id}/send/") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_send_announcement_twice(self): + """ + An announcement can not be sent twice + """ + + self.client.force_authenticate(self.authorized_user) + response = self.client.post(f"{self.url}{self.unsent_announcement.id}/send/") + self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) + response = self.client.post(f"{self.url}{self.unsent_announcement.id}/send/") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_recipients_lookup_to_users(self): + """ + An announcement should be sent to the specified users + """ + + announcement = Announcement.objects.get(pk=4) + recipients = announcement.lookup_recipients() + self.assertEqual(len(recipients), 3) + + def test_recipients_lookup_for_event(self): + """ + Everyone registered to an event should receive the announcement + """ + announcement = Announcement.objects.get(pk=7) + recipients = announcement.lookup_recipients() + self.assertEqual(len(recipients), 3) + + def test_recipients_lookup_for_event_exclude_waiting_list(self): + """ + Everyone registered to an event, except those on waiting list, + should receive the announcement + """ + + announcement = Announcement.objects.get(pk=8) + recipients = announcement.lookup_recipients() + self.assertEqual(len(recipients), 2) + + def test_recipients_lookup_for_meeting(self): + """ + Everyone invited to a meeting should receive the announcement + """ + + announcement = Announcement.objects.get(pk=9) + recipients = announcement.lookup_recipients() + self.assertEqual(len(recipients), 2) + + def test_recipients_lookup_for_only_meeting_attendees(self): + """ + Only those invited to a meeting and accepted should receive the announcement + """ + + announcement = Announcement.objects.get(pk=10) + recipients = announcement.lookup_recipients() + self.assertEqual(len(recipients), 1) diff --git a/lego/apps/oauth/fixtures/development_applications.yaml b/lego/apps/oauth/fixtures/development_applications.yaml index 86b52da66..20aaf3344 100644 --- a/lego/apps/oauth/fixtures/development_applications.yaml +++ b/lego/apps/oauth/fixtures/development_applications.yaml @@ -10,3 +10,18 @@ skip_authorization: true created: '2016-01-1T10:00:00+00:00' updated: '2016-01-1T10:00:00+00:00' +- model: oauth.APIApplication + pk: 2 + fields: + client_id: sC6GkHr8GS2OSRpFzKL2aASnz2eTPLYqK8TAGihF + user: 3 + redirect_uris: http://127.0.0.1:5000/complete/lego/ + client_type: public + authorization_grant_type: authorization-code + client_secret: fBySp20wgSEcxGmFFHB8BHEqjGu0oU0e1QmDyQ7bU8GZLFsa3GIPAbXyYlbOxnwslZLKGEQJr96i9u50bL4Iq1wWQrmDPoUc8P7nJHAENbrw1l5HblOgDkoKvxPKHnk2 + name: Opptak + skip_authorization: false + created: 2023-08-22 07:03:43.167045+00:00 + updated: 2023-08-22 07:03:43.167059+00:00 + algorithm: '' + description: Testopptak \ No newline at end of file diff --git a/lego/apps/reactions/models.py b/lego/apps/reactions/models.py index 4dcc0bb3c..fc7601bac 100644 --- a/lego/apps/reactions/models.py +++ b/lego/apps/reactions/models.py @@ -17,11 +17,9 @@ def get_queryset(self): class Reaction(BasisModel): emoji = models.ForeignKey(Emoji, on_delete=models.CASCADE) - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField(db_index=True) content_object = GenericForeignKey() - objects = ReactionManager() # type: ignore class Meta: diff --git a/lego/apps/restricted/tests/test_models.py b/lego/apps/restricted/tests/test_models.py index 3635aedba..5e799ceb8 100644 --- a/lego/apps/restricted/tests/test_models.py +++ b/lego/apps/restricted/tests/test_models.py @@ -22,7 +22,9 @@ def test_lookup_recipients(self): restricted_mail = RestrictedMail.objects.get(id=1) recipients = restricted_mail.lookup_recipients() - self.assertCountEqual(recipients, ["test1@user.com", "test2@user.com"]) + self.assertCountEqual( + recipients, ["test1@user.com", "test2@user.com", "pr@abakus.no"] + ) def test_mark_used(self): """The used field is not None when the item is marked as used""" diff --git a/lego/apps/tags/tests/test_views.py b/lego/apps/tags/tests/test_views.py index b44426382..a9f22313c 100644 --- a/lego/apps/tags/tests/test_views.py +++ b/lego/apps/tags/tests/test_views.py @@ -31,6 +31,8 @@ def test_fetch_detail(self): "quote": 0, "joblisting": 0, "poll": 0, + "forum": 0, + "thread": 0, }, }, ) diff --git a/lego/apps/users/filters.py b/lego/apps/users/filters.py index 052e3e6b3..0435a1379 100644 --- a/lego/apps/users/filters.py +++ b/lego/apps/users/filters.py @@ -1,11 +1,16 @@ from django.db.models import F, Value from django.db.models.functions import Concat -from django_filters.rest_framework import CharFilter, FilterSet +from django_filters.rest_framework import BaseInFilter, CharFilter, FilterSet from lego.apps.users.models import AbakusGroup, Membership, MembershipHistory, Penalty +class CharInFilter(BaseInFilter, CharFilter): + pass + + class MembershipFilterSet(FilterSet): + role = CharInFilter(field_name="role", lookup_expr="in") userUsername = CharFilter(field_name="user__username", lookup_expr="icontains") userFullname = CharFilter(field_name="userFullname", method="user__fullname") abakusGroupName = CharFilter( diff --git a/lego/apps/users/serializers/penalties.py b/lego/apps/users/serializers/penalties.py index 6ceb052d4..e33fc52e5 100644 --- a/lego/apps/users/serializers/penalties.py +++ b/lego/apps/users/serializers/penalties.py @@ -1,9 +1,13 @@ from rest_framework import serializers +from lego.apps.events.fields import PublicEventField +from lego.apps.events.models import Event from lego.apps.users.models import Penalty class PenaltySerializer(serializers.ModelSerializer): + source_event = PublicEventField(queryset=Event.objects.all(), required=False) + class Meta: model = Penalty fields = ( diff --git a/lego/settings/base.py b/lego/settings/base.py index d3451c226..2480c4afc 100644 --- a/lego/settings/base.py +++ b/lego/settings/base.py @@ -45,6 +45,7 @@ "lego.apps.files", "lego.apps.flatpages", "lego.apps.followers", + "lego.apps.forums", "lego.apps.frontpage", "lego.apps.gallery", "lego.apps.healthchecks", diff --git a/lego/settings/lego.py b/lego/settings/lego.py index b6b23f6eb..398490764 100644 --- a/lego/settings/lego.py +++ b/lego/settings/lego.py @@ -20,9 +20,9 @@ PENALTY_DURATION = timedelta(days=20) # Tuples for ignored (month, day) intervals PENALTY_IGNORE_SUMMER = ((6, 1), (8, 15)) -PENALTY_IGNORE_WINTER = ((12, 1), (1, 10)) +PENALTY_IGNORE_WINTER = ((11, 18), (1, 10)) -BEDKOM_BOOKING_PERIOD = ((10, 2), (10, 18)) +BEDKOM_BOOKING_PERIOD = ((1, 1), (12, 30)) REGISTRATION_CONFIRMATION_TIMEOUT = 60 * 60 * 24 diff --git a/lego/utils/management/commands/import_nerd.py b/lego/utils/management/commands/import_nerd.py deleted file mode 100644 index b687456c7..000000000 --- a/lego/utils/management/commands/import_nerd.py +++ /dev/null @@ -1,278 +0,0 @@ -import logging -import os -from datetime import datetime - -from django.conf import settings -from django.core.management import call_command -from django.utils.crypto import get_random_string - -import yaml -from slugify import slugify - -from lego.apps.articles.models import Article -from lego.apps.events.models import Event -from lego.apps.files.exceptions import UnknownFileType -from lego.apps.files.models import File -from lego.apps.files.storage import storage -from lego.apps.flatpages.models import Page -from lego.apps.joblistings.models import Joblisting -from lego.apps.meetings.models import Meeting -from lego.apps.quotes.models import Quote -from lego.apps.users.fixtures.initial_abakus_groups import initial_tree -from lego.apps.users.models import AbakusGroup, User -from lego.utils.functions import insert_abakus_groups -from lego.utils.management_command import BaseCommand - -log = logging.getLogger(__name__) -IMPORT_DIRECTORY = ".nerd_export" - -lego_group_ids = { - 1: "Users", - 2: "Abakus", - 3: "Abakom", - 4: "Arrkom", - 5: "backup", - 6: "Bedkom", - 7: "Fagkom", - 8: "LaBamba", - 9: "PR", - 10: "readme", - 11: "Webkom", - 12: "Hovedstyret", - 13: "Interessegrupper", - 14: "Students", - 15: "Datateknologi", - 16: "1. klasse Datateknologi", - 17: "2. klasse Datateknologi", - 18: "3. klasse Datateknologi", - 19: "4. klasse Datateknologi", - 20: "5. klasse Datateknologi", - 21: "Kommunikasjonsteknologi", - 22: "1. klasse Kommunikasjonsteknologi", - 23: "2. klasse Kommunikasjonsteknologi", - 24: "3. klasse Kommunikasjonsteknologi", - 25: "4. klasse Kommunikasjonsteknologi", - 26: "5. klasse Kommunikasjonsteknologi", -} - - -def filepath_to_key(filepath): - return None if not filepath else slugify(filepath.rsplit(".", 1)[0]) - - -class Command(BaseCommand): - help = "Imports data from NERD, requires data (fixtures) to be placed inside a directory." - - def add_arguments(self, parser): - parser.add_argument( - "--dry-run", - action="store_true", - default=False, - help="Dry run the import (only test the import).", - ) - parser.add_argument( - "--yes", - action="store_true", - default=False, - help="Answer yes during questions", - ) - - def call_command(self, *args, **options): - call_command(*args, verbosity=self.verbosity, **options) - - def load_yaml(self, fixture): - with open(fixture, "r") as stream: - try: - return yaml.load(stream) - except yaml.YAMLError as exc: - print(exc) - exit(0) - - def load_fixtures(self, fixtures): - for fixture in fixtures: - path = "lego/apps/{}".format(fixture) - self.call_command("loaddata", path) - - def load_initial_fixtures(self): - log.info("Loading initial fixtures:") - - self.load_fixtures( - ["files/fixtures/initial_files.yaml", "tags/fixtures/initial_tags.yaml"] - ) - - log.info("Done loading initial fixtures!") - - def import_nerd_fixture(self, file_name): - log.info(f"Importing fixture: {file_name}") - path = f"{IMPORT_DIRECTORY}/{file_name}" - self.call_command("loaddata", path) - log.info(f"Done importing fixture: {file_name}") - - def find_group_path(self, key, dictionary, path=None): - if not path: - path = [] - for k, v in dictionary.items(): - if k == key: - yield v, path + [k] - elif isinstance(v, dict): - for result, path_str in self.find_group_path(key, v, path + [k]): - yield result, path_str - elif isinstance(v, list): - for d in v: - if isinstance(d, dict): - for result, path_str in self.find_group_path( - key, d, path + [k] - ): - yield result, path_str - - def upload_files(self, uploads_bucket, directory): - for file in os.listdir(directory): - if file[0] == ".": - # Ignore all files / directories that start with a period - continue - file_path = os.path.join(directory, file) - if os.path.isdir(file_path): - continue - try: - file_type = File.get_file_type(file) - except UnknownFileType: - log.warning(f"Unknown filetype for file: {file_path}") - continue - - file_pk = file_path.replace(f"{IMPORT_DIRECTORY}/files/", "") - - log.info(f'Uploading {file_path} to bucket with key: "{file_pk}"') - storage.upload_file(uploads_bucket, file, file_path) - - # Kind of hacky, but no other option on Linux - file_date = datetime.fromtimestamp(os.path.getmtime(file_path)) - # Only create the file if does not exist - File.objects.get_or_create( - pk=file_pk, - defaults={ - "created_at": file_date, - "state": "ready", - "file_type": file_type, - "token": get_random_string(32), - "user": None, - "public": False, - }, - ) - - def handle_fixture_import(self, file_name, skip_questions=False): - if os.path.isdir(f"{IMPORT_DIRECTORY}/{file_name}"): - return - log.info(f"Handling fixture: {file_name}") - if not skip_questions: - choice = input("Do you wish to import this fixture? [Y/n]").lower() - if choice == "n" or choice == "no" or choice == "nei": - print(f"[IGNORE] Ignoring fixture {file_name}\n----------------") - return - self.import_nerd_fixture(file_name) - - def run(self, *args, **options): - if not os.path.exists(IMPORT_DIRECTORY): - log.error(f'Missing import directory: "{IMPORT_DIRECTORY}"') - exit(1) - - self.load_initial_fixtures() - - # Upload files - if os.path.exists(f"{IMPORT_DIRECTORY}/files"): - # Prepare storage bucket for development. - # We skip this in production, where the bucket needs to be created manually. - uploads_bucket = getattr(settings, "AWS_S3_BUCKET", None) - log.info(f"Makes sure the {uploads_bucket} bucket exists") - storage.create_bucket(uploads_bucket) - if not options["yes"]: - choice = input("Do you wish to upload/import all files? [Y/n]").lower() - if choice == "n" or choice == "no" or choice == "nei": - print("[IGNORE] Ignoring upload of files\n----------------") - else: - self.upload_files(uploads_bucket, f"{IMPORT_DIRECTORY}/files") - else: - self.upload_files(uploads_bucket, f"{IMPORT_DIRECTORY}/files") - # End upload files - - file_names = os.listdir(IMPORT_DIRECTORY) - file_names.sort() - - print("Found the following files/fixtures to import:") - for file_name in file_names: - if not os.path.isfile(f"{IMPORT_DIRECTORY}/{file_name}"): - continue - print(f"\t{file_name}") - - if os.path.isfile(f"{IMPORT_DIRECTORY}/1_nerd_export_group_files.yaml"): - self.handle_fixture_import("1_nerd_export_group_files.yaml", options["yes"]) - file_names.remove("1_nerd_export_group_files.yaml") - - if os.path.isfile(f"{IMPORT_DIRECTORY}/1_nerd_export_group_objects.yaml"): - # We need to update the group MPTT mapping - group_tree = initial_tree - nerd_groups = self.load_yaml( - f"{IMPORT_DIRECTORY}/1_nerd_export_group_objects.yaml" - ) - for group_model in nerd_groups: - group_fields = group_model["fields"] - group_fields["id"] = group_model["pk"] - if group_fields["logo"]: - group_fields["logo"] = File.objects.get(key=group_fields["logo"]) - group_name = group_fields["name"] - lego_group_ids[group_model["pk"]] = group_name - group_fields.pop("name", None) - if group_fields["parent"] is None: - group_fields.pop("parent", None) - group_tree[group_name] = [group_fields, {}] - continue - parent = lego_group_ids[group_fields["parent"]] - group_fields.pop("parent", None) - print(f"Attempting to find path for group: {group_name}") - results = list(self.find_group_path(parent, group_tree)) - path = " -> ".join(results[0][1] + [group_name]) - print(f"Found group path: {path}") - if not options["yes"]: - choice = input("Do you wish to import this group? [Y/n]").lower() - if choice == "n" or choice == "no" or choice == "nei": - print(f"[IGNORE] Ignoring {group_name}\n----------------") - continue - results[0][0][1][group_name] = [group_fields, {}] - print( - f"[SUCCESS] Added {group_name} to the import tree\n------------------" - ) - - insert_abakus_groups(group_tree) - AbakusGroup.objects.rebuild() - file_names.remove("1_nerd_export_group_objects.yaml") - # End MPTT mapping - - print("Starting import of fixtures") - for file_name in file_names: - self.handle_fixture_import(file_name, options["yes"]) - - # Make sure user profile pictures are owned by the users - print("Setting ownership of user profile pictures") - for user in User.all_objects.exclude(picture__isnull=True): - if user.picture.token == "token": - continue - user.picture.user = user - user.picture.save() - - # Loop through all the models and generate slug for them if they do not exist - for article in Article.all_objects.all(): - article.save() - - for event in Event.all_objects.all(): - event.save() - - for page in Page.all_objects.all(): - page.save() - - for joblisting in Joblisting.all_objects.all(): - joblisting.save() - - for meeting in Meeting.all_objects.all(): - meeting.save() - - for quote in Quote.all_objects.all(): - quote.save() diff --git a/lego/utils/views.py b/lego/utils/views.py index 063031193..623a08129 100644 --- a/lego/utils/views.py +++ b/lego/utils/views.py @@ -21,7 +21,13 @@ def list(self, request): site_meta = settings.SITE # Allow non-logged in users to see these as well: - allow_anonymous_entities = ["events", "articles", "joblistings", "galleries"] + allow_anonymous_entities = [ + "events", + "articles", + "joblistings", + "galleries", + "forums", + ] # Whereas these require that a user has keyword permissions: permission_entities = { diff --git a/poetry.lock b/poetry.lock index 829c5f4c8..37d3f30e0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiosmtpd" @@ -685,71 +685,63 @@ jinja2 = "*" [[package]] name = "coverage" -version = "7.2.7" +version = "7.4.1" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, - {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, - {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, - {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, - {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, - {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, - {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, - {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, - {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, - {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, - {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, - {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, - {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, - {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, - {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, - {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, - {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, - {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, + {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, + {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, + {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, + {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, + {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, + {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, + {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, + {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, + {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, + {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, + {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, + {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, ] [package.extras] @@ -757,30 +749,34 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "40.0.1" +version = "41.0.6" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "cryptography-40.0.1-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:918cb89086c7d98b1b86b9fdb70c712e5a9325ba6f7d7cfb509e784e0cfc6917"}, - {file = "cryptography-40.0.1-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9618a87212cb5200500e304e43691111570e1f10ec3f35569fdfcd17e28fd797"}, - {file = "cryptography-40.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a4805a4ca729d65570a1b7cac84eac1e431085d40387b7d3bbaa47e39890b88"}, - {file = "cryptography-40.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63dac2d25c47f12a7b8aa60e528bfb3c51c5a6c5a9f7c86987909c6c79765554"}, - {file = "cryptography-40.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0a4e3406cfed6b1f6d6e87ed243363652b2586b2d917b0609ca4f97072994405"}, - {file = "cryptography-40.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1e0af458515d5e4028aad75f3bb3fe7a31e46ad920648cd59b64d3da842e4356"}, - {file = "cryptography-40.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d8aa3609d337ad85e4eb9bb0f8bcf6e4409bfb86e706efa9a027912169e89122"}, - {file = "cryptography-40.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cf91e428c51ef692b82ce786583e214f58392399cf65c341bc7301d096fa3ba2"}, - {file = "cryptography-40.0.1-cp36-abi3-win32.whl", hash = "sha256:650883cc064297ef3676b1db1b7b1df6081794c4ada96fa457253c4cc40f97db"}, - {file = "cryptography-40.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:a805a7bce4a77d51696410005b3e85ae2839bad9aa38894afc0aa99d8e0c3160"}, - {file = "cryptography-40.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cd033d74067d8928ef00a6b1327c8ea0452523967ca4463666eeba65ca350d4c"}, - {file = "cryptography-40.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d36bbeb99704aabefdca5aee4eba04455d7a27ceabd16f3b3ba9bdcc31da86c4"}, - {file = "cryptography-40.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:32057d3d0ab7d4453778367ca43e99ddb711770477c4f072a51b3ca69602780a"}, - {file = "cryptography-40.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f5d7b79fa56bc29580faafc2ff736ce05ba31feaa9d4735048b0de7d9ceb2b94"}, - {file = "cryptography-40.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7c872413353c70e0263a9368c4993710070e70ab3e5318d85510cc91cce77e7c"}, - {file = "cryptography-40.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:28d63d75bf7ae4045b10de5413fb1d6338616e79015999ad9cf6fc538f772d41"}, - {file = "cryptography-40.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6f2bbd72f717ce33100e6467572abaedc61f1acb87b8d546001328d7f466b778"}, - {file = "cryptography-40.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cc3a621076d824d75ab1e1e530e66e7e8564e357dd723f2533225d40fe35c60c"}, - {file = "cryptography-40.0.1.tar.gz", hash = "sha256:2803f2f8b1e95f614419926c7e6f55d828afc614ca5ed61543877ae668cc3472"}, + {file = "cryptography-41.0.6-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:0f27acb55a4e77b9be8d550d762b0513ef3fc658cd3eb15110ebbcbd626db12c"}, + {file = "cryptography-41.0.6-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ae236bb8760c1e55b7a39b6d4d32d2279bc6c7c8500b7d5a13b6fb9fc97be35b"}, + {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afda76d84b053923c27ede5edc1ed7d53e3c9f475ebaf63c68e69f1403c405a8"}, + {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da46e2b5df770070412c46f87bac0849b8d685c5f2679771de277a422c7d0b86"}, + {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ff369dd19e8fe0528b02e8df9f2aeb2479f89b1270d90f96a63500afe9af5cae"}, + {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b648fe2a45e426aaee684ddca2632f62ec4613ef362f4d681a9a6283d10e079d"}, + {file = "cryptography-41.0.6-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5daeb18e7886a358064a68dbcaf441c036cbdb7da52ae744e7b9207b04d3908c"}, + {file = "cryptography-41.0.6-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:068bc551698c234742c40049e46840843f3d98ad7ce265fd2bd4ec0d11306596"}, + {file = "cryptography-41.0.6-cp37-abi3-win32.whl", hash = "sha256:2132d5865eea673fe6712c2ed5fb4fa49dba10768bb4cc798345748380ee3660"}, + {file = "cryptography-41.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:48783b7e2bef51224020efb61b42704207dde583d7e371ef8fc2a5fb6c0aabc7"}, + {file = "cryptography-41.0.6-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8efb2af8d4ba9dbc9c9dd8f04d19a7abb5b49eab1f3694e7b5a16a5fc2856f5c"}, + {file = "cryptography-41.0.6-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5a550dc7a3b50b116323e3d376241829fd326ac47bc195e04eb33a8170902a9"}, + {file = "cryptography-41.0.6-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:85abd057699b98fce40b41737afb234fef05c67e116f6f3650782c10862c43da"}, + {file = "cryptography-41.0.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f39812f70fc5c71a15aa3c97b2bbe213c3f2a460b79bd21c40d033bb34a9bf36"}, + {file = "cryptography-41.0.6-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:742ae5e9a2310e9dade7932f9576606836ed174da3c7d26bc3d3ab4bd49b9f65"}, + {file = "cryptography-41.0.6-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:35f3f288e83c3f6f10752467c48919a7a94b7d88cc00b0668372a0d2ad4f8ead"}, + {file = "cryptography-41.0.6-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4d03186af98b1c01a4eda396b137f29e4e3fb0173e30f885e27acec8823c1b09"}, + {file = "cryptography-41.0.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b27a7fd4229abef715e064269d98a7e2909ebf92eb6912a9603c7e14c181928c"}, + {file = "cryptography-41.0.6-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:398ae1fc711b5eb78e977daa3cbf47cec20f2c08c5da129b7a296055fbb22aed"}, + {file = "cryptography-41.0.6-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7e00fb556bda398b99b0da289ce7053639d33b572847181d6483ad89835115f6"}, + {file = "cryptography-41.0.6-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:60e746b11b937911dc70d164060d28d273e31853bb359e2b2033c9e93e6f3c43"}, + {file = "cryptography-41.0.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3288acccef021e3c3c10d58933f44e8602cf04dba96d9796d70d537bb2f4bbc4"}, + {file = "cryptography-41.0.6.tar.gz", hash = "sha256:422e3e31d63743855e43e5a6fcc8b4acab860f560f9321b0ee6269cc7ed70cc3"}, ] [package.dependencies] @@ -789,12 +785,12 @@ cffi = ">=1.12" [package.extras] docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -pep8test = ["black", "check-manifest", "mypy", "ruff"] -sdist = ["setuptools-rust (>=0.11.4)"] +nox = ["nox"] +pep8test = ["black", "check-sdist", "mypy", "ruff"] +sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] -test = ["iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist"] +test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] -tox = ["tox"] [[package]] name = "cssselect" @@ -1776,13 +1772,13 @@ testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] [[package]] name = "jinja2" -version = "3.1.2" +version = "3.1.3" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, ] [package.dependencies] @@ -2588,20 +2584,20 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pyopenssl" -version = "23.1.1" +version = "23.3.0" description = "Python wrapper module around the OpenSSL library" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pyOpenSSL-23.1.1-py3-none-any.whl", hash = "sha256:9e0c526404a210df9d2b18cd33364beadb0dc858a739b885677bc65e105d4a4c"}, - {file = "pyOpenSSL-23.1.1.tar.gz", hash = "sha256:841498b9bec61623b1b6c47ebbc02367c07d60e0e195f19790817f10cc8db0b7"}, + {file = "pyOpenSSL-23.3.0-py3-none-any.whl", hash = "sha256:6756834481d9ed5470f4a9393455154bc92fe7a64b7bc6ee2c804e78c52099b2"}, + {file = "pyOpenSSL-23.3.0.tar.gz", hash = "sha256:6b2cba5cc46e822750ec3e5a81ee12819850b11303630d575e98108a079c2b12"}, ] [package.dependencies] -cryptography = ">=38.0.0,<41" +cryptography = ">=41.0.5,<42" [package.extras] -docs = ["sphinx (!=5.2.0,!=5.2.0.post0)", "sphinx-rtd-theme"] +docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx-rtd-theme"] test = ["flaky", "pretend", "pytest (>=3.0.1)"] [[package]] @@ -3138,22 +3134,22 @@ files = [ [[package]] name = "tornado" -version = "6.3.2" +version = "6.3.3" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." optional = false python-versions = ">= 3.8" files = [ - {file = "tornado-6.3.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:c367ab6c0393d71171123ca5515c61ff62fe09024fa6bf299cd1339dc9456829"}, - {file = "tornado-6.3.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b46a6ab20f5c7c1cb949c72c1994a4585d2eaa0be4853f50a03b5031e964fc7c"}, - {file = "tornado-6.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2de14066c4a38b4ecbbcd55c5cc4b5340eb04f1c5e81da7451ef555859c833f"}, - {file = "tornado-6.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05615096845cf50a895026f749195bf0b10b8909f9be672f50b0fe69cba368e4"}, - {file = "tornado-6.3.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b17b1cf5f8354efa3d37c6e28fdfd9c1c1e5122f2cb56dac121ac61baa47cbe"}, - {file = "tornado-6.3.2-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:29e71c847a35f6e10ca3b5c2990a52ce38b233019d8e858b755ea6ce4dcdd19d"}, - {file = "tornado-6.3.2-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:834ae7540ad3a83199a8da8f9f2d383e3c3d5130a328889e4cc991acc81e87a0"}, - {file = "tornado-6.3.2-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6a0848f1aea0d196a7c4f6772197cbe2abc4266f836b0aac76947872cd29b411"}, - {file = "tornado-6.3.2-cp38-abi3-win32.whl", hash = "sha256:7efcbcc30b7c654eb6a8c9c9da787a851c18f8ccd4a5a3a95b05c7accfa068d2"}, - {file = "tornado-6.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:0c325e66c8123c606eea33084976c832aa4e766b7dff8aedd7587ea44a604cdf"}, - {file = "tornado-6.3.2.tar.gz", hash = "sha256:4b927c4f19b71e627b13f3db2324e4ae660527143f9e1f2e2fb404f3a187e2ba"}, + {file = "tornado-6.3.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:502fba735c84450974fec147340016ad928d29f1e91f49be168c0a4c18181e1d"}, + {file = "tornado-6.3.3-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:805d507b1f588320c26f7f097108eb4023bbaa984d63176d1652e184ba24270a"}, + {file = "tornado-6.3.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd19ca6c16882e4d37368e0152f99c099bad93e0950ce55e71daed74045908f"}, + {file = "tornado-6.3.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ac51f42808cca9b3613f51ffe2a965c8525cb1b00b7b2d56828b8045354f76a"}, + {file = "tornado-6.3.3-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71a8db65160a3c55d61839b7302a9a400074c9c753040455494e2af74e2501f2"}, + {file = "tornado-6.3.3-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ceb917a50cd35882b57600709dd5421a418c29ddc852da8bcdab1f0db33406b0"}, + {file = "tornado-6.3.3-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:7d01abc57ea0dbb51ddfed477dfe22719d376119844e33c661d873bf9c0e4a16"}, + {file = "tornado-6.3.3-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:9dc4444c0defcd3929d5c1eb5706cbe1b116e762ff3e0deca8b715d14bf6ec17"}, + {file = "tornado-6.3.3-cp38-abi3-win32.whl", hash = "sha256:65ceca9500383fbdf33a98c0087cb975b2ef3bfb874cb35b8de8740cf7f41bd3"}, + {file = "tornado-6.3.3-cp38-abi3-win_amd64.whl", hash = "sha256:22d3c2fa10b5793da13c807e6fc38ff49a4f6e1e3868b0a6f4164768bb8e20f5"}, + {file = "tornado-6.3.3.tar.gz", hash = "sha256:e7d8db41c0181c80d76c982aacc442c0783a2c54d6400fe028954201a2e032fe"}, ] [[package]] @@ -3281,13 +3277,13 @@ twisted = ["twisted (>=20.3.0)", "zope.interface (>=5.2.0)"] [[package]] name = "types-bleach" -version = "6.0.0.3" +version = "6.1.0.0" description = "Typing stubs for bleach" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "types-bleach-6.0.0.3.tar.gz", hash = "sha256:8ce7896d4f658c562768674ffcf07492c7730e128018f03edd163ff912bfadee"}, - {file = "types_bleach-6.0.0.3-py3-none-any.whl", hash = "sha256:d43eaf30a643ca824e16e2dcdb0c87ef9226237e2fa3ac4732a50cb3f32e145f"}, + {file = "types-bleach-6.1.0.0.tar.gz", hash = "sha256:3cf0e55d4618890a00af1151f878b2e2a7a96433850b74e12bede7663d774532"}, + {file = "types_bleach-6.1.0.0-py3-none-any.whl", hash = "sha256:f0bc75d0f6475036ac69afebf37c41d116dfba78dae55db80437caf0fcd35c28"}, ] [[package]] @@ -3336,13 +3332,13 @@ files = [ [[package]] name = "types-python-slugify" -version = "8.0.0.2" +version = "8.0.0.3" description = "Typing stubs for python-slugify" optional = false python-versions = "*" files = [ - {file = "types-python-slugify-8.0.0.2.tar.gz", hash = "sha256:de237b01c69ad3a57ae36a0e8a6a0c65b47bac529483007c1cae2d424cb07cc8"}, - {file = "types_python_slugify-8.0.0.2-py3-none-any.whl", hash = "sha256:9e81f49eac8c805f924695de559fb8dc123ccbd15adb47d40ceaec51c739f044"}, + {file = "types-python-slugify-8.0.0.3.tar.gz", hash = "sha256:868e6610ab9a01c01b2ccc1b982363e694d6bbb4fcf32e0d82688c89dceb4e2c"}, + {file = "types_python_slugify-8.0.0.3-py3-none-any.whl", hash = "sha256:2353c161c79ab6cce955b50720c6cd03586ec297558122236d130e4a19f21209"}, ] [[package]] @@ -3464,17 +3460,17 @@ files = [ [[package]] name = "urllib3" -version = "1.26.15" +version = "1.26.18" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, - {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, + {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, + {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] @@ -3675,4 +3671,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "cede9432aede6718183cecb01d4a4652abe4620a706c20d5935a1c6a074b1f28" +content-hash = "427cb9d9074ddb36b197c06d2a9de7adda35a04c34fe48416284664ab5ad58a4" diff --git a/pyproject.toml b/pyproject.toml index 49694d61e..609e81a70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,18 +75,18 @@ tox = "4.11.3" tblib = "^1.7.0" [tool.poetry.group.coverage.dependencies] -coverage = "7.2.7" +coverage = "7.4.1" [tool.poetry.group.mypy.dependencies] mypy = "1.5.0" mypy-extensions = "1.0.0" django-stubs = "4.2.1" djangorestframework-stubs = "3.14.0" -types-bleach = "6.0.0.3" +types-bleach = "6.1.0.0" types-certifi = "2021.10.8.3" types-docutils = "0.20.0.1" types-Markdown = "3.4.2.9" -types-python-slugify = "8.0.0.2" +types-python-slugify = "8.0.0.3" types-PyYAML = "6.0.12.10" types-requests = "2.31.0.1" types-six = "1.16.21.8"