diff --git a/backend/authentication/admin.py b/backend/authentication/admin.py index a157b8588..14eb6f68a 100644 --- a/backend/authentication/admin.py +++ b/backend/authentication/admin.py @@ -16,6 +16,21 @@ UserTopic, ) +# MARK: Main Tables + +# Remove default Group. +admin.site.unregister(Group) +admin.site.register(Support) + +# MARK: Bridge Tables + +admin.site.register(UserResource) +admin.site.register(UserTask) +admin.site.register(UserTopic) +admin.site.register(SupportEntityType) + +# MARK: Methods + class UserCreationForm(forms.ModelForm[UserModel]): """ @@ -115,12 +130,5 @@ class UserAdmin(BaseUserAdmin): filter_horizontal = [] -# Remove default Group -admin.site.unregister(Group) -admin.site.register(UserResource) -admin.site.register(UserTask) -admin.site.register(UserTopic) -admin.site.register(SupportEntityType) -admin.site.register(Support) # Now register the new UserAdmin... admin.site.register(UserModel, UserAdmin) diff --git a/backend/authentication/factories.py b/backend/authentication/factories.py index 2977a91cb..90fb83739 100644 --- a/backend/authentication/factories.py +++ b/backend/authentication/factories.py @@ -11,6 +11,8 @@ UserTopic, ) +# MARK: Main Tables + class SupportEntityTypeFactory(factory.django.DjangoModelFactory): class Meta: @@ -61,6 +63,9 @@ def verification_partner( pass +# MARK: Bridge Tables + + class UserResourceFactory(factory.django.DjangoModelFactory): class Meta: model = UserResource diff --git a/backend/authentication/models.py b/backend/authentication/models.py index ed449a604..fac5cf481 100644 --- a/backend/authentication/models.py +++ b/backend/authentication/models.py @@ -15,32 +15,7 @@ from django.contrib.postgres.fields import ArrayField from django.db import models - -class SupportEntityType(models.Model): - id = models.IntegerField(primary_key=True) - name = models.CharField(max_length=255) - - def __str__(self) -> str: - return self.name - - -class Support(models.Model): - supporter_type = models.ForeignKey( - "SupportEntityType", on_delete=models.CASCADE, related_name="supporter" - ) - supporter_entity = models.ForeignKey( - "entities.Organization", on_delete=models.CASCADE, related_name="supporter" - ) - supported_type = models.ForeignKey( - "SupportEntityType", on_delete=models.CASCADE, related_name="supported" - ) - supported_entity = models.ForeignKey( - "entities.Organization", on_delete=models.CASCADE, related_name="supported" - ) - creation_date = models.DateTimeField(auto_now_add=True) - - def __str__(self) -> str: - return f"{self.id}" +# MARK: Main Tables class CustomAccountManager(BaseUserManager["UserModel"]): @@ -78,6 +53,33 @@ def create_user( return user +class SupportEntityType(models.Model): + id = models.IntegerField(primary_key=True) + name = models.CharField(max_length=255) + + def __str__(self) -> str: + return self.name + + +class Support(models.Model): + supporter_type = models.ForeignKey( + "SupportEntityType", on_delete=models.CASCADE, related_name="supporter" + ) + supporter_entity = models.ForeignKey( + "entities.Organization", on_delete=models.CASCADE, related_name="supporter" + ) + supported_type = models.ForeignKey( + "SupportEntityType", on_delete=models.CASCADE, related_name="supported" + ) + supported_entity = models.ForeignKey( + "entities.Organization", on_delete=models.CASCADE, related_name="supported" + ) + creation_date = models.DateTimeField(auto_now_add=True) + + def __str__(self) -> str: + return f"{self.id}" + + class UserModel(AbstractUser, PermissionsMixin): id = models.UUIDField(primary_key=True, default=uuid4, editable=False) username = models.CharField(max_length=255, unique=True) @@ -92,7 +94,7 @@ class UserModel(AbstractUser, PermissionsMixin): icon_url = models.ForeignKey( "content.Image", on_delete=models.SET_NULL, blank=True, null=True ) - verifictaion_code = models.UUIDField(blank=True, null=True) + verification_code = models.UUIDField(blank=True, null=True) email = models.EmailField(blank=True) is_confirmed = models.BooleanField(default=False) social_links = ArrayField(models.CharField(max_length=255), blank=True, null=True) @@ -104,7 +106,7 @@ class UserModel(AbstractUser, PermissionsMixin): is_active = models.BooleanField(default=True) is_admin = models.BooleanField(default=False) - objects = CustomAccountManager() # type: ignore + objects: CustomAccountManager = CustomAccountManager() # type: ignore USERNAME_FIELD = "username" @@ -112,6 +114,9 @@ def __str__(self) -> str: return self.username +# MARK: Bridge Tables + + class UserResource(models.Model): user_id = models.ForeignKey(UserModel, on_delete=models.CASCADE) resource_id = models.ForeignKey("content.Resource", on_delete=models.CASCADE) diff --git a/backend/authentication/serializers.py b/backend/authentication/serializers.py index 1cbc6f86d..7f9b731f3 100644 --- a/backend/authentication/serializers.py +++ b/backend/authentication/serializers.py @@ -26,18 +26,7 @@ USER = get_user_model() -class SupportEntityTypeSerializer(serializers.ModelSerializer[SupportEntityType]): - class Meta: - model = SupportEntityType - fields = "__all__" - - def validate(self, data: Dict[str, Union[str, Any]]) -> Dict[str, Union[str, Any]]: - if len(data["name"]) < 3: - raise serializers.ValidationError( - _("The field name must be at least 3 characters long."), - code="invalid_name", - ) - return data +# MARK: Main Tables class SupportSerializer(serializers.ModelSerializer[Support]): @@ -57,6 +46,20 @@ def validate(self, data: Dict[str, Union[str, int]]) -> Dict[str, Union[str, int return data +class SupportEntityTypeSerializer(serializers.ModelSerializer[SupportEntityType]): + class Meta: + model = SupportEntityType + fields = "__all__" + + def validate(self, data: Dict[str, Union[str, Any]]) -> Dict[str, Union[str, Any]]: + if len(data["name"]) < 3: + raise serializers.ValidationError( + _("The field name must be at least 3 characters long."), + code="invalid_name", + ) + return data + + class UserSerializer(serializers.ModelSerializer[UserModel]): class Meta: model = UserModel @@ -79,6 +82,9 @@ def validate(self, data: Dict[str, Union[str, Any]]) -> Dict[str, Union[str, Any return data +# MARK: Bridge Tables + + class UserResourceSerializer(serializers.ModelSerializer[UserResource]): class Meta: model = UserResource @@ -97,6 +103,9 @@ class Meta: fields = "__all__" +# MARK: Methods + + class SignupSerializer(serializers.ModelSerializer[UserModel]): password_confirmed = serializers.CharField(write_only=True) diff --git a/backend/authentication/tests.py b/backend/authentication/tests.py index ebfae4e22..83d997b44 100644 --- a/backend/authentication/tests.py +++ b/backend/authentication/tests.py @@ -113,7 +113,7 @@ def test_signup(client: Client) -> None: assert response.status_code == 201 assert UserModel.objects.filter(username=username) # code for Email confirmation is generated and is a UUID - assert isinstance(user.verifictaion_code, UUID) + assert isinstance(user.verification_code, UUID) assert user.is_confirmed is False # Confirmation Email was sent assert len(mail.outbox) == 1 @@ -149,7 +149,7 @@ def test_signup(client: Client) -> None: assert UserModel.objects.filter(username=second_username).exists() assert user.email == "" assert user.is_confirmed is False - assert user.verifictaion_code is None + assert user.verification_code is None @pytest.mark.django_db diff --git a/backend/authentication/urls.py b/backend/authentication/urls.py index 6507e3c8e..d5699749b 100644 --- a/backend/authentication/urls.py +++ b/backend/authentication/urls.py @@ -7,14 +7,18 @@ router = DefaultRouter() -router.register(r"support_entity_types", views.SupportEntityTypeViewSet) +# MARK: Main Tables + router.register(r"supports", views.SupportViewSet) router.register(r"users", views.UserViewSet) + +# MARK: Bridge Tables + +router.register(r"support_entity_types", views.SupportEntityTypeViewSet) router.register(r"user_resources", views.UserResourceViewSet) router.register(r"user_tasks", views.UserTaskViewSet) router.register(r"user_topics", views.UserTopicViewSet) - urlpatterns = [ path("", include(router.urls)), path("signup/", views.SignupView.as_view(), name="signup"), diff --git a/backend/authentication/views.py b/backend/authentication/views.py index 44e1e75ee..85f1b0ca6 100644 --- a/backend/authentication/views.py +++ b/backend/authentication/views.py @@ -43,10 +43,7 @@ ACTIVIST_EMAIL = os.getenv("ACTIVIST_EMAIL") -class SupportEntityTypeViewSet(viewsets.ModelViewSet[SupportEntityType]): - queryset = SupportEntityType.objects.all() - pagination_class = CustomPagination - serializer_class = SupportEntityTypeSerializer +# MARK: Main Tables class SupportViewSet(viewsets.ModelViewSet[Support]): @@ -61,6 +58,15 @@ class UserViewSet(viewsets.ModelViewSet[UserModel]): serializer_class = UserSerializer +# MARK: Bridge Tables + + +class SupportEntityTypeViewSet(viewsets.ModelViewSet[SupportEntityType]): + queryset = SupportEntityType.objects.all() + pagination_class = CustomPagination + serializer_class = SupportEntityTypeSerializer + + class UserResourceViewSet(viewsets.ModelViewSet[UserResource]): queryset = UserResource.objects.all() pagination_class = CustomPagination @@ -79,6 +85,9 @@ class UserTopicViewSet(viewsets.ModelViewSet[UserTopic]): serializer_class = UserTopicSerializer +# MARK: Methods + + class SignupView(APIView): queryset = UserModel.objects.all() permission_classes = (AllowAny,) @@ -91,9 +100,9 @@ def post(self, request: Request) -> Response: user: UserModel = serializer.save() if user.email != "": - user.verifictaion_code = uuid.uuid4() + user.verification_code = uuid.uuid4() - confirmation_link = f"{FRONTEND_BASE_URL}/confirm/{user.verifictaion_code}" + confirmation_link = f"{FRONTEND_BASE_URL}/confirm/{user.verification_code}" message = f"Welcome to activist.org, {user.username}!, Please confirm your email address by clicking the link: {confirmation_link}" html_message = render_to_string( template_name="signup_email.html", @@ -120,12 +129,12 @@ def post(self, request: Request) -> Response: ) @extend_schema( - parameters=[OpenApiParameter(name="verifictaion_code", type=str, required=True)] + parameters=[OpenApiParameter(name="verification_code", type=str, required=True)] ) def get(self, request: Request) -> Response: """Confirm a user's email address.""" - verifictaion_code = request.GET.get("verifictaion_code") - user = UserModel.objects.filter(verifictaion_code=verifictaion_code).first() + verification_code = request.GET.get("verification_code") + user = UserModel.objects.filter(verification_code=verification_code).first() if user is None: return Response( @@ -134,7 +143,7 @@ def get(self, request: Request) -> Response: ) user.is_confirmed = True - user.verifictaion_code = "" + user.verification_code = "" user.save() return Response( @@ -183,9 +192,9 @@ def get(self, request: Request) -> Response: status=status.HTTP_404_NOT_FOUND, ) - user.verifictaion_code = uuid.uuid4() + user.verification_code = uuid.uuid4() - pwreset_link = f"{FRONTEND_BASE_URL}/pwreset/{user.verifictaion_code}" + pwreset_link = f"{FRONTEND_BASE_URL}/pwreset/{user.verification_code}" message = "Reset your password at activist.org" html_message = render_to_string( template_name="pwreset_email.html", diff --git a/backend/backend/exception_handler.py b/backend/backend/exception_handler.py new file mode 100644 index 000000000..aeaca091b --- /dev/null +++ b/backend/backend/exception_handler.py @@ -0,0 +1,21 @@ +import logging +from typing import Any + +from rest_framework.response import Response +from rest_framework.views import exception_handler + +logger = logging.getLogger(__name__) + + +def bad_request_logger(exception: Any, context: dict[str, Any]) -> Response | None: + # Get the DRF exception handler standard error response + response = exception_handler(exception, context) + + if response is not None: + logger.warning( + f"Bad request: {context['request'].path} -" + f"Status Code: {response.status_code} -" + f"Data: {response.data} -" + ) + + return response diff --git a/backend/backend/settings.py b/backend/backend/settings.py index fd7f1c371..1d9f8f501 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -181,6 +181,7 @@ "DEFAULT_AUTHENTICATION_CLASSES": [ "rest_framework.authentication.TokenAuthentication", ], + "EXCEPTION_HANDLER": "backend.exception_handler.bad_request_logger", } SPECTACULAR_SETTINGS = { diff --git a/backend/content/admin.py b/backend/content/admin.py index 4a18334df..b1d867411 100644 --- a/backend/content/admin.py +++ b/backend/content/admin.py @@ -11,11 +11,16 @@ TopicFormat, ) +# MARK: Main Tables + admin.site.register(Faq) admin.site.register(Image) +admin.site.register(IsoCodeMap) admin.site.register(Resource) -admin.site.register(ResourceTopic) admin.site.register(Task) admin.site.register(Topic) + +# MARK: Main Tables + +admin.site.register(ResourceTopic) admin.site.register(TopicFormat) -admin.site.register(IsoCodeMap) diff --git a/backend/content/factories.py b/backend/content/factories.py index 3f33f05de..7b6885af4 100644 --- a/backend/content/factories.py +++ b/backend/content/factories.py @@ -4,6 +4,8 @@ from .models import Faq, Resource, ResourceTopic, Task, Topic, TopicFormat +# MARK: Main Tables + class FaqFactory(factory.django.DjangoModelFactory): class Meta: @@ -48,6 +50,9 @@ class Meta: deprecation_date = factory.Faker("date") +# MARK: Bridge Tables + + class ResourceTopicFactory(factory.django.DjangoModelFactory): class Meta: model = ResourceTopic diff --git a/backend/content/models.py b/backend/content/models.py index 55c7b4885..dd9452701 100644 --- a/backend/content/models.py +++ b/backend/content/models.py @@ -10,6 +10,8 @@ from backend.mixins.models import CreationDeletionMixin +# MARK: Main Tables + class Discussion(models.Model): id = models.UUIDField(primary_key=True, default=uuid4, editable=False) @@ -25,18 +27,6 @@ def __str__(self) -> str: return f"{self.id}" -class DiscussionEntry(models.Model): - id = models.UUIDField(primary_key=True, default=uuid4, editable=False) - discussion_id = models.ForeignKey(Discussion, on_delete=models.CASCADE) - created_by = models.ForeignKey("authentication.UserModel", on_delete=models.CASCADE) - text = models.CharField(max_length=255, blank=True) - creation_date = models.DateTimeField(auto_now_add=True) - deletion_date = models.DateTimeField(null=True, blank=True) - - def __str__(self) -> str: - return f"{self.id}" - - class Faq(models.Model): id = models.UUIDField(primary_key=True, default=uuid4, editable=False) org_id = models.ForeignKey("entities.Organization", on_delete=models.CASCADE) @@ -48,6 +38,21 @@ def __str__(self) -> str: return self.question +class Image(models.Model): + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) + image_location = models.ImageField( + upload_to="images/", validators=[validate_image_file_extension] + ) + creation_date = models.DateTimeField(auto_now_add=True) + + def __str__(self) -> str: + return f"{self.id}" + + +class IsoCodeMap(models.Model): + code = models.CharField(max_length=2) + + class Resource(models.Model): id = models.UUIDField(primary_key=True, default=uuid4, editable=False) name = models.CharField(max_length=255) @@ -64,6 +69,15 @@ def __str__(self) -> str: return self.name +class Tag(models.Model): + id = models.IntegerField(primary_key=True) + text = models.CharField(max_length=255) + creation_date = models.DateTimeField(auto_now_add=True) + + def __str__(self) -> str: + return f"{self.id}" + + class Task(CreationDeletionMixin): id = models.UUIDField(primary_key=True, default=uuid4, editable=False) name = models.CharField(max_length=255) @@ -87,18 +101,24 @@ def __str__(self) -> str: return self.name -class Tag(models.Model): - id = models.IntegerField(primary_key=True) - text = models.CharField(max_length=255) +# MARK: Bridge Tables + + +class DiscussionEntry(models.Model): + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) + discussion_id = models.ForeignKey(Discussion, on_delete=models.CASCADE) + created_by = models.ForeignKey("authentication.UserModel", on_delete=models.CASCADE) + text = models.CharField(max_length=255, blank=True) creation_date = models.DateTimeField(auto_now_add=True) + deletion_date = models.DateTimeField(null=True, blank=True) def __str__(self) -> str: return f"{self.id}" -class ResourceTopic(models.Model): - resource_id = models.ForeignKey(Resource, on_delete=models.CASCADE) - topic_id = models.ForeignKey(Topic, on_delete=models.CASCADE) +class DiscussionTag(models.Model): + discussion_id = models.ForeignKey(Discussion, on_delete=models.CASCADE) + tag_id = models.ForeignKey(Tag, on_delete=models.CASCADE) def __str__(self) -> str: return f"{self.id}" @@ -112,32 +132,17 @@ def __str__(self) -> str: return f"{self.id}" -class TopicFormat(models.Model): +class ResourceTopic(models.Model): + resource_id = models.ForeignKey(Resource, on_delete=models.CASCADE) topic_id = models.ForeignKey(Topic, on_delete=models.CASCADE) - format_id = models.ForeignKey("events.Format", on_delete=models.CASCADE) def __str__(self) -> str: return f"{self.id}" -class DiscussionTag(models.Model): - discussion_id = models.ForeignKey(Discussion, on_delete=models.CASCADE) - tag_id = models.ForeignKey(Tag, on_delete=models.CASCADE) - - def __str__(self) -> str: - return f"{self.id}" - - -class Image(models.Model): - id = models.UUIDField(primary_key=True, default=uuid4, editable=False) - image_location = models.ImageField( - upload_to="images/", validators=[validate_image_file_extension] - ) - creation_date = models.DateTimeField(auto_now_add=True) +class TopicFormat(models.Model): + topic_id = models.ForeignKey(Topic, on_delete=models.CASCADE) + format_id = models.ForeignKey("events.Format", on_delete=models.CASCADE) def __str__(self) -> str: return f"{self.id}" - - -class IsoCodeMap(models.Model): - code = models.CharField(max_length=2) diff --git a/backend/content/serializers.py b/backend/content/serializers.py index c3a0aa20b..8c972ee19 100644 --- a/backend/content/serializers.py +++ b/backend/content/serializers.py @@ -29,6 +29,8 @@ TopicFormat, ) +# MARK: Main Tables + class DiscussionSerializer(serializers.ModelSerializer[Discussion]): class Meta: @@ -44,25 +46,46 @@ class Meta: ] -class DiscussionEntrySerializer(serializers.ModelSerializer[DiscussionEntry]): - class Meta: - model = DiscussionEntry - fields = [ - "id", - "discussion_id", - "created_by", - "text", - "creation_date", - "deletion_date", - ] - - class FaqSerializer(serializers.ModelSerializer[Faq]): class Meta: model = Faq fields = ["id", "question", "org_id", "answer", "last_updated"] +class ImageSerializer(serializers.ModelSerializer[Image]): + class Meta: + model = Image + fields = ["id", "image_location", "creation_date"] + read_only_fields = ["id", "creation_date"] + + def validate(self, data: Dict[str, Union[str, int]]) -> Dict[str, Union[str, int]]: + image_extensions = [".jpg", ".jpeg", ".png"] + img_format = "" + + try: + with PilImage.open(data["image_location"]) as img: + img.verify() + img_format = img.format.lower() + except Exception: + raise serializers.ValidationError( + _("The image is not valid."), code="corrupted_file" + ) + + if img_format not in image_extensions: + raise serializers.ValidationError( + _("The image must be in jpg, jpeg or png format."), + code="invalid_extension", + ) + + return data + + +class IsoCodeMapSerializer(serializers.ModelSerializer[IsoCodeMap]): + class Meta: + model = IsoCodeMap + fields = "__all__" + + class ResourceSerializer(serializers.ModelSerializer[Resource]): class Meta: model = Resource @@ -78,6 +101,12 @@ class Meta: ] +class TagSerializer(serializers.ModelSerializer[Tag]): + class Meta: + model = Tag + fields = "__all__" + + class TaskSerializer(serializers.ModelSerializer[Task]): class Meta: model = Task @@ -112,65 +141,41 @@ def validate(self, data: Dict[str, Union[str, int]]) -> Dict[str, Union[str, int return data -class TagSerializer(serializers.ModelSerializer[Tag]): - class Meta: - model = Tag - fields = "__all__" +# MARK: Bridge Tables -class ResourceTopicSerializer(serializers.ModelSerializer[ResourceTopic]): +class DiscussionEntrySerializer(serializers.ModelSerializer[DiscussionEntry]): class Meta: - model = ResourceTopic - fields = "__all__" + model = DiscussionEntry + fields = [ + "id", + "discussion_id", + "created_by", + "text", + "creation_date", + "deletion_date", + ] -class ResourceTagSerializer(serializers.ModelSerializer[ResourceTag]): +class DiscussionTagSerializer(serializers.ModelSerializer[DiscussionTag]): class Meta: - model = ResourceTag + model = DiscussionTag fields = "__all__" -class TopicFormatSerializer(serializers.ModelSerializer[TopicFormat]): +class ResourceTagSerializer(serializers.ModelSerializer[ResourceTag]): class Meta: - model = TopicFormat + model = ResourceTag fields = "__all__" -class DiscussionTagSerializer(serializers.ModelSerializer[DiscussionTag]): +class ResourceTopicSerializer(serializers.ModelSerializer[ResourceTopic]): class Meta: - model = DiscussionTag + model = ResourceTopic fields = "__all__" -class ImageSerializer(serializers.ModelSerializer[Image]): - class Meta: - model = Image - fields = ["id", "image_location", "creation_date"] - read_only_fields = ["id", "creation_date"] - - def validate(self, data: Dict[str, Union[str, int]]) -> Dict[str, Union[str, int]]: - image_extensions = [".jpg", ".jpeg", ".png"] - img_format = "" - - try: - with PilImage.open(data["image_location"]) as img: - img.verify() - img_format = img.format.lower() - except Exception: - raise serializers.ValidationError( - _("The image is not valid."), code="corrupted_file" - ) - - if img_format not in image_extensions: - raise serializers.ValidationError( - _("The image must be in jpg, jpeg or png format."), - code="invalid_extension", - ) - - return data - - -class IsoCodeMapSerializer(serializers.ModelSerializer[IsoCodeMap]): +class TopicFormatSerializer(serializers.ModelSerializer[TopicFormat]): class Meta: - model = IsoCodeMap + model = TopicFormat fields = "__all__" diff --git a/backend/content/urls.py b/backend/content/urls.py index be9aeca5b..a05c67c7e 100644 --- a/backend/content/urls.py +++ b/backend/content/urls.py @@ -6,18 +6,23 @@ app_name = "content" router = DefaultRouter() + +# MARK: Main Tables + router.register(r"discussion", views.DiscussionViewSet) -router.register(r"discussion_entry", views.DiscussionEntryViewSet) +router.register(r"faq", views.FaqViewSet) router.register(r"images", views.ImageViewSet) router.register(r"resources", views.ResourceViewSet) router.register(r"tasks", views.TaskViewSet) router.register(r"topics", views.TopicViewSet) + +# MARK: Bridge Tables + +router.register(r"discussion_entry", views.DiscussionEntryViewSet) router.register(r"resource_topics", views.ResourceTopicViewSet) router.register(r"topic_formats", views.TopicFormatViewSet) -router.register(r"faq", views.FaqViewSet) - urlpatterns = [ path("", include(router.urls)), - path("iso_codes/", views.IsoCodeMapListAPIView.as_view(), name="iso_codes"), + path("iso_code_map/", views.IsoCodeMapListAPIView.as_view(), name="iso_code_map"), ] diff --git a/backend/content/views.py b/backend/content/views.py index 2ece0d467..bab12a56b 100644 --- a/backend/content/views.py +++ b/backend/content/views.py @@ -34,6 +34,8 @@ TopicSerializer, ) +# MARK: Main Tables + class DiscussionViewSet(viewsets.ModelViewSet[Discussion]): queryset = Discussion.objects.all() @@ -115,32 +117,57 @@ def destroy(self, request: Request, pk: str | None = None) -> Response: return Response(status=status.HTTP_204_NO_CONTENT) -class DiscussionEntryViewSet(viewsets.ModelViewSet[DiscussionEntry]): - queryset = DiscussionEntry.objects.all() - serializer_class = DiscussionEntrySerializer +class FaqViewSet(viewsets.ModelViewSet[Faq]): + queryset = Faq.objects.all() + serializer_class = FaqSerializer + pagination_class = CustomPagination + + +class ImageViewSet(viewsets.ModelViewSet[Image]): + queryset = Image.objects.all() + serializer_class = ImageSerializer + pagination_class = CustomPagination + + +class IsoCodeMapListAPIView(ListAPIView[IsoCodeMap]): + queryset = IsoCodeMap.objects.all() + serializer_class = IsoCodeMapSerializer + + def get(self, request: Request) -> Response: + queryset = self.get_queryset() + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class ResourceViewSet(viewsets.ModelViewSet[Resource]): + queryset = Resource.objects.all() + serializer_class = ResourceSerializer pagination_class = CustomPagination throttle_classes = [AnonRateThrottle, UserRateThrottle] permission_classes = [IsAuthenticatedOrReadOnly] def create(self, request: Request) -> Response: if request.user.is_authenticated: - request.data["created_by"] = request.user serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - serializer.save() + serializer.save(created_by=request.user) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response( - {"error": "You are not allowed to create a discussion entry."}, + {"error": "You are not allowed to create a resource."}, status=status.HTTP_403_FORBIDDEN, ) def retrieve(self, request: Request, pk: str | None = None) -> Response: - queryset = self.get_queryset() - item = queryset.filter(id=pk).first() + if request.user.is_authenticated: + query = self.queryset.filter( + Q(is_private=False) | Q(is_private=True, created_by=request.user), id=pk + ) + else: + query = self.queryset.filter(Q(is_private=False), id=pk) - serializer = self.get_serializer(item) - return Response(serializer.data, status=status.HTTP_200_OK) + serializer = self.get_serializer(query) + return Response(serializer.data) def list(self, request: Request) -> Response: if request.user.is_authenticated: @@ -148,7 +175,7 @@ def list(self, request: Request) -> Response: Q(is_private=False) | Q(is_private=True, created_by=request.user) ) else: - query = self.queryset.filter() + query = self.queryset.filter(is_private=False) serializer = self.get_serializer(query, many=True) return self.get_paginated_response(self.paginate_queryset(serializer.data)) @@ -157,7 +184,7 @@ def update(self, request: Request, pk: str | None = None) -> Response: item = self.get_object() if item.created_by != request.user: return Response( - {"error": "You are not allowed to update this discussion entry."}, + {"error": "You are not allowed to update this resource."}, status=status.HTTP_403_FORBIDDEN, ) serializer = self.get_serializer(item, data=request.data) @@ -169,7 +196,7 @@ def partial_update(self, request: Request, pk: str | None = None) -> Response: item = self.get_object() if item.created_by != request.user: return Response( - {"error": "You are not allowed to update this discussion entry."}, + {"error": "You are not allowed to update this resource."}, status=status.HTTP_403_FORBIDDEN, ) serializer = self.get_serializer(item, data=request.data, partial=True) @@ -181,7 +208,7 @@ def destroy(self, request: Request, pk: str | None = None) -> Response: item = self.get_object() if item.created_by != request.user: return Response( - {"error": "You are not allowed to delete this discussion entry."}, + {"error": "You are not allowed to delete this resource."}, status=status.HTTP_403_FORBIDDEN, ) @@ -189,47 +216,48 @@ def destroy(self, request: Request, pk: str | None = None) -> Response: return Response(status=status.HTTP_204_NO_CONTENT) -class FaqViewSet(viewsets.ModelViewSet[Faq]): - queryset = Faq.objects.all() - serializer_class = FaqSerializer +class TaskViewSet(viewsets.ModelViewSet[Task]): + queryset = Task.objects.all() + serializer_class = TaskSerializer pagination_class = CustomPagination -class ImageViewSet(viewsets.ModelViewSet[Image]): - queryset = Image.objects.all() - serializer_class = ImageSerializer +class TopicViewSet(viewsets.ModelViewSet[Topic]): + queryset = Topic.objects.all() + serializer_class = TopicSerializer + pagination_class = CustomPagination -class ResourceViewSet(viewsets.ModelViewSet[Resource]): - queryset = Resource.objects.all() - serializer_class = ResourceSerializer +# MARK: Bridge Tables + + +class DiscussionEntryViewSet(viewsets.ModelViewSet[DiscussionEntry]): + queryset = DiscussionEntry.objects.all() + serializer_class = DiscussionEntrySerializer pagination_class = CustomPagination throttle_classes = [AnonRateThrottle, UserRateThrottle] permission_classes = [IsAuthenticatedOrReadOnly] def create(self, request: Request) -> Response: if request.user.is_authenticated: + request.data["created_by"] = request.user serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - serializer.save(created_by=request.user) + serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response( - {"error": "You are not allowed to create a resource."}, + {"error": "You are not allowed to create a discussion entry."}, status=status.HTTP_403_FORBIDDEN, ) def retrieve(self, request: Request, pk: str | None = None) -> Response: - if request.user.is_authenticated: - query = self.queryset.filter( - Q(is_private=False) | Q(is_private=True, created_by=request.user), id=pk - ) - else: - query = self.queryset.filter(Q(is_private=False), id=pk) + queryset = self.get_queryset() + item = queryset.filter(id=pk).first() - serializer = self.get_serializer(query) - return Response(serializer.data) + serializer = self.get_serializer(item) + return Response(serializer.data, status=status.HTTP_200_OK) def list(self, request: Request) -> Response: if request.user.is_authenticated: @@ -237,7 +265,7 @@ def list(self, request: Request) -> Response: Q(is_private=False) | Q(is_private=True, created_by=request.user) ) else: - query = self.queryset.filter(is_private=False) + query = self.queryset.filter() serializer = self.get_serializer(query, many=True) return self.get_paginated_response(self.paginate_queryset(serializer.data)) @@ -246,7 +274,7 @@ def update(self, request: Request, pk: str | None = None) -> Response: item = self.get_object() if item.created_by != request.user: return Response( - {"error": "You are not allowed to update this resource."}, + {"error": "You are not allowed to update this discussion entry."}, status=status.HTTP_403_FORBIDDEN, ) serializer = self.get_serializer(item, data=request.data) @@ -258,7 +286,7 @@ def partial_update(self, request: Request, pk: str | None = None) -> Response: item = self.get_object() if item.created_by != request.user: return Response( - {"error": "You are not allowed to update this resource."}, + {"error": "You are not allowed to update this discussion entry."}, status=status.HTTP_403_FORBIDDEN, ) serializer = self.get_serializer(item, data=request.data, partial=True) @@ -270,7 +298,7 @@ def destroy(self, request: Request, pk: str | None = None) -> Response: item = self.get_object() if item.created_by != request.user: return Response( - {"error": "You are not allowed to delete this resource."}, + {"error": "You are not allowed to delete this discussion entry."}, status=status.HTTP_403_FORBIDDEN, ) @@ -278,19 +306,6 @@ def destroy(self, request: Request, pk: str | None = None) -> Response: return Response(status=status.HTTP_204_NO_CONTENT) -class TaskViewSet(viewsets.ModelViewSet[Task]): - queryset = Task.objects.all() - serializer_class = TaskSerializer - pagination_class = CustomPagination - - -class TopicViewSet(viewsets.ModelViewSet[Topic]): - queryset = Topic.objects.all() - serializer_class = TopicSerializer - - pagination_class = CustomPagination - - class ResourceTopicViewSet(viewsets.ModelViewSet[ResourceTopic]): queryset = ResourceTopic.objects.all() serializer_class = ResourceTopicSerializer @@ -301,13 +316,3 @@ class TopicFormatViewSet(viewsets.ModelViewSet[TopicFormat]): queryset = TopicFormat.objects.all() serializer_class = TopicFormatSerializer pagination_class = CustomPagination - - -class IsoCodeMapListAPIView(ListAPIView[IsoCodeMap]): - queryset = IsoCodeMap.objects.all() - serializer_class = IsoCodeMapSerializer - - def get(self, request: Request) -> Response: - queryset = self.get_queryset() - serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/backend/entities/admin.py b/backend/entities/admin.py index 47e4003e5..9bec21206 100644 --- a/backend/entities/admin.py +++ b/backend/entities/admin.py @@ -6,6 +6,7 @@ GroupImage, GroupMember, GroupResource, + GroupText, GroupTopic, Organization, OrganizationApplication, @@ -14,22 +15,32 @@ OrganizationMember, OrganizationResource, OrganizationTask, + OrganizationText, OrganizationTopic, StatusType, ) +# MARK: Main Tables + admin.site.register(Group) +admin.site.register(Organization) + +# MARK: Bridge Tables + admin.site.register(GroupEvent) admin.site.register(GroupImage) admin.site.register(GroupMember) admin.site.register(GroupResource) +admin.site.register(GroupText) admin.site.register(GroupTopic) -admin.site.register(Organization) + admin.site.register(OrganizationApplication) admin.site.register(OrganizationEvent) admin.site.register(OrganizationImage) admin.site.register(OrganizationMember) admin.site.register(OrganizationResource) admin.site.register(OrganizationTask) +admin.site.register(OrganizationText) admin.site.register(OrganizationTopic) + admin.site.register(StatusType) diff --git a/backend/entities/factories.py b/backend/entities/factories.py index 56f5ef441..b7bb3692f 100644 --- a/backend/entities/factories.py +++ b/backend/entities/factories.py @@ -8,6 +8,7 @@ GroupImage, GroupMember, GroupResource, + GroupText, GroupTopic, Organization, OrganizationApplication, @@ -17,9 +18,12 @@ OrganizationMember, OrganizationResource, OrganizationTask, + OrganizationText, OrganizationTopic, ) +# MARK: Main Tables + class OrganizationFactory(factory.django.DjangoModelFactory): class Meta: @@ -33,6 +37,76 @@ class Meta: is_high_risk = factory.Faker("boolean") +class GroupFactory(factory.django.DjangoModelFactory): + class Meta: + model = Group + + org_id = factory.SubFactory(OrganizationFactory) + name = factory.Faker("word") + tagline = factory.Faker("word") + description = factory.Faker("text") + social_links = factory.List([factory.Faker("word") for _ in range(10)]) + created_by = factory.SubFactory("authentication.factories.UserFactory") + creation_date = factory.LazyFunction(datetime.datetime.now) + deletion_date = factory.LazyFunction(datetime.datetime.now) + + +# MARK: Bridge Tables + + +class GroupEventFactory(factory.django.DjangoModelFactory): + class Meta: + model = GroupEvent + + group_id = factory.SubFactory(GroupFactory) + event_id = factory.SubFactory("events.factories.EventFactory") + + +class GroupImageFactory(factory.django.DjangoModelFactory): + class Meta: + model = GroupImage + + group_id = factory.SubFactory(GroupFactory) + image_id = factory.SubFactory("content.factories.ImageFactory") + + +class GroupMemberFactory(factory.django.DjangoModelFactory): + class Meta: + model = GroupMember + + group_id = factory.SubFactory(GroupFactory) + user_id = factory.SubFactory("authentication.factories.UserFactory") + is_admin = factory.Faker("boolean") + + +class GroupResourceFactory(factory.django.DjangoModelFactory): + class Meta: + model = GroupResource + + group_id = factory.SubFactory(GroupFactory) + resource_id = factory.SubFactory("content.factories.ResourceFactory") + + +class GroupTextFactory(factory.django.DjangoModelFactory): + class Meta: + model = GroupText + + group_id = factory.SubFactory(GroupFactory) + iso = factory.Faker("word") + primary = factory.Fakeer("boolean") + description = factory.Faker("text") + get_involved = factory.Faker("text") + donate_prompt = factory.Faker("text") + + +class GroupTopicFactory(factory.django.DjangoModelFactory): + class Meta: + model = GroupTopic + + group_id = factory.SubFactory(GroupFactory) + topic_id = factory.SubFactory("content.factories.TopicFactory") + + class OrganizationApplicationStatusFactory(factory.django.DjangoModelFactory): class Meta: model = OrganizationApplicationStatus @@ -87,20 +161,6 @@ class Meta: resource_id = factory.SubFactory("content.factories.ResourceFactory") -class GroupFactory(factory.django.DjangoModelFactory): - class Meta: - model = Group - - org_id = factory.SubFactory(OrganizationFactory) - name = factory.Faker("word") - tagline = factory.Faker("word") - description = factory.Faker("text") - social_links = factory.List([factory.Faker("word") for _ in range(10)]) - created_by = factory.SubFactory("authentication.factories.UserFactory") - creation_date = factory.LazyFunction(datetime.datetime.now) - deletion_date = factory.LazyFunction(datetime.datetime.now) - - class OrganizationTaskFactory(factory.django.DjangoModelFactory): class Meta: model = OrganizationTask @@ -110,50 +170,21 @@ class Meta: group_id = factory.SubFactory(GroupFactory) -class OrganizationTopicFactory(factory.django.DjangoModelFactory): +class OrganizationTextFactory(factory.django.DjangoModelFactory): class Meta: - model = OrganizationTopic + model = OrganizationText org_id = factory.SubFactory(OrganizationFactory) - topic_id = factory.SubFactory("content.factories.TopicFactory") - - -class GroupEventFactory(factory.django.DjangoModelFactory): - class Meta: - model = GroupEvent - - group_id = factory.SubFactory(GroupFactory) - event_id = factory.SubFactory("events.factories.EventFactory") - - -class GroupImageFactory(factory.django.DjangoModelFactory): - class Meta: - model = GroupImage - - group_id = factory.SubFactory(GroupFactory) - image_id = factory.SubFactory("content.factories.ImageFactory") - - -class GroupMemberFactory(factory.django.DjangoModelFactory): - class Meta: - model = GroupMember - - group_id = factory.SubFactory(GroupFactory) - user_id = factory.SubFactory("authentication.factories.UserFactory") - is_admin = factory.Faker("boolean") - - -class GroupResourceFactory(factory.django.DjangoModelFactory): - class Meta: - model = GroupResource - - group_id = factory.SubFactory(GroupFactory) - resource_id = factory.SubFactory("content.factories.ResourceFactory") + iso = factory.Faker("word") + primary = factory.Fakeer("boolean") + description = factory.Faker("text") + get_involved = factory.Faker("text") + donate_prompt = factory.Faker("text") -class GroupTopicFactory(factory.django.DjangoModelFactory): +class OrganizationTopicFactory(factory.django.DjangoModelFactory): class Meta: - model = GroupTopic + model = OrganizationTopic - group_id = factory.SubFactory(GroupFactory) + org_id = factory.SubFactory(OrganizationFactory) topic_id = factory.SubFactory("content.factories.TopicFactory") diff --git a/backend/entities/models.py b/backend/entities/models.py index 67a1be7c0..5f72bc985 100644 --- a/backend/entities/models.py +++ b/backend/entities/models.py @@ -7,22 +7,27 @@ from django.contrib.postgres.fields import ArrayField from django.db import models -from authentication.enums import StatusTypes +from authentication import enums + +# MARK: Main Tables class Organization(models.Model): id = models.UUIDField(primary_key=True, default=uuid4, editable=False) - name = models.CharField(max_length=255, unique=True) + name = models.CharField(max_length=255) tagline = models.CharField(max_length=255, blank=True) icon_url = models.OneToOneField( "content.Image", on_delete=models.CASCADE, null=True, blank=True ) + location = models.CharField(max_length=255) + # location_id = models.OneToOneField( + # "content.Location", on_delete=models.CASCADE, null=False, blank=False + # ) created_by = models.ForeignKey( "authentication.UserModel", related_name="created_orgs", on_delete=models.CASCADE, ) - description = models.TextField(max_length=500) social_links = ArrayField( models.CharField(max_length=255), default=list, blank=True ) @@ -31,7 +36,7 @@ class Organization(models.Model): status = models.ForeignKey( "StatusType", on_delete=models.CASCADE, - default=StatusTypes.PENDING.value, + default=enums.StatusTypes.PENDING.value, blank=True, null=True, ) @@ -43,39 +48,54 @@ def __str__(self) -> str: return self.name -class OrganizationApplicationStatus(models.Model): - id = models.IntegerField(primary_key=True) - status_name = models.CharField(max_length=255) +class Group(models.Model): + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) + name = models.CharField(max_length=255) + tagline = models.CharField(max_length=255, blank=True) + org_id = models.ForeignKey(Organization, on_delete=models.CASCADE) + location = models.CharField(max_length=255) + # location_id = models.OneToOneField( + # "content.Location", on_delete=models.CASCADE, null=False, blank=False + # ) + about_images = models.ManyToManyField( + "content.Image", related_name="about_img", blank=True + ) + created_by = models.ForeignKey("authentication.UserModel", on_delete=models.CASCADE) + get_involved_url = models.URLField(blank=True) + social_links = ArrayField( + models.CharField(max_length=255), default=list, blank=True + ) + category = models.CharField(max_length=255) + creation_date = models.DateTimeField(auto_now_add=True) def __str__(self) -> str: - return self.status_name + return self.name -class OrganizationApplication(models.Model): - org_id = models.ForeignKey(Organization, on_delete=models.CASCADE) - status = models.ForeignKey("StatusType", on_delete=models.CASCADE, default=1) - orgs_in_favor = models.ManyToManyField( - "entities.Organization", related_name="in_favor", blank=True - ) - orgs_against = models.ManyToManyField( - "entities.Organization", related_name="against", blank=True +class Status(models.Model): + status_type = models.ForeignKey("StatusType", on_delete=models.CASCADE) + org_id = models.ForeignKey( + Organization, on_delete=models.CASCADE, related_name="org_status" ) - creation_date = models.DateTimeField(auto_now_add=True) + user_id = models.ForeignKey("authentication.UserModel", on_delete=models.CASCADE) def __str__(self) -> str: - return f"{self.creation_date}" + return f"{self.org_id.name} - {self.status_type}" -class OrganizationEvent(models.Model): - org_id = models.ForeignKey(Organization, on_delete=models.CASCADE) +# MARK: Bridge Tables + + +class GroupEvent(models.Model): + group_id = models.ForeignKey(Group, on_delete=models.CASCADE) event_id = models.ForeignKey("events.Event", on_delete=models.CASCADE) def __str__(self) -> str: return f"{self.id}" -class OrganizationImage(models.Model): - org_id = models.ForeignKey(Organization, on_delete=models.CASCADE) +class GroupImage(models.Model): + group_id = models.ForeignKey(Group, on_delete=models.CASCADE) image_id = models.ForeignKey("content.Image", on_delete=models.CASCADE) sequence_index = models.IntegerField() @@ -83,8 +103,8 @@ def __str__(self) -> str: return f"{self.id}" -class OrganizationMember(models.Model): - org_id = models.ForeignKey(Organization, on_delete=models.CASCADE) +class GroupMember(models.Model): + group_id = models.ForeignKey(Group, on_delete=models.CASCADE) user_id = models.ForeignKey("authentication.UserModel", on_delete=models.CASCADE) is_owner = models.BooleanField(default=False) is_admin = models.BooleanField(default=False) @@ -94,73 +114,64 @@ def __str__(self) -> str: return f"{self.id}" -class OrganizationResource(models.Model): - org_id = models.ForeignKey(Organization, on_delete=models.CASCADE) +class GroupResource(models.Model): + group_id = models.ForeignKey(Group, on_delete=models.CASCADE) resource_id = models.ForeignKey("content.Resource", on_delete=models.CASCADE) def __str__(self) -> str: return f"{self.id}" -class Group(models.Model): - id = models.UUIDField(primary_key=True, default=uuid4, editable=False) - org_id = models.ForeignKey(Organization, on_delete=models.CASCADE) - name = models.CharField(max_length=255) - tagline = models.CharField(max_length=255, blank=True) - about_images = models.ManyToManyField( - "content.Image", related_name="about_img", blank=True - ) - created_by = models.ForeignKey("authentication.UserModel", on_delete=models.CASCADE) - get_involved_url = models.URLField(blank=True) +class GroupText(models.Model): + group_id = models.ForeignKey(Group, on_delete=models.CASCADE) + iso = models.ForeignKey("content.IsoCodeMap", on_delete=models.CASCADE) + primary = models.BooleanField(default=False) description = models.TextField(max_length=500) - social_links = ArrayField( - models.CharField(max_length=255), default=list, blank=True - ) - category = models.CharField(max_length=255) - creation_date = models.DateTimeField(auto_now_add=True) + get_involved = models.TextField(max_length=500, blank=True) + donate_prompt = models.TextField(max_length=500, blank=True) + + +class GroupTopic(models.Model): + group_id = models.ForeignKey(Group, on_delete=models.CASCADE) + topic_id = models.ForeignKey("content.Topic", on_delete=models.CASCADE) def __str__(self) -> str: - return self.name + return f"{self.id}" -class OrganizationTask(models.Model): +class OrganizationApplication(models.Model): org_id = models.ForeignKey(Organization, on_delete=models.CASCADE) - task_id = models.ForeignKey("content.Task", on_delete=models.CASCADE) - group_id = models.ForeignKey( - "Group", on_delete=models.CASCADE, null=True, blank=True + status = models.ForeignKey("StatusType", on_delete=models.CASCADE, default=1) + orgs_in_favor = models.ManyToManyField( + "entities.Organization", related_name="in_favor", blank=True ) + orgs_against = models.ManyToManyField( + "entities.Organization", related_name="against", blank=True + ) + creation_date = models.DateTimeField(auto_now_add=True) def __str__(self) -> str: - return f"{self.id}" + return f"{self.creation_date}" -class OrganizationTopic(models.Model): - org_id = models.ForeignKey(Organization, on_delete=models.CASCADE) - topic_id = models.ForeignKey("content.Topic", on_delete=models.CASCADE) +class OrganizationApplicationStatus(models.Model): + id = models.IntegerField(primary_key=True) + status_name = models.CharField(max_length=255) def __str__(self) -> str: - return f"{self.id}" + return self.status_name -class OrganizationText(models.Model): +class OrganizationEvent(models.Model): org_id = models.ForeignKey(Organization, on_delete=models.CASCADE) - iso = models.ForeignKey("content.IsoCodeMap", on_delete=models.CASCADE) - primary = models.BooleanField(default=False) - description = models.TextField(max_length=500) - get_involved = models.TextField(max_length=500) - donate_prompt = models.TextField(max_length=500) - - -class GroupEvent(models.Model): - group_id = models.ForeignKey(Group, on_delete=models.CASCADE) event_id = models.ForeignKey("events.Event", on_delete=models.CASCADE) def __str__(self) -> str: return f"{self.id}" -class GroupImage(models.Model): - group_id = models.ForeignKey(Group, on_delete=models.CASCADE) +class OrganizationImage(models.Model): + org_id = models.ForeignKey(Organization, on_delete=models.CASCADE) image_id = models.ForeignKey("content.Image", on_delete=models.CASCADE) sequence_index = models.IntegerField() @@ -168,8 +179,8 @@ def __str__(self) -> str: return f"{self.id}" -class GroupMember(models.Model): - group_id = models.ForeignKey(Group, on_delete=models.CASCADE) +class OrganizationMember(models.Model): + org_id = models.ForeignKey(Organization, on_delete=models.CASCADE) user_id = models.ForeignKey("authentication.UserModel", on_delete=models.CASCADE) is_owner = models.BooleanField(default=False) is_admin = models.BooleanField(default=False) @@ -179,40 +190,40 @@ def __str__(self) -> str: return f"{self.id}" -class GroupResource(models.Model): - group_id = models.ForeignKey(Group, on_delete=models.CASCADE) +class OrganizationResource(models.Model): + org_id = models.ForeignKey(Organization, on_delete=models.CASCADE) resource_id = models.ForeignKey("content.Resource", on_delete=models.CASCADE) def __str__(self) -> str: return f"{self.id}" -class GroupTopic(models.Model): - group_id = models.ForeignKey(Group, on_delete=models.CASCADE) - topic_id = models.ForeignKey("content.Topic", on_delete=models.CASCADE) +class OrganizationTask(models.Model): + org_id = models.ForeignKey(Organization, on_delete=models.CASCADE) + task_id = models.ForeignKey("content.Task", on_delete=models.CASCADE) + group_id = models.ForeignKey( + "Group", on_delete=models.CASCADE, null=True, blank=True + ) def __str__(self) -> str: return f"{self.id}" -class GroupText(models.Model): - group_id = models.ForeignKey(Group, on_delete=models.CASCADE) +class OrganizationText(models.Model): + org_id = models.ForeignKey(Organization, on_delete=models.CASCADE) iso = models.ForeignKey("content.IsoCodeMap", on_delete=models.CASCADE) primary = models.BooleanField(default=False) - description = models.TextField(max_length=500) - get_involved = models.TextField(max_length=500) - donate_prompt = models.TextField(max_length=500) + description = models.TextField(max_length=2500) + get_involved = models.TextField(max_length=500, blank=True) + donate_prompt = models.TextField(max_length=500, blank=True) -class Status(models.Model): - status_type = models.ForeignKey("StatusType", on_delete=models.CASCADE) - org_id = models.ForeignKey( - Organization, on_delete=models.CASCADE, related_name="org_status" - ) - user_id = models.ForeignKey("authentication.UserModel", on_delete=models.CASCADE) +class OrganizationTopic(models.Model): + org_id = models.ForeignKey(Organization, on_delete=models.CASCADE) + topic_id = models.ForeignKey("content.Topic", on_delete=models.CASCADE) def __str__(self) -> str: - return f"{self.org_id.name} - {self.status_type}" + return f"{self.id}" class StatusType(models.Model): diff --git a/backend/entities/serializers.py b/backend/entities/serializers.py index a01f3c9b7..f3cc732e2 100644 --- a/backend/entities/serializers.py +++ b/backend/entities/serializers.py @@ -10,6 +10,7 @@ GroupImage, GroupMember, GroupResource, + GroupText, GroupTopic, Organization, OrganizationApplication, @@ -18,11 +19,20 @@ OrganizationMember, OrganizationResource, OrganizationTask, + OrganizationText, OrganizationTopic, Status, StatusType, ) +# MARK: Main Tables + + +class GroupSerializer(serializers.ModelSerializer[Group]): + class Meta: + model = Group + fields = "__all__" + class OrganizationSerializer(serializers.ModelSerializer[Organization]): class Meta: @@ -38,8 +48,8 @@ class Meta: "name", "tagline", "icon_url", + "location", "created_by", - "description", "social_links", "is_high_risk", "status", @@ -48,89 +58,98 @@ class Meta: ] -class OrganizationApplicationSerializer( - serializers.ModelSerializer[OrganizationApplication] -): +class StatusSerializer(serializers.ModelSerializer[Status]): class Meta: - model = OrganizationApplication + model = Status fields = "__all__" -class OrganizationEventSerializer(serializers.ModelSerializer[OrganizationEvent]): +# MARK: Bridge Tables + + +class GroupEventSerializer(serializers.ModelSerializer[GroupEvent]): class Meta: - model = OrganizationEvent + model = GroupEvent fields = "__all__" -class OrganizationMemberSerializer(serializers.ModelSerializer[OrganizationMember]): +class GroupImageSerializer(serializers.ModelSerializer[GroupImage]): class Meta: - model = OrganizationMember + model = GroupImage fields = "__all__" -class OrganizationImageSerializer(serializers.ModelSerializer[OrganizationImage]): +class GroupMemberSerializer(serializers.ModelSerializer[GroupMember]): class Meta: - model = OrganizationImage + model = GroupMember fields = "__all__" -class OrganizationResourceSerializer(serializers.ModelSerializer[OrganizationResource]): +class GroupResourceSerializer(serializers.ModelSerializer[GroupResource]): class Meta: - model = OrganizationResource + model = GroupResource fields = "__all__" -class GroupSerializer(serializers.ModelSerializer[Group]): +class GroupTextSerializer(serializers.ModelSerializer[GroupText]): class Meta: - model = Group + model = GroupText fields = "__all__" -class GroupImageSerializer(serializers.ModelSerializer[GroupImage]): +class GroupTopicSerializer(serializers.ModelSerializer[GroupTopic]): class Meta: - model = GroupImage + model = GroupTopic fields = "__all__" -class OrganizationTaskSerializer(serializers.ModelSerializer[OrganizationTask]): +class OrganizationApplicationSerializer( + serializers.ModelSerializer[OrganizationApplication] +): class Meta: - model = OrganizationTask + model = OrganizationApplication fields = "__all__" -class OrganizationTopicSerializer(serializers.ModelSerializer[OrganizationTopic]): +class OrganizationEventSerializer(serializers.ModelSerializer[OrganizationEvent]): class Meta: - model = OrganizationTopic + model = OrganizationEvent fields = "__all__" -class GroupEventSerializer(serializers.ModelSerializer[GroupEvent]): +class OrganizationMemberSerializer(serializers.ModelSerializer[OrganizationMember]): class Meta: - model = GroupEvent + model = OrganizationMember fields = "__all__" -class GroupMemberSerializer(serializers.ModelSerializer[GroupMember]): +class OrganizationImageSerializer(serializers.ModelSerializer[OrganizationImage]): class Meta: - model = GroupMember + model = OrganizationImage fields = "__all__" -class GroupResourceSerializer(serializers.ModelSerializer[GroupResource]): +class OrganizationResourceSerializer(serializers.ModelSerializer[OrganizationResource]): class Meta: - model = GroupResource + model = OrganizationResource fields = "__all__" -class GroupTopicSerializer(serializers.ModelSerializer[GroupTopic]): +class OrganizationTaskSerializer(serializers.ModelSerializer[OrganizationTask]): class Meta: - model = GroupTopic + model = OrganizationTask fields = "__all__" -class StatusSerializer(serializers.ModelSerializer[Status]): +class OrganizationTextSerializer(serializers.ModelSerializer[OrganizationText]): class Meta: - model = Status + model = OrganizationText + fields = "__all__" + + +class OrganizationTopicSerializer(serializers.ModelSerializer[OrganizationTopic]): + class Meta: + model = OrganizationTopic fields = "__all__" diff --git a/backend/entities/urls.py b/backend/entities/urls.py index d73d03c47..0ac217c37 100644 --- a/backend/entities/urls.py +++ b/backend/entities/urls.py @@ -7,21 +7,30 @@ router = DefaultRouter() +# MARK: Main Tables + +router.register(r"groups", views.GroupViewSet) router.register(r"organizations", views.OrganizationViewSet, basename="organization") +router.register(r"status", views.StatusViewSet) + +# MARK: Bridge Tables + +router.register(r"group_events", views.GroupEventViewSet) +router.register(r"group_images", views.GroupImageViewSet) +router.register(r"group_members", views.GroupMemberViewSet) +router.register(r"group_resources", views.GroupResourceViewSet) +router.register(r"group_texts", views.GroupTextViewSet) +router.register(r"group_topics", views.GroupTopicViewSet) + router.register(r"organization_applications", views.OrganizationApplicationViewSet) router.register(r"organization_events", views.OrganizationEventViewSet) router.register(r"organization_images", views.OrganizationImageViewSet) router.register(r"organization_members", views.OrganizationMemberViewSet) router.register(r"organization_resources", views.OrganizationResourceViewSet) -router.register(r"groups", views.GroupViewSet) router.register(r"organization_tasks", views.OrganizationTaskViewSet) +router.register(r"organization_texts", views.OrganizationTextViewSet) router.register(r"organization_topics", views.OrganizationTopicViewSet) -router.register(r"group_events", views.GroupEventViewSet) -router.register(r"group_images", views.GroupImageViewSet) -router.register(r"group_members", views.GroupMemberViewSet) -router.register(r"group_resources", views.GroupResourceViewSet) -router.register(r"group_topics", views.GroupTopicViewSet) -router.register(r"status", views.StatusViewSet) + router.register(r"status_type", views.StatusTypeViewSet) urlpatterns = [ diff --git a/backend/entities/views.py b/backend/entities/views.py index c14e77bd4..972b0d5f0 100644 --- a/backend/entities/views.py +++ b/backend/entities/views.py @@ -15,6 +15,7 @@ GroupImage, GroupMember, GroupResource, + GroupText, GroupTopic, Organization, OrganizationApplication, @@ -23,6 +24,7 @@ OrganizationMember, OrganizationResource, OrganizationTask, + OrganizationText, OrganizationTopic, Status, StatusType, @@ -33,6 +35,7 @@ GroupMemberSerializer, GroupResourceSerializer, GroupSerializer, + GroupTextSerializer, GroupTopicSerializer, OrganizationApplicationSerializer, OrganizationEventSerializer, @@ -41,11 +44,72 @@ OrganizationResourceSerializer, OrganizationSerializer, OrganizationTaskSerializer, + OrganizationTextSerializer, OrganizationTopicSerializer, StatusSerializer, StatusTypeSerializer, ) +# MARK: Main Tables + + +class GroupViewSet(viewsets.ModelViewSet[Group]): + queryset = Group.objects.all() + serializer_class = GroupSerializer + pagination_class = CustomPagination + + 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: + 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}"} + 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) + + return Response(serializer.data, status=status.HTTP_200_OK) + + def partial_update(self, request: Request, *args: str, **kwargs: int) -> Response: + group = self.queryset.filter(id=kwargs["pk"]).first() + + if group is None: + 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"}, + status.HTTP_401_UNAUTHORIZED, + ) + + 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: + group = self.queryset.filter(id=kwargs["pk"]).first() + + if group is None: + 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 + ) + class OrganizationViewSet(viewsets.ModelViewSet[Organization]): queryset = Organization.objects.all() @@ -67,8 +131,7 @@ def create(self, request: Request) -> Response: return Response(serializer.data, status=status.HTTP_201_CREATED) def retrieve(self, request: Request, pk: str | None = None) -> Response: - org = self.queryset.filter(id=pk).first() - if org: + if org := self.queryset.filter(id=pk).first(): serializer = self.get_serializer(org) return Response(serializer.data, status=status.HTTP_200_OK) @@ -132,7 +195,6 @@ def destroy(self, request: Request, pk: str | None = None) -> Response: org.is_high_risk = False org.status_updated = None org.tagline = "" - org.description = "" org.social_links = [] org.save() @@ -141,32 +203,18 @@ def destroy(self, request: Request, pk: str | None = None) -> Response: ) -class OrganizationApplicationViewSet(viewsets.ModelViewSet[OrganizationApplication]): - queryset = OrganizationApplication.objects.all() - serializer_class = OrganizationApplicationSerializer - pagination_class = CustomPagination - - -class OrganizationEventViewSet(viewsets.ModelViewSet[OrganizationEvent]): - queryset = OrganizationEvent.objects.all() - serializer_class = OrganizationEventSerializer - pagination_class = CustomPagination - - -class OrganizationMemberViewSet(viewsets.ModelViewSet[OrganizationMember]): - queryset = OrganizationMember.objects.all() - serializer_class = OrganizationMemberSerializer +class StatusViewSet(viewsets.ModelViewSet[Status]): + queryset = Status.objects.all() + serializer_class = StatusSerializer pagination_class = CustomPagination -class OrganizationImageViewSet(viewsets.ModelViewSet[OrganizationImage]): - queryset = OrganizationImage.objects.all() - serializer_class = OrganizationImageSerializer +# MARK: Bridge Tables -class OrganizationResourceViewSet(viewsets.ModelViewSet[OrganizationResource]): - queryset = OrganizationResource.objects.all() - serializer_class = OrganizationResourceSerializer +class GroupEventViewSet(viewsets.ModelViewSet[GroupEvent]): + queryset = GroupEvent.objects.all() + serializer_class = GroupEventSerializer pagination_class = CustomPagination @@ -176,103 +224,74 @@ class GroupImageViewSet(viewsets.ModelViewSet[GroupImage]): pagination_class = CustomPagination -class GroupViewSet(viewsets.ModelViewSet[Group]): - queryset = Group.objects.all() - serializer_class = GroupSerializer +class GroupMemberViewSet(viewsets.ModelViewSet[GroupMember]): + queryset = GroupMember.objects.all() + serializer_class = GroupMemberSerializer pagination_class = CustomPagination - 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: - 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}"} - 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) +class GroupResourceViewSet(viewsets.ModelViewSet[GroupResource]): + queryset = GroupResource.objects.all() + serializer_class = GroupResourceSerializer + pagination_class = CustomPagination - return Response(serializer.data, status=status.HTTP_200_OK) - def partial_update(self, request: Request, *args: str, **kwargs: int) -> Response: - group = self.queryset.filter(id=kwargs["pk"]).first() +class GroupTextViewSet(viewsets.ModelViewSet[GroupText]): + queryset = GroupText.objects.all() + serializer_class = GroupTextSerializer + pagination_class = CustomPagination - if group is None: - 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"}, - status.HTTP_401_UNAUTHORIZED, - ) - 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) +class GroupTopicViewSet(viewsets.ModelViewSet[GroupTopic]): + queryset = GroupTopic.objects.all() + serializer_class = GroupTopicSerializer + pagination_class = CustomPagination - def destroy(self, request: Request, *args: str, **kwargs: int) -> Response: - group = self.queryset.filter(id=kwargs["pk"]).first() - if group is None: - 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 - ) +class OrganizationApplicationViewSet(viewsets.ModelViewSet[OrganizationApplication]): + queryset = OrganizationApplication.objects.all() + serializer_class = OrganizationApplicationSerializer + pagination_class = CustomPagination -class OrganizationTaskViewSet(viewsets.ModelViewSet[OrganizationTask]): - queryset = OrganizationTask.objects.all() - serializer_class = OrganizationTaskSerializer +class OrganizationEventViewSet(viewsets.ModelViewSet[OrganizationEvent]): + queryset = OrganizationEvent.objects.all() + serializer_class = OrganizationEventSerializer pagination_class = CustomPagination -class OrganizationTopicViewSet(viewsets.ModelViewSet[OrganizationTopic]): - queryset = OrganizationTopic.objects.all() - serializer_class = OrganizationTopicSerializer +class OrganizationMemberViewSet(viewsets.ModelViewSet[OrganizationMember]): + queryset = OrganizationMember.objects.all() + serializer_class = OrganizationMemberSerializer pagination_class = CustomPagination -class GroupEventViewSet(viewsets.ModelViewSet[GroupEvent]): - queryset = GroupEvent.objects.all() - serializer_class = GroupEventSerializer - pagination_class = CustomPagination +class OrganizationImageViewSet(viewsets.ModelViewSet[OrganizationImage]): + queryset = OrganizationImage.objects.all() + serializer_class = OrganizationImageSerializer -class GroupMemberViewSet(viewsets.ModelViewSet[GroupMember]): - queryset = GroupMember.objects.all() - serializer_class = GroupMemberSerializer +class OrganizationResourceViewSet(viewsets.ModelViewSet[OrganizationResource]): + queryset = OrganizationResource.objects.all() + serializer_class = OrganizationResourceSerializer pagination_class = CustomPagination -class GroupResourceViewSet(viewsets.ModelViewSet[GroupResource]): - queryset = GroupResource.objects.all() - serializer_class = GroupResourceSerializer +class OrganizationTaskViewSet(viewsets.ModelViewSet[OrganizationTask]): + queryset = OrganizationTask.objects.all() + serializer_class = OrganizationTaskSerializer pagination_class = CustomPagination -class GroupTopicViewSet(viewsets.ModelViewSet[GroupTopic]): - queryset = GroupTopic.objects.all() - serializer_class = GroupTopicSerializer +class OrganizationTextViewSet(viewsets.ModelViewSet[OrganizationText]): + queryset = OrganizationText.objects.all() + serializer_class = OrganizationTextSerializer pagination_class = CustomPagination -class StatusViewSet(viewsets.ModelViewSet[Status]): - queryset = Status.objects.all() - serializer_class = StatusSerializer +class OrganizationTopicViewSet(viewsets.ModelViewSet[OrganizationTopic]): + queryset = OrganizationTopic.objects.all() + serializer_class = OrganizationTopicSerializer pagination_class = CustomPagination diff --git a/backend/events/admin.py b/backend/events/admin.py index 78472e867..276e7e2c3 100644 --- a/backend/events/admin.py +++ b/backend/events/admin.py @@ -14,7 +14,14 @@ Role, ) +# MARK: Main Tables + admin.site.register(Event) +admin.site.register(Format) +admin.site.register(Role) + +# MARK: Bridge Tables + admin.site.register(EventAttendee) admin.site.register(EventAttendeeStatus) admin.site.register(EventResource) @@ -23,5 +30,3 @@ admin.site.register(EventText) admin.site.register(EventTopic) admin.site.register(EventFormat) -admin.site.register(Format) -admin.site.register(Role) diff --git a/backend/events/factories.py b/backend/events/factories.py index 8200a8afc..80ab35ca5 100644 --- a/backend/events/factories.py +++ b/backend/events/factories.py @@ -15,6 +15,8 @@ Role, ) +# MARK: Main Tables + class EventFactory(factory.django.DjangoModelFactory): class Meta: @@ -58,6 +60,9 @@ class Meta: deprecation_date = factory.Faker("future_date", end_date="+30d") +# MARK: Bridge Tables + + class EventAttendeeFactory(factory.django.DjangoModelFactory): class Meta: model = EventAttendee @@ -68,19 +73,19 @@ class Meta: attendee_status = factory.SubFactory("events.factories.EventAttendeeStatusFactory") -class EventFormatFactory(factory.django.DjangoModelFactory): +class EventAttendeeStatusFactory(factory.django.DjangoModelFactory): class Meta: - model = EventFormat + model = EventAttendeeStatus - event_id = factory.SubFactory(EventFactory) - format_id = factory.SubFactory(FormatFactory) + status_name = factory.Faker("word") -class EventAttendeeStatusFactory(factory.django.DjangoModelFactory): +class EventFormatFactory(factory.django.DjangoModelFactory): class Meta: - model = EventAttendeeStatus + model = EventFormat - status_name = factory.Faker("word") + event_id = factory.SubFactory(EventFactory) + format_id = factory.SubFactory(FormatFactory) class EventResourceFactory(factory.django.DjangoModelFactory): diff --git a/backend/events/models.py b/backend/events/models.py index a36e9f883..8317857a3 100644 --- a/backend/events/models.py +++ b/backend/events/models.py @@ -9,6 +9,8 @@ from backend.mixins.models import CreationDeletionMixin +# MARK: Main Tables + class Event(CreationDeletionMixin): id = models.UUIDField(primary_key=True, default=uuid4, editable=False) @@ -23,8 +25,6 @@ class Event(CreationDeletionMixin): "content.Image", on_delete=models.CASCADE, blank=True, null=True ) type = models.CharField(max_length=255) - description = models.TextField(max_length=500) - get_involved_text = models.TextField(max_length=500) online_location_link = 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) @@ -65,6 +65,9 @@ def __str__(self) -> str: return self.name +# MARK: Bridge Tables + + class EventAttendee(models.Model): event_id = models.ForeignKey(Event, on_delete=models.CASCADE) user_id = models.ForeignKey("authentication.UserModel", on_delete=models.CASCADE) @@ -77,14 +80,6 @@ def __str__(self) -> str: return f"{self.user_id} - {self.event_id}" -class EventFormat(models.Model): - event_id = models.ForeignKey(Event, on_delete=models.CASCADE) - format_id = models.ForeignKey("Format", on_delete=models.CASCADE) - - def __str__(self) -> str: - return f"{self.id}" - - class EventAttendeeStatus(models.Model): id = models.IntegerField(primary_key=True) status_name = models.CharField(max_length=255) @@ -93,6 +88,14 @@ def __str__(self) -> str: return self.status_name +class EventFormat(models.Model): + event_id = models.ForeignKey(Event, on_delete=models.CASCADE) + format_id = models.ForeignKey("Format", on_delete=models.CASCADE) + + def __str__(self) -> str: + return f"{self.id}" + + class EventResource(models.Model): event_id = models.ForeignKey(Event, on_delete=models.CASCADE) resource_id = models.ForeignKey("content.Resource", on_delete=models.CASCADE) @@ -109,6 +112,14 @@ def __str__(self) -> str: return f"{self.id}" +class EventTag(models.Model): + event_id = models.ForeignKey(Event, on_delete=models.CASCADE) + tag_id = models.ForeignKey("content.Tag", on_delete=models.CASCADE) + + def __str__(self) -> str: + return f"{self.id}" + + class EventTask(models.Model): event_id = models.ForeignKey(Event, on_delete=models.CASCADE) task_id = models.ForeignKey("content.Task", on_delete=models.CASCADE) @@ -124,7 +135,7 @@ class EventText(models.Model): ) primary = models.BooleanField() description = models.TextField(max_length=500) - get_involved = models.TextField(max_length=500) + get_involved = models.TextField(max_length=500, blank=True) def __str__(self) -> str: return f"{self.id}" @@ -136,11 +147,3 @@ class EventTopic(models.Model): def __str__(self) -> str: return f"{self.id}" - - -class EventTag(models.Model): - event_id = models.ForeignKey(Event, on_delete=models.CASCADE) - tag_id = models.ForeignKey("content.Tag", on_delete=models.CASCADE) - - def __str__(self) -> str: - return f"{self.id}" diff --git a/backend/events/serializers.py b/backend/events/serializers.py index 5668edcf1..144c1f59a 100644 --- a/backend/events/serializers.py +++ b/backend/events/serializers.py @@ -28,6 +28,8 @@ Role, ) +# MARK: Main Tables + class EventSerializer(serializers.ModelSerializer[Event]): class Meta: @@ -68,21 +70,24 @@ def validate(self, data: Dict[str, Union[str, int]]) -> Dict[str, Union[str, int return data +# MARK: Bridge Tables + + class EventAttendeeSerializer(serializers.ModelSerializer[EventAttendee]): class Meta: model = EventAttendee fields = "__all__" -class EventFormatSerializer(serializers.ModelSerializer[EventFormat]): +class EventAttendeeStatusSerializer(serializers.ModelSerializer[EventAttendeeStatus]): class Meta: - model = EventFormat + model = EventAttendeeStatus fields = "__all__" -class EventAttendeeStatusSerializer(serializers.ModelSerializer[EventAttendeeStatus]): +class EventFormatSerializer(serializers.ModelSerializer[EventFormat]): class Meta: - model = EventAttendeeStatus + model = EventFormat fields = "__all__" @@ -98,6 +103,12 @@ class Meta: fields = "__all__" +class EventTagSerializer(serializers.ModelSerializer[EventTag]): + class Meta: + model = EventTag + fields = "__all__" + + class EventTaskSerializer(serializers.ModelSerializer[EventTask]): class Meta: model = EventTask @@ -114,9 +125,3 @@ class EventTopicSerializer(serializers.ModelSerializer[EventTopic]): class Meta: model = EventTopic fields = "__all__" - - -class EventTagSerializer(serializers.ModelSerializer[EventTag]): - class Meta: - model = EventTag - fields = "__all__" diff --git a/backend/events/urls.py b/backend/events/urls.py index bd38a731e..59ca90b80 100644 --- a/backend/events/urls.py +++ b/backend/events/urls.py @@ -6,15 +6,22 @@ app_name = "events" router = DefaultRouter() + +# MARK: Main Tables + router.register(r"events", views.EventViewSet) router.register(r"formats", views.FormatViewSet) router.register(r"roles", views.RoleViewSet) + +# MARK: Bridge Tables + router.register(r"event_attendees", views.EventAttendeeViewSet) router.register(r"event_formats", views.EventFormatViewSet) router.register(r"event_attendee_statuses", views.EventAttendeeStatusViewSet) router.register(r"event_resources", views.EventResourceViewSet) router.register(r"event_roles", views.EventRoleViewSet) router.register(r"event_tasks", views.EventTaskViewSet) +router.register(r"event_texts", views.EventTextViewSet) router.register(r"event_topics", views.EventTopicViewSet) urlpatterns = [ diff --git a/backend/events/views.py b/backend/events/views.py index e87273fc9..d1de501d4 100644 --- a/backend/events/views.py +++ b/backend/events/views.py @@ -33,6 +33,8 @@ RoleSerializer, ) +# MARK: Main Tables + class EventViewSet(viewsets.ModelViewSet[Event]): queryset = Event.objects.all() @@ -53,24 +55,27 @@ class RoleViewSet(viewsets.ModelViewSet[Role]): pagination_class = CustomPagination +# MARK: Bridge Tables + + class EventAttendeeViewSet(viewsets.ModelViewSet[EventAttendee]): queryset = EventAttendee.objects.all() serializer_class = EventAttendeeSerializer pagination_class = CustomPagination -class EventFormatViewSet(viewsets.ModelViewSet[EventFormat]): - queryset = EventFormat.objects.all() - serializer_class = EventFormatSerializer - pagination_class = CustomPagination - - class EventAttendeeStatusViewSet(viewsets.ModelViewSet[EventAttendeeStatus]): queryset = EventAttendeeStatus.objects.all() serializer_class = EventAttendeeStatusSerializer pagination_class = CustomPagination +class EventFormatViewSet(viewsets.ModelViewSet[EventFormat]): + queryset = EventFormat.objects.all() + serializer_class = EventFormatSerializer + pagination_class = CustomPagination + + class EventResourceViewSet(viewsets.ModelViewSet[EventResource]): queryset = EventResource.objects.all() serializer_class = EventResourceSerializer diff --git a/backend/fixtures/iso_code_map.json b/backend/fixtures/iso_code_map.json new file mode 100644 index 000000000..9e221b84f --- /dev/null +++ b/backend/fixtures/iso_code_map.json @@ -0,0 +1,9 @@ +[ + { + "model": "content.isocodemap", + "pk": 1, + "fields": { + "code": "en" + } + } +] diff --git a/backend/fixtures/superuser.json b/backend/fixtures/superuser.json index 9000d4e0a..1b22a93a1 100644 --- a/backend/fixtures/superuser.json +++ b/backend/fixtures/superuser.json @@ -16,11 +16,12 @@ "description": "", "verified": false, "verification_method": "", - "verification_partner": "7664552d-e9cb-49f8-9683-a58acdd4f504", + "verification_partner": null, "email": "admin@activist.org", "is_high_risk": false, "is_active": true, "is_admin": false, + "is_confirmed": true, "groups": [], "user_permissions": [] } diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt index 24b25b62f..6082d027f 100644 --- a/backend/requirements-dev.txt +++ b/backend/requirements-dev.txt @@ -51,7 +51,7 @@ django-stubs-ext==4.2.5 # via # -r requirements.txt # django-stubs -djangorestframework==3.15.1 +djangorestframework==3.15.2 # via # -r requirements.txt # drf-spectacular @@ -183,7 +183,7 @@ uritemplate==4.1.1 # via # -r requirements.txt # drf-spectacular -urllib3==2.0.7 +urllib3==2.2.2 # via # -r requirements.txt # requests diff --git a/backend/requirements.txt b/backend/requirements.txt index 0a40235fb..23db7769c 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -32,7 +32,7 @@ django-stubs-ext==4.2.5 # via # -r requirements.in # django-stubs -djangorestframework==3.15.1 +djangorestframework==3.15.2 # via # -r requirements.in # drf-spectacular @@ -92,7 +92,7 @@ typing-extensions==4.8.0 # mypy uritemplate==4.1.1 # via drf-spectacular -urllib3==2.0.7 +urllib3==2.2.2 # via # requests # types-requests diff --git a/docker-compose.yml b/docker-compose.yml index 23ecf97f9..20c4bf700 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,8 @@ services: command: sh -c "python manage.py makemigrations && python manage.py migrate && python manage.py loaddata fixtures/superuser.json && + python manage.py loaddata fixtures/status_types.json && + python manage.py loaddata fixtures/iso_code_map.json && python manage.py runserver 0.0.0.0:${BACKEND_PORT}" ports: - "${BACKEND_PORT}:${BACKEND_PORT}" diff --git a/frontend/components/card/CardAbout.vue b/frontend/components/card/CardAbout.vue deleted file mode 100644 index d14df78c6..000000000 --- a/frontend/components/card/CardAbout.vue +++ /dev/null @@ -1,256 +0,0 @@ - - - diff --git a/frontend/components/card/CardConnect.vue b/frontend/components/card/CardConnect.vue index e958ca1f5..b790cb187 100644 --- a/frontend/components/card/CardConnect.vue +++ b/frontend/components/card/CardConnect.vue @@ -8,13 +8,13 @@ class="cursor-pointer break-all rounded-lg p-1 text-light-text transition-all hover:text-light-distinct-text dark:text-dark-text dark:hover:text-dark-distinct-text" > import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue"; +import type { Organization } from "~/types/entities/organization"; import { IconMap } from "~/types/icon-map"; const props = defineProps<{ - socialLinks?: string[]; - userIsAdmin?: boolean; + pageType: "organization" | "group" | "event" | "other"; }>(); +const { userIsSignedIn } = useUser(); +const paramsID = useRoute().params.id; +const paramsIDGroup = useRoute().params.groupID; + +const id = typeof paramsID === "string" ? paramsID : undefined; +const idGroup = typeof paramsIDGroup === "string" ? paramsIDGroup : undefined; + +const organizationStore = useOrganizationStore(); +let organization: Organization; +const group = useGroupStore(); +const event = useEventStore(); + +if (props.pageType == "organization") { + await organizationStore.fetchByID(id); + organization = organizationStore.organization; +} else if (props.pageType == "group") { + await group.fetchByID(idGroup); +} else if (props.pageType == "event") { + await event.fetchByID(id); +} + const editModeEnabled = ref(false); -const socialLinksRef = computed(() => props.socialLinks); +const socialLinksRef = computed(() => { + if (props.pageType == "organization") { + return organization.socialLinks; + } else if (props.pageType == "group") { + return group.socialLinks; + } else if (props.pageType == "event") { + return event.socialLinks; + } else { + return [""]; + } +}); const toggleEditMode = () => { editModeEnabled.value = !editModeEnabled.value; diff --git a/frontend/components/card/CardDetails.vue b/frontend/components/card/CardDetails.vue index d0784750d..935b032f2 100644 --- a/frontend/components/card/CardDetails.vue +++ b/frontend/components/card/CardDetails.vue @@ -8,15 +8,15 @@ {{ $t("components.card-details.header") }} - @@ -44,21 +44,12 @@ diff --git a/frontend/components/card/CardOrgApplicationVote.vue b/frontend/components/card/CardOrgApplicationVote.vue index f26d29082..634931469 100644 --- a/frontend/components/card/CardOrgApplicationVote.vue +++ b/frontend/components/card/CardOrgApplicationVote.vue @@ -55,8 +55,8 @@ diff --git a/frontend/components/card/about/CardAboutGroup.vue b/frontend/components/card/about/CardAboutGroup.vue new file mode 100644 index 000000000..d6804c5c4 --- /dev/null +++ b/frontend/components/card/about/CardAboutGroup.vue @@ -0,0 +1,131 @@ + + + diff --git a/frontend/components/card/about/CardAboutOrganization.vue b/frontend/components/card/about/CardAboutOrganization.vue new file mode 100644 index 000000000..3b5d368d0 --- /dev/null +++ b/frontend/components/card/about/CardAboutOrganization.vue @@ -0,0 +1,130 @@ + + + diff --git a/frontend/components/card/discussion/CardDiscussion.vue b/frontend/components/card/discussion/CardDiscussion.vue index 04d14947d..7e1f1fdde 100644 --- a/frontend/components/card/discussion/CardDiscussion.vue +++ b/frontend/components/card/discussion/CardDiscussion.vue @@ -78,7 +78,7 @@ diff --git a/frontend/components/card/get-involved/CardGetInvolvedGroup.vue b/frontend/components/card/get-involved/CardGetInvolvedGroup.vue new file mode 100644 index 000000000..197e6f538 --- /dev/null +++ b/frontend/components/card/get-involved/CardGetInvolvedGroup.vue @@ -0,0 +1,63 @@ + + + diff --git a/frontend/components/card/get-involved/CardGetInvolvedOrganization.vue b/frontend/components/card/get-involved/CardGetInvolvedOrganization.vue new file mode 100644 index 000000000..e6b8baa3b --- /dev/null +++ b/frontend/components/card/get-involved/CardGetInvolvedOrganization.vue @@ -0,0 +1,84 @@ + + + diff --git a/frontend/components/card/search-result/CardSearchResult.vue b/frontend/components/card/search-result/CardSearchResult.vue index d1e5ba09e..1b989cd14 100644 --- a/frontend/components/card/search-result/CardSearchResult.vue +++ b/frontend/components/card/search-result/CardSearchResult.vue @@ -166,7 +166,7 @@ :link="onlineLocation" label="components.meta-tag-video.view-video" /> - +
@@ -177,7 +177,7 @@ :link="onlineLocation" label="components.meta-tag-video.view-video" /> - +
import { useLinkURL } from "~/composables/useLinkURL"; -import type { Event } from "~/types/event"; -import type { Group } from "~/types/group"; +import type { User } from "~/types/auth/user"; +import type { Resource } from "~/types/content/resource"; +import type { Group } from "~/types/entities/group"; +import type { Organization } from "~/types/entities/organization"; +import type { Event } from "~/types/events/event"; import { IconMap } from "~/types/icon-map"; -import type { Organization } from "~/types/organization"; -import type { Resource } from "~/types/resource"; -import type { User } from "~/types/user"; const props = defineProps<{ organization?: Organization; diff --git a/frontend/components/card/search-result/CardSearchResultEvent.vue b/frontend/components/card/search-result/CardSearchResultEvent.vue index 7fd33e7c4..d140b9105 100644 --- a/frontend/components/card/search-result/CardSearchResultEvent.vue +++ b/frontend/components/card/search-result/CardSearchResultEvent.vue @@ -7,7 +7,7 @@ diff --git a/frontend/components/form/FormTextField.vue b/frontend/components/form/text/FormTextInput.vue similarity index 100% rename from frontend/components/form/FormTextField.vue rename to frontend/components/form/text/FormTextInput.vue diff --git a/frontend/components/header/HeaderAppPage.vue b/frontend/components/header/HeaderAppPage.vue index 8d6233003..ec86b9a20 100644 --- a/frontend/components/header/HeaderAppPage.vue +++ b/frontend/components/header/HeaderAppPage.vue @@ -1,10 +1,5 @@ diff --git a/frontend/components/modal/edit/about/ModalEditAboutGroup.vue b/frontend/components/modal/edit/about/ModalEditAboutGroup.vue new file mode 100644 index 000000000..c95debec1 --- /dev/null +++ b/frontend/components/modal/edit/about/ModalEditAboutGroup.vue @@ -0,0 +1,90 @@ +