From 6ac18444faf0cc4c15e9248860c1285ff90879d1 Mon Sep 17 00:00:00 2001 From: Andrew Tavis McAllister Date: Sat, 21 Sep 2024 23:02:51 +0200 Subject: [PATCH 1/7] #938 Exapnd fixtures to product human readable results --- backend/authentication/admin.py | 2 +- backend/authentication/factories.py | 1 + backend/authentication/models.py | 1 + .../management/commands/populate_db.py | 80 +++++++-------- backend/backend/settings.py | 2 +- backend/content/serializers.py | 4 +- backend/entities/factories.py | 12 +-- backend/entities/models.py | 5 +- backend/entities/serializers.py | 3 +- backend/entities/views.py | 14 +-- backend/events/factories.py | 4 +- backend/events/models.py | 3 +- backend/fixtures/superuser.json | 2 +- backend/fixtures/topics.json | 98 +++++++++++++++++++ backend/utils/models.py | 7 ++ backend/utils/utils.py | 30 +----- docker-compose.yml | 3 +- .../components/card/about/CardAboutGroup.vue | 5 +- .../card/search-result/CardSearchResult.vue | 4 +- frontend/composables/fetch.ts | 31 ++++-- .../organizations/[id]/groups/[id]/about.vue | 7 +- frontend/stores/event.ts | 11 ++- frontend/stores/group.ts | 16 ++- frontend/stores/organization.ts | 16 +-- 24 files changed, 236 insertions(+), 125 deletions(-) create mode 100644 backend/fixtures/topics.json create mode 100644 backend/utils/models.py diff --git a/backend/authentication/admin.py b/backend/authentication/admin.py index 9761b2108..ebd476ccb 100644 --- a/backend/authentication/admin.py +++ b/backend/authentication/admin.py @@ -86,7 +86,7 @@ class UserAdmin(BaseUserAdmin): add_form = UserCreationForm # The fields to be used in displaying the User model. - list_display = ["email", "is_admin"] + list_display = ["username", "email", "is_admin"] list_filter = ["is_admin"] fieldsets = [ (None, {"fields": ["email", "password"]}), diff --git a/backend/authentication/factories.py b/backend/authentication/factories.py index e833d3782..987419dc1 100644 --- a/backend/authentication/factories.py +++ b/backend/authentication/factories.py @@ -39,6 +39,7 @@ class Meta: username = factory.Faker("user_name") name = factory.Faker("name") + location = factory.Faker("city") description = factory.Faker("text", max_nb_chars=500) verified = factory.Faker("boolean") verification_method = factory.Faker("word") diff --git a/backend/authentication/models.py b/backend/authentication/models.py index 5f1f3d18b..4ad430c5e 100644 --- a/backend/authentication/models.py +++ b/backend/authentication/models.py @@ -87,6 +87,7 @@ class UserModel(AbstractUser, PermissionsMixin): username = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, blank=True) password = models.CharField(max_length=255) + location = models.CharField(max_length=100, blank=True) description = models.TextField(max_length=500, blank=True) verified = models.BooleanField(default=False) verification_method = models.CharField(max_length=30, blank=True) diff --git a/backend/backend/management/commands/populate_db.py b/backend/backend/management/commands/populate_db.py index 262bd4280..09d376ddd 100644 --- a/backend/backend/management/commands/populate_db.py +++ b/backend/backend/management/commands/populate_db.py @@ -1,10 +1,12 @@ +import random from argparse import ArgumentParser from typing import TypedDict, Unpack from django.core.management.base import BaseCommand -from authentication.factories import UserFactory +from authentication.factories import UserFactory, UserTopicFactory from authentication.models import UserModel +from content.models import Topic from entities.factories import ( GroupFactory, GroupTextFactory, @@ -18,9 +20,9 @@ class Options(TypedDict): users: int - orgs: int - groups: int - events: int + orgs_per_user: int + groups_per_org: int + events_per_org: int class Command(BaseCommand): @@ -28,15 +30,15 @@ class Command(BaseCommand): def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument("--users", type=int, default=10) - parser.add_argument("--opu", type=int, default=1) # orgs per user - parser.add_argument("--gpo", type=int, default=1) # groups per org - parser.add_argument("--epo", type=int, default=1) # events per org + parser.add_argument("--orgs-per-user", type=int, default=1) + parser.add_argument("--groups-per-org", type=int, default=1) + parser.add_argument("--events-per-org", type=int, default=1) def handle(self, *args: str, **options: Unpack[Options]) -> None: - n_users = options.get("users") - n_orgs_per_user = options.get("opu") - n_groups_per_org = options.get("gpo") - n_events_per_org = options.get("epo") + num_users = options.get("users") + num_orgs_per_user = options.get("orgs_per_user") + num_groups_per_org = options.get("groups_per_org") + num_events_per_org = options.get("events_per_org") # Clear all tables before creating new data. UserModel.objects.exclude(username="admin").delete() @@ -44,73 +46,59 @@ def handle(self, *args: str, **options: Unpack[Options]) -> None: Group.objects.all().delete() Event.objects.all().delete() + topics = Topic.objects.all() + try: users = [ UserFactory(username=f"activist_{i}", name=f"Activist {i}") - for i in range(n_users) + for i in range(num_users) ] - for i, user in enumerate(users): - user_location = "Berlin" - user_topic = "Climate" + for u, user in enumerate(users): + user_topic = random.choice(topics) + UserTopicFactory(user_id=user, topic_id=user_topic) - for _ in range(n_orgs_per_user): + for o in range(num_orgs_per_user): user_org = OrganizationFactory( - name=f"{user_location} {user_topic} Organization {i}", + name=f"{user_topic.name} Organization (u: {u} o: {o})", created_by=user, ) - OrganizationTextFactory( - org_id=user_org, - iso="en", - primary=True, - description="This is an org", - get_involved="Get involved!", - donate_prompt="Donate!", - ) + OrganizationTextFactory(org_id=user_org, iso="en", primary=True) - for g in range(n_groups_per_org): + for g in range(num_groups_per_org): user_org_group = GroupFactory( org_id=user_org, - name=f"{user_location} {user_topic} Group {i}-{g}", + name=f"{user_topic.name} Group (u: {u} o: {o} g: {g})", created_by=user, ) GroupTextFactory( - group_id=user_org_group, - iso="en", - primary=True, - description="This is a group", - get_involved="Get involved!", - donate_prompt="Donate!", + group_id=user_org_group, iso="en", primary=True ) - for e in range(n_events_per_org): + for e in range(num_events_per_org): user_org_event = EventFactory( - name=f"{user_location} {user_topic} Event {i}-{e}", + name=f"{user_topic.name} Event (u: {u} o: {o} e: {e})", created_by=user, ) EventTextFactory( - event_id=user_org_event, - iso="en", - primary=True, - description="This is a group", - get_involved="Get involved!", + event_id=user_org_event, iso="en", primary=True ) self.stdout.write( self.style.ERROR( - f"Number of users created: {n_users}\n" - f"Number of organizations created: {n_users * n_orgs_per_user}\n" - f"Number of groups created: {n_users * n_orgs_per_user * n_groups_per_org}\n" - f"Number of events created: {n_users * n_orgs_per_user * n_events_per_org}\n" + f"Number of users created: {num_users}\n" + f"Number of organizations created: {num_users * num_orgs_per_user}\n" + f"Number of groups created: {num_users * num_orgs_per_user * num_groups_per_org}\n" + f"Number of events created: {num_users * num_orgs_per_user * num_events_per_org}\n" ) ) - except Exception as error: + except TypeError as error: self.stdout.write( self.style.ERROR( - f"An error occurred during the creation of dummy data: {error}" + f"A type error occurred during the creation of dummy data: {error}. Make sure to use dashes for populate_db arguments and that they're of the appropriate types." ) ) diff --git a/backend/backend/settings.py b/backend/backend/settings.py index c2f8f7237..e4d73f852 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -174,7 +174,7 @@ "rest_framework.throttling.AnonRateThrottle", "rest_framework.throttling.UserRateThrottle", ], - "DEFAULT_THROTTLE_RATES": {"anon": "7/min", "user": "10/min"}, + "DEFAULT_THROTTLE_RATES": {"anon": "20/min", "user": "30/min"}, "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "DEFAULT_PAGINATION_ORDERS_OBJECTS": False, diff --git a/backend/content/serializers.py b/backend/content/serializers.py index e8fe3e32d..77ba9550e 100644 --- a/backend/content/serializers.py +++ b/backend/content/serializers.py @@ -117,13 +117,13 @@ class Meta: fields = "__all__" def validate(self, data: Dict[str, Union[str, int]]) -> Dict[str, Union[str, int]]: - if data["active"] is True and data["deprecation_date"] is not None: + if data["active"] is True and data.get("deprecation_date") is not None: raise serializers.ValidationError( _("Active topics cannot have a deprecation date."), code="active_topic_with_deprecation_error", ) - if data["active"] is False and data["deprecation_date"] is None: + if data["active"] is False and data.get("deprecation_date") is None: raise serializers.ValidationError( _("Deprecated topics must have a deprecation date."), code="inactive_topic_no_deprecation_error", diff --git a/backend/entities/factories.py b/backend/entities/factories.py index b3892ba77..418645f72 100644 --- a/backend/entities/factories.py +++ b/backend/entities/factories.py @@ -103,9 +103,9 @@ class Meta: group_id = factory.SubFactory(GroupFactory) iso = factory.Faker("word") primary = factory.Faker("boolean") - description = factory.Faker("text") - get_involved = factory.Faker("text") - donate_prompt = factory.Faker("text") + description = factory.Faker(provider="text", locale="la", max_nb_chars=1000) + get_involved = factory.Faker(provider="text", locale="la") + donate_prompt = factory.Faker(provider="text", locale="la") class GroupTopicFactory(factory.django.DjangoModelFactory): @@ -190,9 +190,9 @@ class Meta: org_id = factory.SubFactory(OrganizationFactory) iso = "en" primary = factory.Faker("boolean") - description = factory.Faker("text") - get_involved = factory.Faker("text") - donate_prompt = factory.Faker("text") + description = factory.Faker(provider="text", locale="la", max_nb_chars=1000) + get_involved = factory.Faker(provider="text", locale="la") + donate_prompt = factory.Faker(provider="text", locale="la") class OrganizationTopicFactory(factory.django.DjangoModelFactory): diff --git a/backend/entities/models.py b/backend/entities/models.py index 4dcd1d000..a3e4de22f 100644 --- a/backend/entities/models.py +++ b/backend/entities/models.py @@ -8,6 +8,7 @@ from django.db import models from authentication import enums +from utils.models import ISO_CHOICES # MARK: Main Tables @@ -129,7 +130,7 @@ def __str__(self) -> str: class GroupText(models.Model): group_id = models.ForeignKey(Group, on_delete=models.CASCADE) - iso = models.CharField(max_length=2) + iso = models.CharField(max_length=2, choices=ISO_CHOICES) primary = models.BooleanField(default=False) description = models.TextField(max_length=500) get_involved = models.TextField(max_length=500, blank=True) @@ -216,7 +217,7 @@ def __str__(self) -> str: class OrganizationText(models.Model): org_id = models.ForeignKey(Organization, on_delete=models.CASCADE) - iso = models.CharField(max_length=2) + iso = models.CharField(max_length=2, choices=ISO_CHOICES) primary = models.BooleanField(default=False) description = models.TextField(max_length=2500) get_involved = models.TextField(max_length=500, blank=True) diff --git a/backend/entities/serializers.py b/backend/entities/serializers.py index afceeb74b..fb8efea4a 100644 --- a/backend/entities/serializers.py +++ b/backend/entities/serializers.py @@ -79,8 +79,8 @@ def validate(self, data: dict[str, Any]) -> dict[str, Any]: raise serializers.ValidationError( "You must accept the terms of service to create an organization." ) - return data + return data def create(self, validated_data: dict[str, Any]) -> Organization: description = validated_data.pop("description", None) @@ -90,6 +90,7 @@ def create(self, validated_data: dict[str, Any]) -> Organization: org_id=org, description=description ) org.org_text = org_text + return org diff --git a/backend/entities/views.py b/backend/entities/views.py index 972b0d5f0..aab425400 100644 --- a/backend/entities/views.py +++ b/backend/entities/views.py @@ -2,7 +2,7 @@ from django.utils import timezone from rest_framework import status, viewsets from rest_framework.authentication import TokenAuthentication -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.request import Request from rest_framework.response import Response from rest_framework.throttling import AnonRateThrottle, UserRateThrottle @@ -116,18 +116,15 @@ class OrganizationViewSet(viewsets.ModelViewSet[Organization]): serializer_class = OrganizationSerializer pagination_class = CustomPagination throttle_classes = [AnonRateThrottle, UserRateThrottle] - permission_classes = [ - IsAuthenticated, - ] - authentication_classes = [ - TokenAuthentication, - ] + permission_classes = [IsAuthenticatedOrReadOnly] + authentication_classes = [TokenAuthentication] def create(self, request: Request) -> Response: serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) org = serializer.save(created_by=request.user) OrganizationApplication.objects.create(org_id=org) + return Response(serializer.data, status=status.HTTP_201_CREATED) def retrieve(self, request: Request, pk: str | None = None) -> Response: @@ -139,6 +136,7 @@ def retrieve(self, request: Request, pk: str | None = None) -> Response: def list(self, request: Request) -> Response: serializer = self.get_serializer(self.get_queryset(), many=True) + return Response(serializer.data, status=status.HTTP_200_OK) def update(self, request: Request, pk: str | None = None) -> Response: @@ -157,6 +155,7 @@ def update(self, request: Request, pk: str | None = None) -> Response: serializer = self.get_serializer(org, data=request.data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() + return Response(serializer.data, status.HTTP_200_OK) def partial_update(self, request: Request, pk: str | None = None) -> Response: @@ -175,6 +174,7 @@ def partial_update(self, request: Request, pk: str | None = None) -> Response: serializer = self.get_serializer(org, data=request.data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() + return Response(serializer.data, status.HTTP_200_OK) def destroy(self, request: Request, pk: str | None = None) -> Response: diff --git a/backend/events/factories.py b/backend/events/factories.py index c2aafa09d..1a8d961f4 100644 --- a/backend/events/factories.py +++ b/backend/events/factories.py @@ -142,8 +142,8 @@ class Meta: event_id = factory.SubFactory(EventFactory) iso = factory.Faker("word") primary = factory.Faker("boolean") - description = factory.Faker("text") - get_involved = factory.Faker("text") + description = factory.Faker(provider="text", locale="la", max_nb_chars=1000) + get_involved = factory.Faker(provider="text", locale="la") class EventTopicFactory(factory.django.DjangoModelFactory): diff --git a/backend/events/models.py b/backend/events/models.py index 936d8685b..208ed0a4b 100644 --- a/backend/events/models.py +++ b/backend/events/models.py @@ -8,6 +8,7 @@ from django.db import models from backend.mixins.models import CreationDeletionMixin +from utils.models import ISO_CHOICES # MARK: Main Tables @@ -130,7 +131,7 @@ def __str__(self) -> str: class EventText(models.Model): event_id = models.ForeignKey(Event, on_delete=models.CASCADE) - iso = models.CharField(max_length=2) + iso = models.CharField(max_length=2, choices=ISO_CHOICES) primary = models.BooleanField() description = models.TextField(max_length=500) get_involved = models.TextField(max_length=500, blank=True) diff --git a/backend/fixtures/superuser.json b/backend/fixtures/superuser.json index 1b22a93a1..1b741b6fd 100644 --- a/backend/fixtures/superuser.json +++ b/backend/fixtures/superuser.json @@ -20,7 +20,7 @@ "email": "admin@activist.org", "is_high_risk": false, "is_active": true, - "is_admin": false, + "is_admin": true, "is_confirmed": true, "groups": [], "user_permissions": [] diff --git a/backend/fixtures/topics.json b/backend/fixtures/topics.json new file mode 100644 index 000000000..00995272c --- /dev/null +++ b/backend/fixtures/topics.json @@ -0,0 +1,98 @@ +[ +{ + "model": "content.topic", + "pk": "24b970c6-5231-403b-9e9d-7de8375193ef", + "fields": { + "name": "Democracy", + "active": true, + "description": "Democracy", + "creation_date": "2024-09-21T20:21:47.121Z", + "last_updated": "2024-09-21T20:21:47.121Z", + "deprecation_date": null + } +}, +{ + "model": "content.topic", + "pk": "2c649964-f64c-4693-be31-0a9fdf981974", + "fields": { + "name": "Women's Rights", + "active": true, + "description": "Women's Rights", + "creation_date": "2024-09-21T20:24:41.462Z", + "last_updated": "2024-09-21T20:24:41.462Z", + "deprecation_date": null + } +}, +{ + "model": "content.topic", + "pk": "2d49b6e2-8916-4543-83f3-9782e8a9680a", + "fields": { + "name": "Animal Rights", + "active": true, + "description": "Animal Rights", + "creation_date": "2024-09-21T20:22:07.547Z", + "last_updated": "2024-09-21T20:22:07.547Z", + "deprecation_date": null + } +}, +{ + "model": "content.topic", + "pk": "b3c6c355-a084-4766-9963-3e54e1af3004", + "fields": { + "name": "Racial Justice", + "active": true, + "description": "Racial Justice", + "creation_date": "2024-09-21T20:24:28.226Z", + "last_updated": "2024-09-21T20:24:28.226Z", + "deprecation_date": null + } +}, +{ + "model": "content.topic", + "pk": "b8d8c9c2-e75b-4d49-aca7-c22cfb1f8b77", + "fields": { + "name": "Housing", + "active": true, + "description": "Housing", + "creation_date": "2024-09-21T20:22:46.519Z", + "last_updated": "2024-09-21T20:22:46.519Z", + "deprecation_date": null + } +}, +{ + "model": "content.topic", + "pk": "d13860c9-4467-4810-a2d5-7826f3769913", + "fields": { + "name": "Education", + "active": true, + "description": "Education", + "creation_date": "2024-09-21T20:22:23.446Z", + "last_updated": "2024-09-21T20:22:23.446Z", + "deprecation_date": null + } +}, +{ + "model": "content.topic", + "pk": "e3715e45-153d-4cce-b957-d68f5a6d4256", + "fields": { + "name": "Climate", + "active": true, + "description": "Climate", + "creation_date": "2024-09-21T20:20:33.450Z", + "last_updated": "2024-09-21T20:20:33.450Z", + "deprecation_date": null + } +}, +{ + "model": "content.topic", + "pk": "ec635e18-a89c-46d3-9a14-8f84be96af9b", + "fields": { + "name": "LGTBQIA+", + "active": true, + "description": "LGTBQIA+", + "creation_date": "2024-09-21T20:23:58.389Z", + "last_updated": "2024-09-21T20:23:58.389Z", + "deprecation_date": null + } +} +] diff --git a/backend/utils/models.py b/backend/utils/models.py new file mode 100644 index 000000000..e21207136 --- /dev/null +++ b/backend/utils/models.py @@ -0,0 +1,7 @@ +ISO_CHOICES = [ + ("de", "de"), + ("en", "en"), + ("es", "es"), + ("fr", "fr"), + ("pt", "pt"), +] diff --git a/backend/utils/utils.py b/backend/utils/utils.py index a73262c96..f757b2145 100644 --- a/backend/utils/utils.py +++ b/backend/utils/utils.py @@ -12,7 +12,7 @@ def validate_creation_and_deletion_dates(data: Any) -> None: code="invalid_creation_date", ) - if data["creation_date"] < data["deletion_date"]: + if data.get("deletion_date") and data.get("deletion_date") < data["creation_date"]: raise serializers.ValidationError( _("The field deletion_date cannot be before creation_date."), code="invalid_date_order", @@ -20,31 +20,11 @@ def validate_creation_and_deletion_dates(data: Any) -> None: def validate_creation_and_deprecation_dates(data: Any) -> None: - if data["deprecation_date"] < data["creation_date"]: + if ( + data.get("deprecation_date") + and data.get("deprecation_date") < data["creation_date"] + ): raise serializers.ValidationError( _("The field deprecation_date cannot be before creation_date."), code="invalid_date_order", ) - - -def validate_flags_number(data: Any) -> None: - if int(data["total_flags"]) < 0: - raise serializers.ValidationError( - _("The field total_flags cannot be negative."), - code="negative_total_flags", - ) - - -def validate_empty(value: Any, field_name: Any) -> None: - if value == "" or value is None: - raise serializers.ValidationError( - _(f"The field {field_name} has to be filled."), code="empty_field" - ) - - -def validate_object_existence(model: Any, object_id: Any) -> None: - if model.objects.filter(id=object_id).exists(): - raise serializers.ValidationError( - _(f"There is no {model.__name__} object with id {object_id}."), - code="inexistent_object", - ) diff --git a/docker-compose.yml b/docker-compose.yml index cb53c28a4..9873891e0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,8 @@ services: python manage.py migrate && python manage.py loaddata fixtures/superuser.json && python manage.py loaddata fixtures/status_types.json && - python manage.py populate_db --users 10 --opu 1 --gpo 1 --epo 1 && + python manage.py loaddata fixtures/topics.json && + python manage.py populate_db --users 10 --orgs-per-user 1 --groups-per-org 1 --events-per-org 1 && python manage.py runserver 0.0.0.0:${BACKEND_PORT}" ports: - "${BACKEND_PORT}:${BACKEND_PORT}" diff --git a/frontend/components/card/about/CardAboutGroup.vue b/frontend/components/card/about/CardAboutGroup.vue index 5cb356e88..12e1de7ff 100644 --- a/frontend/components/card/about/CardAboutGroup.vue +++ b/frontend/components/card/about/CardAboutGroup.vue @@ -91,7 +91,10 @@ const props = defineProps<{ const res = await useAsyncData( async () => - await fetchWithToken(`/entities/group_texts?group_id=${props.group.id}`, {}) + await fetchWithOptionalToken( + `/entities/group_texts?group_id=${props.group.id}`, + {} + ) ); const groupTexts = res.data as unknown as GroupText[]; diff --git a/frontend/components/card/search-result/CardSearchResult.vue b/frontend/components/card/search-result/CardSearchResult.vue index 3e7dc83cf..c65ae89d4 100644 --- a/frontend/components/card/search-result/CardSearchResult.vue +++ b/frontend/components/card/search-result/CardSearchResult.vue @@ -210,11 +210,11 @@ -
{{ description }} -
+

diff --git a/frontend/composables/fetch.ts b/frontend/composables/fetch.ts index cb895551a..238be4195 100644 --- a/frontend/composables/fetch.ts +++ b/frontend/composables/fetch.ts @@ -1,15 +1,30 @@ -export const fetchWithToken = async ( +/** + * Returns data given the authentication status of the user. + * @param url Backend URL to make the request to. + * @param data Data to be returned. + * @returns The resulting data from the table. + */ +export const fetchWithOptionalToken = async ( url: string, data: object | {} | undefined ) => { const token = localStorage.getItem("accessToken"); - const res = await $fetch.raw(BASE_BACKEND_URL + url, { - data, - headers: { - Authorization: `Token ${token}`, - }, - }); + if (token) { + const res = await $fetch.raw(BASE_BACKEND_URL + url, { + data, + headers: { + Authorization: `Token ${token}`, + }, + }); - return res._data; + return res._data; + } else { + const res = await $fetch.raw(BASE_BACKEND_URL + url, { + data, + headers: {}, + }); + + return res._data; + } }; diff --git a/frontend/pages/organizations/[id]/groups/[id]/about.vue b/frontend/pages/organizations/[id]/groups/[id]/about.vue index 81dbfc9c3..a8b5c667c 100644 --- a/frontend/pages/organizations/[id]/groups/[id]/about.vue +++ b/frontend/pages/organizations/[id]/groups/[id]/about.vue @@ -97,9 +97,12 @@ const aboveLargeBP = useBreakpoint("lg"); const { id } = useRoute().params; const [resOrg, resOrgTexts] = await Promise.all([ - useAsyncData(async () => await fetchWithToken(`/entities/groups/${id}`, {})), useAsyncData( - async () => await fetchWithToken(`/entities/group_texts?org_id=${id}`, {}) + async () => await fetchWithOptionalToken(`/entities/groups/${id}`, {}) + ), + useAsyncData( + async () => + await fetchWithOptionalToken(`/entities/group_texts?org_id=${id}`, {}) ), ]); diff --git a/frontend/stores/event.ts b/frontend/stores/event.ts index 5ca47ef40..ce384537f 100644 --- a/frontend/stores/event.ts +++ b/frontend/stores/event.ts @@ -49,25 +49,28 @@ export const useEventStore = defineStore("event", { const [resEvent, resEventTexts] = await Promise.all([ useAsyncData( - async () => await fetchWithToken(`/entities/events/${id}`, {}) + async () => await fetchWithOptionalToken(`/entities/events/${id}`, {}) ), // useAsyncData( // async () => - // await fetchWithToken( + // await fetchWithOptionalToken( // `/entities/event_faq?event_id=${id}`, // {} // ) // ), // useAsyncData( // async () => - // await fetchWithToken( + // await fetchWithOptionalToken( // `/entities/event_resources?event_id=${id}`, // {} // ) // ), useAsyncData( async () => - await fetchWithToken(`/entities/event_texts?event_id=${id}`, {}) + await fetchWithOptionalToken( + `/entities/event_texts?event_id=${id}`, + {} + ) ), ]); diff --git a/frontend/stores/group.ts b/frontend/stores/group.ts index 081ba8bfb..74e312dbb 100644 --- a/frontend/stores/group.ts +++ b/frontend/stores/group.ts @@ -34,29 +34,35 @@ export const useGroupStore = defineStore("group", { const [resGroup, resGroupOrg, resGroupTexts] = await Promise.all([ useAsyncData( - async () => await fetchWithToken(`/entities/groups/${id}`, {}) + async () => await fetchWithOptionalToken(`/entities/groups/${id}`, {}) ), useAsyncData( async () => - await fetchWithToken(`/entities/organizations?group_id=${id}`, {}) + await fetchWithOptionalToken( + `/entities/organizations?group_id=${id}`, + {} + ) ), // useAsyncData( // async () => - // await fetchWithToken( + // await fetchWithOptionalToken( // `/entities/group_faq?group_id=${id}`, // {} // ) // ), // useAsyncData( // async () => - // await fetchWithToken( + // await fetchWithOptionalToken( // `/entities/group_resources?group_id=${id}`, // {} // ) // ), useAsyncData( async () => - await fetchWithToken(`/entities/group_texts?group_id=${id}`, {}) + await fetchWithOptionalToken( + `/entities/group_texts?group_id=${id}`, + {} + ) ), ]); diff --git a/frontend/stores/organization.ts b/frontend/stores/organization.ts index 1dbdbd9d9..a7a23a67f 100644 --- a/frontend/stores/organization.ts +++ b/frontend/stores/organization.ts @@ -89,28 +89,29 @@ export const useOrganizationStore = defineStore("organization", { const [responseOrg, responseOrgTexts] = await Promise.all([ useAsyncData( - async () => await fetchWithToken(`/entities/organizations/${id}`, {}) + async () => + await fetchWithOptionalToken(`/entities/organizations/${id}`, {}) ), // useAsyncData( // async () => - // await fetchWithToken( + // await fetchWithOptionalToken( // `/entities/organization_faq?org_id=${id}`, // {} // ) // ), // useAsyncData( - // async () => await fetchWithToken(`/entities/groups?org_id=${id}`, {}) + // async () => await fetchWithOptionalToken(`/entities/groups?org_id=${id}`, {}) // ), // useAsyncData( // async () => - // await fetchWithToken( + // await fetchWithOptionalToken( // `/entities/organization_resources?org_id=${id}`, // {} // ) // ), useAsyncData( async () => - await fetchWithToken( + await fetchWithOptionalToken( `/entities/organization_texts?org_id=${id}`, {} ) @@ -156,7 +157,8 @@ export const useOrganizationStore = defineStore("organization", { const [responseOrgs] = await Promise.all([ useAsyncData( - async () => await fetchWithToken(`/entities/organizations/`, {}) + async () => + await fetchWithOptionalToken(`/entities/organizations/`, {}) ), ]); @@ -167,7 +169,7 @@ export const useOrganizationStore = defineStore("organization", { orgs._value.map((org) => useAsyncData( async () => - await fetchWithToken( + await fetchWithOptionalToken( `/entities/organization_texts?org_id=${org.id}`, {} ) From bab73c247cb17d692f33417ac591b943246d0f19 Mon Sep 17 00:00:00 2001 From: tosta Date: Sun, 22 Sep 2024 21:43:57 +0200 Subject: [PATCH 2/7] fixed mypy errors inside populate_db --- backend/backend/management/commands/populate_db.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/backend/management/commands/populate_db.py b/backend/backend/management/commands/populate_db.py index 09d376ddd..641f0b861 100644 --- a/backend/backend/management/commands/populate_db.py +++ b/backend/backend/management/commands/populate_db.py @@ -35,10 +35,10 @@ def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument("--events-per-org", type=int, default=1) def handle(self, *args: str, **options: Unpack[Options]) -> None: - num_users = options.get("users") - num_orgs_per_user = options.get("orgs_per_user") - num_groups_per_org = options.get("groups_per_org") - num_events_per_org = options.get("events_per_org") + num_users = options["users"] + num_orgs_per_user = options["orgs_per_user"] + num_groups_per_org = options["groups_per_org"] + num_events_per_org = options["events_per_org"] # Clear all tables before creating new data. UserModel.objects.exclude(username="admin").delete() @@ -64,7 +64,7 @@ def handle(self, *args: str, **options: Unpack[Options]) -> None: created_by=user, ) - OrganizationTextFactory(org_id=user_org, iso="en", primary=True) + OrganizationTextFactory(org_id=user_org, iso="wt", primary=True) for g in range(num_groups_per_org): user_org_group = GroupFactory( From df0438194767f050e6cb33c724f0759137dfd81d Mon Sep 17 00:00:00 2001 From: Andrew Tavis McAllister Date: Mon, 23 Sep 2024 08:59:14 +0200 Subject: [PATCH 3/7] #938 Add dev mode and splash to ease access to orgs and events --- CONTRIBUTING.md | 2 +- README.md | 2 +- frontend/components/FriendlyCaptcha.vue | 5 +- .../card/search-result/CardSearchResult.vue | 2 +- frontend/components/header/HeaderWebsite.vue | 47 ++++++++++++++++++- frontend/components/landing/LandingSplash.vue | 22 +++++++++ frontend/i18n/en-US.json | 4 ++ frontend/stores/dev.ts | 14 ++++++ frontend/stores/modals.ts | 2 +- 9 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 frontend/stores/dev.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7c977ce57..f239e9c76 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -200,7 +200,7 @@ git remote add upstream https://github.com/activist-org/activist.git # docker compose --env-file .env.dev down ``` -5. You can visit to see the development build once the container is up and running. +5. You can visit to see the development build once the container is up and running. From there click `View organizations` or `View events` to explore the platform. > [!NOTE] > Feel free to contact the team in the [Development room on Matrix](https://matrix.to/#/!CRgLpGeOBNwxYCtqmK:matrix.org?via=matrix.org&via=acter.global&via=chat.0x7cd.xyz) if you're having problems getting your environment setup! diff --git a/README.md b/README.md index d28c5c081..a6ec8fcd5 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,7 @@ git remote add upstream https://github.com/activist-org/activist.git # docker compose --env-file .env.dev down ``` -6. You can then visit to see the development frontend build once the container is up and running. +6. You can then visit to see the development frontend build once the container is up and running. From there click `View organizations` or `View events` to explore the platform. > [!NOTE] > Feel free to contact the team in the [Development room on Matrix](https://matrix.to/#/!CRgLpGeOBNwxYCtqmK:matrix.org?via=matrix.org&via=acter.global&via=chat.0x7cd.xyz) if you're having problems getting your environment setup! If you're having issues with Docker and just want to get the frontend or backend up and running, please see [the section on this in the contributing guide](https://github.com/activist-org/activist/blob/main/CONTRIBUTING.md#using-yarn-or-python). diff --git a/frontend/components/FriendlyCaptcha.vue b/frontend/components/FriendlyCaptcha.vue index 1130246b8..1d528d7bb 100644 --- a/frontend/components/FriendlyCaptcha.vue +++ b/frontend/components/FriendlyCaptcha.vue @@ -7,7 +7,7 @@ }" > { console.log("Captcha response:", response); diff --git a/frontend/components/card/search-result/CardSearchResult.vue b/frontend/components/card/search-result/CardSearchResult.vue index c65ae89d4..73b757805 100644 --- a/frontend/components/card/search-result/CardSearchResult.vue +++ b/frontend/components/card/search-result/CardSearchResult.vue @@ -211,7 +211,7 @@ -->

{{ description }}

diff --git a/frontend/components/header/HeaderWebsite.vue b/frontend/components/header/HeaderWebsite.vue index 928d680c5..46b0a9381 100644 --- a/frontend/components/header/HeaderWebsite.vue +++ b/frontend/components/header/HeaderWebsite.vue @@ -52,7 +52,47 @@ + + + +
+ + diff --git a/frontend/i18n/en-US.json b/frontend/i18n/en-US.json index 098141e2e..59bf2a1b5 100644 --- a/frontend/i18n/en-US.json +++ b/frontend/i18n/en-US.json @@ -248,6 +248,10 @@ "components.landing_splash.message_1": "A platform for growing our movements and organizing actions.", "components.landing_splash.message_2": "Free, open-source, privacy-focused and governed by our community.", "components.landing_splash.request_access_aria_label": "Request access to activist.org", + "components.landing_splash.view_events": "View events", + "components.landing_splash.view_events_aria_label": "View the events section of the activist platform", + "components.landing_splash.view_organizations": "View organizations", + "components.landing_splash.view_organizations_aria_label": "View the organizations section of the activist platform", "components.landing_tech_banner.open_header": "Open", "components.landing_tech_banner.open_source_tagline": "Our code and processes", "components.landing_tech_banner.open_source_text": "We're dedicated to working in the open to build trust with our partner organizations and fellow activists. All who want to help us build are welcome!", diff --git a/frontend/stores/dev.ts b/frontend/stores/dev.ts new file mode 100644 index 000000000..a7ac6d6d8 --- /dev/null +++ b/frontend/stores/dev.ts @@ -0,0 +1,14 @@ +import { useLocalStorage } from "@vueuse/core"; +import { defineStore } from "pinia"; + +export const useDevMode = defineStore("devMode", { + state: () => ({ + active: useLocalStorage("active", false), + }), + + actions: { + check() { + this.active = window.location.href.includes("localhost:3000"); + }, + }, +}); diff --git a/frontend/stores/modals.ts b/frontend/stores/modals.ts index d47fe7f22..d5578ae6d 100644 --- a/frontend/stores/modals.ts +++ b/frontend/stores/modals.ts @@ -11,7 +11,7 @@ export const useModals = defineStore("modals", { actions: { openModal(modalName: string) { - const modals = this.modals; + const { modals } = this; for (const key in modals) { modals[key].isOpen = false; } From 0c0aae51052e6e3a6a1f57fffb2d8608b86a9c86 Mon Sep 17 00:00:00 2001 From: Andrew Tavis McAllister Date: Wed, 25 Sep 2024 01:13:22 +0200 Subject: [PATCH 4/7] Expand event models+ to allow for list and create store --- .../management/commands/populate_db.py | 7 +- backend/backend/settings.py | 2 +- backend/entities/serializers.py | 1 + backend/entities/views.py | 7 ++ backend/events/factories.py | 4 +- backend/events/models.py | 4 + backend/events/serializers.py | 53 ++++++++++-- backend/events/views.py | 76 +++++++++++++++- frontend/assets/css/tailwind.css | 4 + .../components/landing/LandingContent.vue | 4 +- frontend/components/landing/LandingSplash.vue | 38 ++++---- .../components/page/PageCommunityFooter.vue | 4 +- frontend/pages/events/index.vue | 17 ++-- frontend/stores/event.ts | 86 +++++++++++++++++-- frontend/stores/organization.ts | 11 ++- frontend/types/entities/organization.d.ts | 2 +- frontend/types/events/event.d.ts | 45 +++++++++- frontend/utils/testEntities.ts | 6 +- 18 files changed, 306 insertions(+), 65 deletions(-) diff --git a/backend/backend/management/commands/populate_db.py b/backend/backend/management/commands/populate_db.py index 641f0b861..00d373e0e 100644 --- a/backend/backend/management/commands/populate_db.py +++ b/backend/backend/management/commands/populate_db.py @@ -60,7 +60,7 @@ def handle(self, *args: str, **options: Unpack[Options]) -> None: for o in range(num_orgs_per_user): user_org = OrganizationFactory( - name=f"{user_topic.name} Organization (u: {u} o: {o})", + name=f"{user_topic.name} Organization (U{u}-O{o})", created_by=user, ) @@ -69,7 +69,7 @@ def handle(self, *args: str, **options: Unpack[Options]) -> None: for g in range(num_groups_per_org): user_org_group = GroupFactory( org_id=user_org, - name=f"{user_topic.name} Group (u: {u} o: {o} g: {g})", + name=f"{user_topic.name} Group (U{u}-O{o}-G{g})", created_by=user, ) @@ -79,7 +79,8 @@ def handle(self, *args: str, **options: Unpack[Options]) -> None: for e in range(num_events_per_org): user_org_event = EventFactory( - name=f"{user_topic.name} Event (u: {u} o: {o} e: {e})", + name=f"{user_topic.name} Event (U{u}-O{o}-E{e})", + type=random.choice(["learn", "action"]), created_by=user, ) diff --git a/backend/backend/settings.py b/backend/backend/settings.py index e4d73f852..94c45627a 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -174,7 +174,7 @@ "rest_framework.throttling.AnonRateThrottle", "rest_framework.throttling.UserRateThrottle", ], - "DEFAULT_THROTTLE_RATES": {"anon": "20/min", "user": "30/min"}, + "DEFAULT_THROTTLE_RATES": {"anon": "40/min", "user": "60/min"}, "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "DEFAULT_PAGINATION_ORDERS_OBJECTS": False, diff --git a/backend/entities/serializers.py b/backend/entities/serializers.py index fb8efea4a..7f2ba3097 100644 --- a/backend/entities/serializers.py +++ b/backend/entities/serializers.py @@ -85,6 +85,7 @@ def validate(self, data: dict[str, Any]) -> dict[str, Any]: def create(self, validated_data: dict[str, Any]) -> Organization: description = validated_data.pop("description", None) org = Organization.objects.create(**validated_data) + if org and description: org_text = OrganizationText.objects.create( org_id=org, description=description diff --git a/backend/entities/views.py b/backend/entities/views.py index aab425400..5dec122df 100644 --- a/backend/entities/views.py +++ b/backend/entities/views.py @@ -60,6 +60,7 @@ class GroupViewSet(viewsets.ModelViewSet[Group]): def list(self, request: Request, *args: str, **kwargs: int) -> Response: serializer = self.get_serializer(self.queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) def create(self, request: Request) -> Response: @@ -67,6 +68,7 @@ def create(self, request: Request) -> Response: serializer.is_valid(raise_exception=True) serializer.save(created_by=request.user) data = {"message": f"New Group created: {serializer.data}"} + return Response(data, status=status.HTTP_201_CREATED) def retrieve(self, request: Request, *args: str, **kwargs: int) -> Response: @@ -82,6 +84,7 @@ def partial_update(self, request: Request, *args: str, **kwargs: int) -> Respons return Response( {"error": "Group not found"}, status=status.HTTP_404_NOT_FOUND ) + if request.user != group.created_by: return Response( {"error": "You are not authorized to update this group"}, @@ -91,6 +94,7 @@ def partial_update(self, request: Request, *args: str, **kwargs: int) -> Respons serializer = self.get_serializer(group, data=request.data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) def destroy(self, request: Request, *args: str, **kwargs: int) -> Response: @@ -100,11 +104,13 @@ def destroy(self, request: Request, *args: str, **kwargs: int) -> Response: return Response( {"error": "Group not found"}, status=status.HTTP_404_NOT_FOUND ) + if request.user != group.created_by: return Response( {"error": "You are not authorized to delete this group"}, status.HTTP_401_UNAUTHORIZED, ) + group.delete() return Response( {"message": "Group deleted successfully"}, status=status.HTTP_200_OK @@ -130,6 +136,7 @@ def create(self, request: Request) -> Response: def retrieve(self, request: Request, pk: str | None = None) -> Response: if org := self.queryset.filter(id=pk).first(): serializer = self.get_serializer(org) + return Response(serializer.data, status=status.HTTP_200_OK) return Response({"error": "Organization not found"}, status.HTTP_404_NOT_FOUND) diff --git a/backend/events/factories.py b/backend/events/factories.py index 1a8d961f4..f89b20e9f 100644 --- a/backend/events/factories.py +++ b/backend/events/factories.py @@ -23,11 +23,13 @@ class EventFactory(factory.django.DjangoModelFactory): class Meta: model = Event + django_get_or_create = ("created_by",) name = factory.Faker("word") tagline = factory.Faker("word") - type = factory.Faker("word") + type = random.choice(["learn", "action"]) online_location_link = factory.Faker("url") + offline_location = factory.Faker("city") offline_location_lat = factory.Faker("latitude") offline_location_long = factory.Faker("longitude") start_time = factory.LazyFunction( diff --git a/backend/events/models.py b/backend/events/models.py index 208ed0a4b..5a9ea051b 100644 --- a/backend/events/models.py +++ b/backend/events/models.py @@ -27,6 +27,7 @@ class Event(CreationDeletionMixin): ) type = models.CharField(max_length=255) online_location_link = models.CharField(max_length=255, blank=True) + offline_location = models.CharField(max_length=255, blank=True) offline_location_lat = models.FloatField(null=True, blank=True) offline_location_long = models.FloatField(null=True, blank=True) get_involved_url = models.URLField(blank=True) @@ -36,6 +37,9 @@ class Event(CreationDeletionMixin): is_private = models.BooleanField(default=False) start_time = models.DateTimeField() end_time = models.DateTimeField() + event_text = models.ForeignKey( + "EventText", on_delete=models.CASCADE, null=True, blank=True + ) def __str__(self) -> str: return self.name diff --git a/backend/events/serializers.py b/backend/events/serializers.py index 144c1f59a..50ac43e13 100644 --- a/backend/events/serializers.py +++ b/backend/events/serializers.py @@ -2,7 +2,7 @@ Serializers for the events app. """ -from typing import Dict, Union +from typing import Any, Dict, Union from django.utils.dateparse import parse_datetime from django.utils.translation import gettext as _ @@ -31,10 +31,43 @@ # MARK: Main Tables +class EventTextSerializer(serializers.ModelSerializer[EventText]): + class Meta: + model = EventText + fields = "__all__" + + class EventSerializer(serializers.ModelSerializer[Event]): + event_text = EventTextSerializer(read_only=True) + description = serializers.CharField(write_only=True, required=False) + class Meta: model = Event - fields = "__all__" + + extra_kwargs = { + "created_by": {"read_only": True}, + "social_links": {"required": False}, + "description": {"write_only": True}, + } + + fields = [ + "id", + "name", + "tagline", + "icon_url", + "type", + "online_location_link", + "offline_location", + "offline_location_lat", + "offline_location_long", + "created_by", + "social_links", + "is_private", + "start_time", + "end_time", + "event_text", + "description", + ] def validate(self, data: Dict[str, Union[str, int]]) -> Dict[str, Union[str, int]]: if parse_datetime(data["start_time"]) > parse_datetime(data["end_time"]): # type: ignore @@ -47,6 +80,16 @@ def validate(self, data: Dict[str, Union[str, int]]) -> Dict[str, Union[str, int return data + def create(self, validated_data: dict[str, Any]) -> Event: + description = validated_data.pop("description", None) + event = Event.objects.create(**validated_data) + + if event and description: + event_text = Event.objects.create(event_id=event, description=description) + event.event_text = event_text + + return event + class FormatSerializer(serializers.ModelSerializer[Event]): class Meta: @@ -115,12 +158,6 @@ class Meta: fields = "__all__" -class EventTextSerializer(serializers.ModelSerializer[EventText]): - class Meta: - model = EventText - fields = "__all__" - - class EventTopicSerializer(serializers.ModelSerializer[EventTopic]): class Meta: model = EventTopic diff --git a/backend/events/views.py b/backend/events/views.py index d1de501d4..fcd73c437 100644 --- a/backend/events/views.py +++ b/backend/events/views.py @@ -1,4 +1,8 @@ -from rest_framework import viewsets +from rest_framework import status, viewsets +from rest_framework.authentication import TokenAuthentication +from rest_framework.permissions import IsAuthenticatedOrReadOnly +from rest_framework.request import Request +from rest_framework.response import Response from rest_framework.throttling import ( AnonRateThrottle, UserRateThrottle, @@ -41,6 +45,76 @@ class EventViewSet(viewsets.ModelViewSet[Event]): serializer_class = EventSerializer pagination_class = CustomPagination throttle_classes = [AnonRateThrottle, UserRateThrottle] + permission_classes = [IsAuthenticatedOrReadOnly] + authentication_classes = [TokenAuthentication] + + def create(self, request: Request) -> Response: + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def retrieve(self, request: Request, pk: str | None = None) -> Response: + if event := self.queryset.filter(id=pk).first(): + serializer = self.get_serializer(event) + + return Response(serializer.data, status=status.HTTP_200_OK) + + return Response({"error": "Event not found"}, status.HTTP_404_NOT_FOUND) + + def list(self, request: Request) -> Response: + serializer = self.get_serializer(self.get_queryset(), many=True) + + return Response(serializer.data, status=status.HTTP_200_OK) + + def update(self, request: Request, pk: str | None = None) -> Response: + event = self.queryset.filter(id=pk).first() + if event is None: + return Response({"error": "Event not found"}, status.HTTP_404_NOT_FOUND) + + if request.user != event.created_by: + return Response( + {"error": "You are not authorized to update this event"}, + status.HTTP_401_UNAUTHORIZED, + ) + + serializer = self.get_serializer(event, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response(serializer.data, status.HTTP_200_OK) + + def partial_update(self, request: Request, pk: str | None = None) -> Response: + event = self.queryset.filter(id=pk).first() + if event is None: + return Response({"error": "Event not found"}, status.HTTP_404_NOT_FOUND) + + if request.user != event.created_by: + return Response( + {"error": "You are not authorized to update this event"}, + status.HTTP_401_UNAUTHORIZED, + ) + + serializer = self.get_serializer(event, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response(serializer.data, status.HTTP_200_OK) + + def destroy(self, request: Request, pk: str | None = None) -> Response: + event = self.queryset.filter(id=pk).first() + if event is None: + return Response({"error": "Event not found"}, status.HTTP_404_NOT_FOUND) + + if request.user != event.created_by: + return Response( + {"error": "You are not authorized to delete this event"}, + status.HTTP_401_UNAUTHORIZED, + ) + + event.save() + + return Response({"message": "Event deleted successfully"}, status.HTTP_200_OK) class FormatViewSet(viewsets.ModelViewSet[Format]): diff --git a/frontend/assets/css/tailwind.css b/frontend/assets/css/tailwind.css index 76aa6984a..20319c392 100644 --- a/frontend/assets/css/tailwind.css +++ b/frontend/assets/css/tailwind.css @@ -131,6 +131,10 @@ @apply focus-brand elem-shadow-sm bg-light-layer-0 text-light-text hover:bg-light-highlight active:bg-light-layer-0 dark:border dark:border-dark-text dark:bg-dark-layer-0 dark:text-dark-text dark:hover:bg-dark-highlight dark:active:bg-dark-layer-0; } + .style-btns-next-to-one-another { + @apply mx-auto grid max-w-[70%] grid-cols-1 gap-y-4 sm:mx-0 sm:max-w-[90%] sm:grid-cols-2 sm:grid-rows-1 sm:gap-x-4 sm:gap-y-0 md:max-w-[70%] md:gap-x-6 lg:max-w-[60%] xl:max-w-[70%] xl:gap-x-8 2xl:max-w-[80%]; + } + .style-cta { @apply focus-brand border border-light-text bg-light-cta-orange fill-light-text text-light-text hover:bg-light-cta-orange/80 active:bg-light-cta-orange dark:border-dark-cta-orange dark:bg-dark-cta-orange/10 dark:fill-dark-cta-orange dark:text-dark-cta-orange dark:hover:bg-dark-cta-orange/25 dark:active:bg-dark-cta-orange/10; } diff --git a/frontend/components/landing/LandingContent.vue b/frontend/components/landing/LandingContent.vue index e3606a130..e7e5243e0 100644 --- a/frontend/components/landing/LandingContent.vue +++ b/frontend/components/landing/LandingContent.vue @@ -135,9 +135,7 @@ > {{ $t(text) }}

-
+
- - +
+ + +
-
+
diff --git a/frontend/pages/events/index.vue b/frontend/pages/events/index.vue index a70d5c430..810315598 100644 --- a/frontend/pages/events/index.vue +++ b/frontend/pages/events/index.vue @@ -13,23 +13,18 @@
- - +
diff --git a/frontend/stores/event.ts b/frontend/stores/event.ts index ce384537f..0ec3ff0b1 100644 --- a/frontend/stores/event.ts +++ b/frontend/stores/event.ts @@ -1,7 +1,10 @@ import type { Event, + EventText, PiniaResEvent, + PiniaResEvents, PiniaResEventText, + PiniaResEventTexts, } from "~/types/events/event"; interface EventStore { @@ -23,18 +26,23 @@ export const useEventStore = defineStore("event", { createdBy: "", iconURL: "", type: "learn", + onlineLocationLink: "", offlineLocation: "", + offlineLocationLat: "", + offlineLocationLong: "", getInvolvedURL: "", socialLinks: [""], startTime: "", + endTime: "", + creationDate: "", - // event_organizations organizations: [], - // event_text + eventTextID: "", description: "", getInvolved: "", }, + events: [], }), actions: { @@ -49,7 +57,7 @@ export const useEventStore = defineStore("event", { const [resEvent, resEventTexts] = await Promise.all([ useAsyncData( - async () => await fetchWithOptionalToken(`/entities/events/${id}`, {}) + async () => await fetchWithOptionalToken(`/events/events/${id}`, {}) ), // useAsyncData( // async () => @@ -68,7 +76,7 @@ export const useEventStore = defineStore("event", { useAsyncData( async () => await fetchWithOptionalToken( - `/entities/event_texts?event_id=${id}`, + `/events/event_texts?event_id=${id}`, {} ) ), @@ -101,7 +109,69 @@ export const useEventStore = defineStore("event", { // MARK: Fetch All - async fetchAll() {}, + async fetchAll() { + this.loading = true; + + const [responseEvents] = await Promise.all([ + useAsyncData( + async () => await fetchWithOptionalToken(`/events/events/`, {}) + ), + ]); + + const events = responseEvents.data as unknown as PiniaResEvents; + + console.log(`Here: ${JSON.stringify(events._value)}`); + + if (events._value) { + const responseEventTexts = (await Promise.all( + events._value.map((event) => + useAsyncData( + async () => + await fetchWithOptionalToken( + `/events/event_texts?event_id=${event.id}`, + {} + ) + ) + ) + )) as unknown as PiniaResEventTexts[]; + + const eventTextsData = responseEventTexts.map( + (text) => text.data._value.results[0] + ) as unknown as EventText[]; + + const eventsWithTexts = events._value.map( + (event: Event, index: number) => { + const texts = eventTextsData[index]; + return { + id: event.id, + name: event.name, + tagline: event.tagline, + createdBy: event.createdBy, + iconURL: event.iconURL, + type: event.type, + onlineLocationLink: event.onlineLocationLink, + offlineLocation: event.offlineLocation, + offlineLocationLat: event.offlineLocationLat, + offlineLocationLong: event.offlineLocationLong, + getInvolvedURL: event.getInvolvedURL, + socialLinks: event.socialLinks, + startTime: event.startTime, + endTime: event.endTime, + creationDate: event.creationDate, + organizations: event.organizations, + + eventTextID: event.eventTextID, + description: texts.description, + getInvolved: texts.getInvolved, + }; + } + ); + + this.events = eventsWithTexts; + } + + this.loading = false; + }, // MARK: Update @@ -109,6 +179,10 @@ export const useEventStore = defineStore("event", { // MARK: Delete - async delete() {}, + async delete(id: string | undefined) { + this.loading = true; + + this.loading = false; + }, }, }); diff --git a/frontend/stores/organization.ts b/frontend/stores/organization.ts index a7a23a67f..b0e9620ea 100644 --- a/frontend/stores/organization.ts +++ b/frontend/stores/organization.ts @@ -33,7 +33,7 @@ export const useOrganizationStore = defineStore("organization", { status: 1, groups: [], - organization_text_id: "", + organizationTextID: "", description: "", getInvolved: "", donationPrompt: "", @@ -145,7 +145,7 @@ export const useOrganizationStore = defineStore("organization", { this.organization.description = texts.description; this.organization.getInvolved = texts.getInvolved; this.organization.donationPrompt = texts.donationPrompt; - this.organization.organization_text_id = texts.id; + this.organization.organizationTextID = texts.id; this.loading = false; }, @@ -181,8 +181,6 @@ export const useOrganizationStore = defineStore("organization", { (text) => text.data._value.results[0] ) as unknown as OrganizationText[]; - console.log(`Here: ${JSON.stringify(orgTextsData)}`); - const organizationsWithTexts = orgs._value.map( (organization: Organization, index: number) => { const texts = orgTextsData[index]; @@ -197,7 +195,8 @@ export const useOrganizationStore = defineStore("organization", { socialLinks: organization.socialLinks, status: organization.status, groups: organization.groups, - organization_text_id: organization.organization_text_id, + + organizationTextID: organization.organizationTextID, description: texts.description, getInvolved: texts.getInvolved, donationPrompt: texts.donationPrompt, @@ -237,7 +236,7 @@ export const useOrganizationStore = defineStore("organization", { const responseOrgTexts = await $fetch( BASE_BACKEND_URL + - `/entities/organization_texts/${org.organization_text_id}/`, + `/entities/organization_texts/${org.organizationTextID}/`, { method: "PUT", body: { diff --git a/frontend/types/entities/organization.d.ts b/frontend/types/entities/organization.d.ts index 83f0d2d1d..42380eeb5 100644 --- a/frontend/types/entities/organization.d.ts +++ b/frontend/types/entities/organization.d.ts @@ -36,7 +36,7 @@ export interface Organization { // organization_task // task?: Task[]; - organization_text_id: string; + organizationTextID: string; description: string; getInvolved: string; donationPrompt: string; diff --git a/frontend/types/events/event.d.ts b/frontend/types/events/event.d.ts index 5cec4c85a..2bb67e826 100644 --- a/frontend/types/events/event.d.ts +++ b/frontend/types/events/event.d.ts @@ -30,7 +30,7 @@ export interface Event { // event_task // task?: Task[]; - // event_text + eventTextID: string; description: string; getInvolved: string; @@ -100,6 +100,13 @@ export interface PiniaResEvent { _value: Event; } +export interface PiniaResEvents { + __v_isShallow: boolean; + __v_isRef: boolean; + _rawValue: Event[]; + _value: Event[]; +} + export interface PiniaResEventText { __v_isShallow: boolean; __v_isRef: boolean; @@ -116,3 +123,39 @@ export interface PiniaResEventText { results: EventText[]; }; } + +export interface PiniaResEventTexts { + data: { + __v_isShallow: boolean; + __v_isRef: boolean; + _rawValue: { + count: number; + next: null; + previous: null; + results: EventText[]; + }; + _value: { + count: number; + next: null; + previous: null; + results: EventText[]; + }; + }; + pending: { + __v_isShallow: boolean; + __v_isRef: boolean; + _rawValue: boolean; + _value: boolean; + }; + error: { + _object: { [$key: string]: null }; + _key: string; + __v_isRef: boolean; + }; + status: { + __v_isShallow: boolean; + __v_isRef: boolean; + _rawValue: string; + _value: string; + }; +} diff --git a/frontend/utils/testEntities.ts b/frontend/utils/testEntities.ts index 3d805dfac..4b5bcd49a 100644 --- a/frontend/utils/testEntities.ts +++ b/frontend/utils/testEntities.ts @@ -48,7 +48,7 @@ export const testClimateOrg: Organization = { groups: ["Fundraising", "Campaigning"], socialLinks: ["climate-org@mastodon", "climate-org@email"], iconURL: "URL/for/image", - organization_text_id: "06cb36a3-13c5-4518-b676-33ec734744ed", + organizationTextID: "06cb36a3-13c5-4518-b676-33ec734744ed", description: "Testing how organizations work", getInvolved: "Hey, get involved!", donationPrompt: "Hey thanks!", @@ -119,7 +119,7 @@ export const testTechOrg: Organization = { groups: [testTechGroup1, testTechGroup2], socialLinks: ["tfb@mastodon", "tfb@email"], // donationPrompt: "Hey thanks!", - organization_text_id: "06cb36a3-13c5-4518-b676-33ec734744ed", + organizationTextID: "06cb36a3-13c5-4518-b676-33ec734744ed", description: "Testing how organizations work", getInvolved: "Hey, get involved!", donationPrompt: "Hey thanks!", @@ -145,6 +145,7 @@ export const testClimateEvent: Event = { // supportingUsers: [user, user], // iconURL: "/images/an_image.svg", socialLinks: ["climate_org@mastodon", "climate_org@email.com"], + eventTextID: "", }; export const testTechEvent: Event = { @@ -162,6 +163,7 @@ export const testTechEvent: Event = { startTime: new Date().toLocaleDateString(), // supportingUsers: [user, user, user], socialLinks: [""], + eventTextID: "", }; export const testResource: Resource = { From ad335c9642dfd31f1e74b1d776f2ccc5553a230a Mon Sep 17 00:00:00 2001 From: Andrew Tavis McAllister Date: Wed, 25 Sep 2024 01:19:07 +0200 Subject: [PATCH 5/7] Correct Event as EventText in create serializer --- backend/events/serializers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/events/serializers.py b/backend/events/serializers.py index 50ac43e13..5ecbd1024 100644 --- a/backend/events/serializers.py +++ b/backend/events/serializers.py @@ -85,7 +85,9 @@ def create(self, validated_data: dict[str, Any]) -> Event: event = Event.objects.create(**validated_data) if event and description: - event_text = Event.objects.create(event_id=event, description=description) + event_text = EventText.objects.create( + event_id=event, description=description + ) event.event_text = event_text return event From 2ab649e89377fe9c9d4eda6df1e8c2afce390d66 Mon Sep 17 00:00:00 2001 From: Andrew Tavis McAllister Date: Wed, 25 Sep 2024 01:27:23 +0200 Subject: [PATCH 6/7] Fixes to event views --- backend/entities/views.py | 1 + backend/events/views.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/entities/views.py b/backend/entities/views.py index 5dec122df..38abc7f0d 100644 --- a/backend/entities/views.py +++ b/backend/entities/views.py @@ -112,6 +112,7 @@ def destroy(self, request: Request, *args: str, **kwargs: int) -> Response: ) group.delete() + return Response( {"message": "Group deleted successfully"}, status=status.HTTP_200_OK ) diff --git a/backend/events/views.py b/backend/events/views.py index fcd73c437..2ddf7378c 100644 --- a/backend/events/views.py +++ b/backend/events/views.py @@ -51,6 +51,8 @@ class EventViewSet(viewsets.ModelViewSet[Event]): def create(self, request: Request) -> Response: serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) + event = serializer.save(created_by=request.user) + Event.objects.create(id=event) return Response(serializer.data, status=status.HTTP_201_CREATED) @@ -112,7 +114,7 @@ def destroy(self, request: Request, pk: str | None = None) -> Response: status.HTTP_401_UNAUTHORIZED, ) - event.save() + event.delete() return Response({"message": "Event deleted successfully"}, status.HTTP_200_OK) From 37dc2596c34c36725b25f880a2f37c64f99e39cd Mon Sep 17 00:00:00 2001 From: Andrew Tavis McAllister Date: Wed, 25 Sep 2024 01:46:52 +0200 Subject: [PATCH 7/7] Update event views to incldue args and kwargs as required by mixins --- .../management/commands/populate_db.py | 6 ++-- backend/entities/views.py | 23 +++++++------ backend/events/views.py | 34 +++++++++---------- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/backend/backend/management/commands/populate_db.py b/backend/backend/management/commands/populate_db.py index 00d373e0e..e799d7a94 100644 --- a/backend/backend/management/commands/populate_db.py +++ b/backend/backend/management/commands/populate_db.py @@ -60,7 +60,7 @@ def handle(self, *args: str, **options: Unpack[Options]) -> None: for o in range(num_orgs_per_user): user_org = OrganizationFactory( - name=f"{user_topic.name} Organization (U{u}-O{o})", + name=f"{user_topic.name} Organization (U{u}:O{o})", created_by=user, ) @@ -69,7 +69,7 @@ def handle(self, *args: str, **options: Unpack[Options]) -> None: for g in range(num_groups_per_org): user_org_group = GroupFactory( org_id=user_org, - name=f"{user_topic.name} Group (U{u}-O{o}-G{g})", + name=f"{user_topic.name} Group (U{u}:O{o}:G{g})", created_by=user, ) @@ -79,7 +79,7 @@ def handle(self, *args: str, **options: Unpack[Options]) -> None: for e in range(num_events_per_org): user_org_event = EventFactory( - name=f"{user_topic.name} Event (U{u}-O{o}-E{e})", + name=f"{user_topic.name} Event (U{u}:O{o}:E{e})", type=random.choice(["learn", "action"]), created_by=user, ) diff --git a/backend/entities/views.py b/backend/entities/views.py index 38abc7f0d..a4b7f1852 100644 --- a/backend/entities/views.py +++ b/backend/entities/views.py @@ -67,15 +67,17 @@ def create(self, request: Request) -> Response: serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) serializer.save(created_by=request.user) - data = {"message": f"New Group created: {serializer.data}"} + data = {"message": f"New group created: {serializer.data}"} return Response(data, status=status.HTTP_201_CREATED) def retrieve(self, request: Request, *args: str, **kwargs: int) -> Response: - group = self.queryset.get(id=kwargs["pk"]) - serializer = self.get_serializer(group) + if group := self.queryset.get(id=kwargs["pk"]): + serializer = self.get_serializer(group) - return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.data, status=status.HTTP_200_OK) + + return Response({"error": "Group not found"}, status.HTTP_404_NOT_FOUND) def partial_update(self, request: Request, *args: str, **kwargs: int) -> Response: group = self.queryset.filter(id=kwargs["pk"]).first() @@ -126,13 +128,19 @@ class OrganizationViewSet(viewsets.ModelViewSet[Organization]): permission_classes = [IsAuthenticatedOrReadOnly] authentication_classes = [TokenAuthentication] + def list(self, request: Request) -> Response: + serializer = self.get_serializer(self.get_queryset(), many=True) + + return Response(serializer.data, status=status.HTTP_200_OK) + def create(self, request: Request) -> Response: serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) org = serializer.save(created_by=request.user) OrganizationApplication.objects.create(org_id=org) + data = {"message": f"New organization created: {serializer.data}"} - return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(data, status=status.HTTP_201_CREATED) def retrieve(self, request: Request, pk: str | None = None) -> Response: if org := self.queryset.filter(id=pk).first(): @@ -142,11 +150,6 @@ def retrieve(self, request: Request, pk: str | None = None) -> Response: return Response({"error": "Organization not found"}, status.HTTP_404_NOT_FOUND) - def list(self, request: Request) -> Response: - serializer = self.get_serializer(self.get_queryset(), many=True) - - return Response(serializer.data, status=status.HTTP_200_OK) - def update(self, request: Request, pk: str | None = None) -> Response: org = self.queryset.filter(id=pk).first() if org is None: diff --git a/backend/events/views.py b/backend/events/views.py index 2ddf7378c..1713a1c2b 100644 --- a/backend/events/views.py +++ b/backend/events/views.py @@ -48,29 +48,29 @@ class EventViewSet(viewsets.ModelViewSet[Event]): permission_classes = [IsAuthenticatedOrReadOnly] authentication_classes = [TokenAuthentication] - def create(self, request: Request) -> Response: + def list(self, request: Request, *args: str, **kwargs: int) -> Response: + serializer = self.get_serializer(self.get_queryset(), many=True) + + return Response(serializer.data, status=status.HTTP_200_OK) + + def create(self, request: Request, *args: str, **kwargs: int) -> Response: serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - event = serializer.save(created_by=request.user) - Event.objects.create(id=event) + serializer.save(created_by=request.user) + data = {"message": f"New event created: {serializer.data}"} - return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(data, status=status.HTTP_201_CREATED) - def retrieve(self, request: Request, pk: str | None = None) -> Response: - if event := self.queryset.filter(id=pk).first(): + def retrieve(self, request: Request, *args: str, **kwargs: int) -> Response: + if event := self.queryset.get(id=kwargs["pk"]): serializer = self.get_serializer(event) return Response(serializer.data, status=status.HTTP_200_OK) return Response({"error": "Event not found"}, status.HTTP_404_NOT_FOUND) - def list(self, request: Request) -> Response: - serializer = self.get_serializer(self.get_queryset(), many=True) - - return Response(serializer.data, status=status.HTTP_200_OK) - - def update(self, request: Request, pk: str | None = None) -> Response: - event = self.queryset.filter(id=pk).first() + def update(self, request: Request, *args: str, **kwargs: int) -> Response: + event = self.queryset.filter(id=kwargs["pk"]).first() if event is None: return Response({"error": "Event not found"}, status.HTTP_404_NOT_FOUND) @@ -86,8 +86,8 @@ def update(self, request: Request, pk: str | None = None) -> Response: return Response(serializer.data, status.HTTP_200_OK) - def partial_update(self, request: Request, pk: str | None = None) -> Response: - event = self.queryset.filter(id=pk).first() + def partial_update(self, request: Request, *args: str, **kwargs: int) -> Response: + event = self.queryset.filter(id=kwargs["pk"]).first() if event is None: return Response({"error": "Event not found"}, status.HTTP_404_NOT_FOUND) @@ -103,8 +103,8 @@ def partial_update(self, request: Request, pk: str | None = None) -> Response: return Response(serializer.data, status.HTTP_200_OK) - def destroy(self, request: Request, pk: str | None = None) -> Response: - event = self.queryset.filter(id=pk).first() + def destroy(self, request: Request, *args: str, **kwargs: int) -> Response: + event = self.queryset.filter(id=kwargs["pk"]).first() if event is None: return Response({"error": "Event not found"}, status.HTTP_404_NOT_FOUND)