diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fd0834200..374b303ad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -216,7 +216,7 @@ git remote add upstream https://github.com/activist-org/activist.git 6. You can visit to see the development frontend once the container is up and running. From there click `View organizations` or `View events` to explore the platform. -7. To view the backend admin UI and Swagger UI, visit and respectively. +7. To view the backend admin UI and Swagger UI, visit and respectively. 8. If you'd like to sign in to the frontend via or the Django admin panel via , then you can use the fixtures `admin` user with the password `admin`. @@ -238,11 +238,21 @@ The frontend currently uses [Yarn 1](https://classic.yarnpkg.com/lang/en/docs/in # In the root activist directory: cd frontend yarn install -yarn run dev +yarn run dev:local ``` You can then visit http://localhost:3000/ to see the development frontend build once the server is up and running. +You can also build the production version locally: + +```bash +# In activist/frontend: +yarn build:local + +# Run the production build: +node .output/server/index.mjs +``` +

diff --git a/README.md b/README.md index 09adb5522..5888ccd9b 100644 --- a/README.md +++ b/README.md @@ -196,9 +196,16 @@ git remote add upstream https://github.com/activist-org/activist.git # docker compose --env-file .env.dev down ``` + Sometimes changes to the database can cause the database population to fail in your environment. If this happens, you can destroy the deployment and rebuild it: + + ```bash + # Destroy your current docker-compose deployment: + docker-compose rm -f -v --env-file .env.dev + ``` + 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. -7. To view the backend admin UI and Swagger UI, visit and respectively. +7. To view the backend admin UI and Swagger UI, visit and respectively. 8. If you'd like to sign in to the frontend via or the Django admin panel via , then you can use the fixtures `admin` user with the password `admin`. diff --git a/backend/backend/management/commands/populate_db.py b/backend/backend/management/commands/populate_db.py index 9e0e90d74..f4649a2b8 100644 --- a/backend/backend/management/commands/populate_db.py +++ b/backend/backend/management/commands/populate_db.py @@ -10,7 +10,6 @@ from entities.factories import ( GroupFactory, GroupTextFactory, - OrganizationEventFactory, OrganizationFactory, OrganizationTextFactory, ) @@ -60,42 +59,47 @@ def handle(self, *args: str, **options: Unpack[Options]) -> None: UserTopicFactory(user_id=user, topic_id=user_topic) for o in range(num_orgs_per_user): + for e in range(num_events_per_org): + event_type = random.choice(["learn", "action"]) + event_type_verb = ( + "Learning about" + if event_type == "learn" + else "Fighting for" + ) + + event_texts = EventTextFactory(iso="en", primary=True) + + user_org_event = EventFactory( + name=f"{user_topic.name} Event o{o}:e{e}", + tagline=f"{event_type_verb} {user_topic.name}", + type=event_type, + texts=event_texts, + created_by=user, + ) + + org_texts = OrganizationTextFactory(iso="wt", primary=True) + user_org = OrganizationFactory( created_by=user, org_name=f"organization_u{u}_o{o}", + texts=org_texts, name=f"{user_topic.name} Organization", tagline=f"Fighting for {user_topic.name.lower()}", ) - OrganizationTextFactory(org_id=user_org, iso="wt", primary=True) + user_org.events.set([user_org_event]) for g in range(num_groups_per_org): - user_org_group = GroupFactory( + group_texts = GroupTextFactory(iso="en", primary=True) + + _ = GroupFactory( created_by=user, group_name=f"group_u{u}_o{o}_g{g}", org_id=user_org, + texts=group_texts, name=f"{user_topic.name} Group", ) - GroupTextFactory( - group_id=user_org_group, iso="en", primary=True - ) - - for e in range(num_events_per_org): - user_org_event = EventFactory( - created_by=user, - name=f"{user_topic.name} Event o{o}:e{e}", - type=random.choice(["learn", "action"]), - ) - - EventTextFactory( - event_id=user_org_event, iso="en", primary=True - ) - - OrganizationEventFactory( - org_id=user_org, event_id=user_org_event - ) - self.stdout.write( self.style.ERROR( f"Number of users created: {num_users}\n" diff --git a/backend/content/factories.py b/backend/content/factories.py index 3662fe7cd..3f3a417d5 100644 --- a/backend/content/factories.py +++ b/backend/content/factories.py @@ -117,7 +117,7 @@ class Meta: created_by = factory.SubFactory("authentication.factories.UserFactory") name = factory.Faker("name") description = factory.Faker("text") - location_id = factory.SubFactory(EntityLocationFactory) + location = factory.SubFactory(EntityLocationFactory) url = factory.Faker("url") is_private = factory.Faker("boolean") terms_checked = factory.Faker("boolean") diff --git a/backend/content/models.py b/backend/content/models.py index b25b22ee6..688c92c60 100644 --- a/backend/content/models.py +++ b/backend/content/models.py @@ -68,7 +68,7 @@ class Resource(models.Model): name = models.CharField(max_length=255) description = models.TextField(max_length=500) category = models.CharField(max_length=255, blank=True) - location_id = models.OneToOneField( + location = models.OneToOneField( "content.Location", on_delete=models.CASCADE, null=False, blank=False ) url = models.URLField(max_length=255) diff --git a/backend/entities/admin.py b/backend/entities/admin.py index 75551f558..0f3347b1a 100644 --- a/backend/entities/admin.py +++ b/backend/entities/admin.py @@ -62,7 +62,7 @@ class OrganizationAdmin(admin.ModelAdmin[Organization]): class OrganizationTextAdmin(admin.ModelAdmin[OrganizationText]): - list_display = ["id", "org_id"] + list_display = ["id"] admin.site.register(Group, GroupAdmin) diff --git a/backend/entities/factories.py b/backend/entities/factories.py index 974fa064c..559c684a0 100644 --- a/backend/entities/factories.py +++ b/backend/entities/factories.py @@ -37,7 +37,7 @@ class Meta: terms_checked = factory.Faker("boolean") status = factory.SubFactory("entities.factories.StatusTypeFactory", name="Active") is_high_risk = factory.Faker("boolean") - location_id = factory.SubFactory("content.factories.EntityLocationFactory") + location = factory.SubFactory("content.factories.EntityLocationFactory") acceptance_date = factory.LazyFunction( lambda: datetime.datetime.now(tz=datetime.timezone.utc) ) @@ -56,7 +56,7 @@ class Meta: ) terms_checked = factory.Faker("boolean") category = factory.Faker("word") - location_id = factory.SubFactory("content.factories.EntityLocationFactory") + location = factory.SubFactory("content.factories.EntityLocationFactory") # MARK: Bridge Tables @@ -99,8 +99,7 @@ class GroupTextFactory(factory.django.DjangoModelFactory): class Meta: model = GroupText - group_id = factory.SubFactory(GroupFactory) - iso = factory.Faker("word") + iso = "en" primary = factory.Faker("boolean") description = factory.Faker(provider="text", locale="la", max_nb_chars=1000) get_involved = factory.Faker(provider="text", locale="la") @@ -186,7 +185,6 @@ class OrganizationTextFactory(factory.django.DjangoModelFactory): class Meta: model = OrganizationText - org_id = factory.SubFactory(OrganizationFactory) iso = "en" primary = factory.Faker("boolean") description = factory.Faker(provider="text", locale="la", max_nb_chars=1000) diff --git a/backend/entities/models.py b/backend/entities/models.py index a012db902..b18c0baa4 100644 --- a/backend/entities/models.py +++ b/backend/entities/models.py @@ -25,7 +25,7 @@ class Organization(models.Model): icon_url = models.OneToOneField( "content.Image", on_delete=models.CASCADE, blank=True, null=True ) - location_id = models.OneToOneField( + location = models.OneToOneField( "content.Location", on_delete=models.CASCADE, null=False, blank=False ) get_involved_url = models.URLField(blank=True) @@ -41,9 +41,11 @@ class Organization(models.Model): status_updated = models.DateTimeField(auto_now=True, null=True) acceptance_date = models.DateTimeField(blank=True, null=True) deletion_date = models.DateTimeField(blank=True, null=True) - org_text = models.ForeignKey( + texts = models.ForeignKey( "OrganizationText", on_delete=models.CASCADE, blank=True, null=True ) + events = models.ManyToManyField("events.Event", blank=True) + resources = models.ManyToManyField("content.Resource", blank=True) def __str__(self) -> str: return self.name @@ -56,13 +58,18 @@ class Group(models.Model): group_name = models.CharField(max_length=255) name = models.CharField(max_length=255) tagline = models.CharField(max_length=255, blank=True) - location_id = models.OneToOneField( + location = models.OneToOneField( "content.Location", on_delete=models.CASCADE, null=False, blank=False ) category = models.CharField(max_length=255) get_involved_url = models.URLField(blank=True) terms_checked = models.BooleanField(default=False) creation_date = models.DateTimeField(auto_now_add=True) + texts = models.ForeignKey( + "GroupText", on_delete=models.CASCADE, blank=True, null=True + ) + events = models.ManyToManyField("events.Event", blank=True) + resources = models.ManyToManyField("content.Resource", blank=True) def __str__(self) -> str: return self.name @@ -135,7 +142,7 @@ def __str__(self) -> str: class GroupText(models.Model): - group_id = models.ForeignKey(Group, on_delete=models.CASCADE) + group_id = models.ForeignKey(Group, on_delete=models.CASCADE, null=True) iso = models.CharField(max_length=3, choices=ISO_CHOICES) primary = models.BooleanField(default=False) description = models.TextField(max_length=500) @@ -243,6 +250,7 @@ def __str__(self) -> str: class OrganizationTask(models.Model): + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) org_id = models.ForeignKey(Organization, on_delete=models.CASCADE) task_id = models.ForeignKey("content.Task", on_delete=models.CASCADE) group_id = models.ForeignKey( @@ -254,7 +262,7 @@ def __str__(self) -> str: class OrganizationText(models.Model): - org_id = models.ForeignKey(Organization, on_delete=models.CASCADE) + org_id = models.ForeignKey(Organization, on_delete=models.CASCADE, null=True) iso = models.CharField(max_length=3, choices=ISO_CHOICES) primary = models.BooleanField(default=False) description = models.TextField(max_length=2500) diff --git a/backend/entities/serializers.py b/backend/entities/serializers.py index 3e18b9a30..00b0f37c5 100644 --- a/backend/entities/serializers.py +++ b/backend/entities/serializers.py @@ -6,6 +6,7 @@ from rest_framework import serializers +from content.serializers import LocationSerializer, ResourceSerializer from events.serializers import EventSerializer from .models import ( @@ -38,23 +39,55 @@ # MARK: Main Tables +class GroupTextSerializer(serializers.ModelSerializer[GroupText]): + class Meta: + model = GroupText + fields = "__all__" + + class GroupSerializer(serializers.ModelSerializer[Group]): + texts = GroupTextSerializer() + location = LocationSerializer(read_only=True) + events = EventSerializer(many=True, read_only=True) + resources = ResourceSerializer(many=True, read_only=True) + class Meta: model = Group + extra_kwargs = { + "created_by": {"read_only": True}, + } + fields = "__all__" + def validate(self, data: dict[str, Any]) -> dict[str, Any]: + if data.get("terms_checked") is False: + raise serializers.ValidationError( + "You must accept the terms of service to create a group." + ) + + return data + + def create(self, validated_data: dict[str, Any]) -> Group: + group = Group.objects.create(**validated_data) -class OrganizationTextSerializer(serializers.ModelSerializer[OrganizationText]): - # mypy thinks a generic type argument is needed for StringRelatedField. - org_id = serializers.StringRelatedField(source="org_id.id") # type: ignore[var-annotated] + if group: + texts = GroupText.objects.create(group_id=group) + group.texts = texts + return group + + +class OrganizationTextSerializer(serializers.ModelSerializer[OrganizationText]): class Meta: model = OrganizationText fields = "__all__" class OrganizationSerializer(serializers.ModelSerializer[Organization]): - org_text = OrganizationTextSerializer(read_only=True) + texts = OrganizationTextSerializer() + location = LocationSerializer(read_only=True) + events = EventSerializer(many=True, read_only=True) + resources = ResourceSerializer(many=True, read_only=True) class Meta: model = Organization @@ -65,22 +98,7 @@ class Meta: "acceptance_date": {"read_only": True}, } - fields = [ - "id", - "created_by", - "org_name", - "name", - "tagline", - "icon_url", - "location_id", - "get_involved_url", - "terms_checked", - "is_high_risk", - "status", - "status_updated", - "acceptance_date", - "org_text", - ] + fields = "__all__" def validate(self, data: dict[str, Any]) -> dict[str, Any]: if data.get("terms_checked") is False: @@ -91,14 +109,11 @@ def validate(self, data: dict[str, Any]) -> dict[str, Any]: return data 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 - ) - org.org_text = org_text + if org: + texts = OrganizationText.objects.create(org_id=org) + org.texts = texts return org @@ -148,12 +163,6 @@ class Meta: fields = "__all__" -class GroupTextSerializer(serializers.ModelSerializer[GroupText]): - class Meta: - model = GroupText - fields = "__all__" - - class GroupTopicSerializer(serializers.ModelSerializer[GroupTopic]): class Meta: model = GroupTopic diff --git a/backend/events/factories.py b/backend/events/factories.py index 5e3a1d2b5..3f9d48ba5 100644 --- a/backend/events/factories.py +++ b/backend/events/factories.py @@ -30,7 +30,8 @@ class Meta: tagline = factory.Faker("word") type = random.choice(["learn", "action"]) online_location_link = factory.Faker("url") - offline_location_id = factory.SubFactory("content.factories.EventLocationFactory") + offline_location = factory.SubFactory("content.factories.EventLocationFactory") + is_private = factory.Faker("boolean") start_time = factory.LazyFunction( lambda: datetime.datetime.now(tz=datetime.timezone.utc) ) @@ -49,7 +50,6 @@ class Meta: + datetime.timedelta(days=30), ] ) - is_private = factory.Faker("boolean") class FormatFactory(factory.django.DjangoModelFactory): @@ -139,8 +139,7 @@ class EventTextFactory(factory.django.DjangoModelFactory): class Meta: model = EventText - event_id = factory.SubFactory(EventFactory) - iso = factory.Faker("word") + iso = "en" primary = factory.Faker("boolean") description = factory.Faker(provider="text", locale="la", max_nb_chars=1000) get_involved = factory.Faker(provider="text", locale="la") diff --git a/backend/events/models.py b/backend/events/models.py index 904cf2591..709127306 100644 --- a/backend/events/models.py +++ b/backend/events/models.py @@ -25,7 +25,7 @@ class Event(models.Model): ) type = models.CharField(max_length=255) online_location_link = models.CharField(max_length=255, blank=True) - offline_location_id = models.OneToOneField( + offline_location = models.OneToOneField( "content.Location", on_delete=models.CASCADE, null=False, blank=False ) get_involved_url = models.URLField(blank=True) @@ -34,9 +34,10 @@ class Event(models.Model): end_time = models.DateTimeField() creation_date = models.DateTimeField(auto_now_add=True) deletion_date = models.DateTimeField(blank=True, null=True) - event_text = models.ForeignKey( + texts = models.ForeignKey( "EventText", on_delete=models.CASCADE, blank=True, null=True ) + resources = models.ManyToManyField("content.Resource", blank=True) def __str__(self) -> str: return self.name @@ -155,10 +156,10 @@ def __str__(self) -> str: class EventText(models.Model): - event_id = models.ForeignKey(Event, on_delete=models.CASCADE) + event_id = models.ForeignKey(Event, on_delete=models.CASCADE, null=True) iso = models.CharField(max_length=3, choices=ISO_CHOICES) - primary = models.BooleanField() - description = models.TextField(max_length=500) + primary = models.BooleanField(default=False) + description = models.TextField(max_length=2500) get_involved = models.TextField(max_length=500, blank=True) def __str__(self) -> str: diff --git a/backend/events/serializers.py b/backend/events/serializers.py index f88af3d4c..6f5d3a75b 100644 --- a/backend/events/serializers.py +++ b/backend/events/serializers.py @@ -8,6 +8,7 @@ from django.utils.translation import gettext as _ from rest_framework import serializers +from content.serializers import LocationSerializer, ResourceSerializer from utils.utils import ( validate_creation_and_deletion_dates, validate_creation_and_deprecation_dates, @@ -40,7 +41,9 @@ class Meta: class EventSerializer(serializers.ModelSerializer[Event]): - event_text = EventTextSerializer() + texts = EventTextSerializer() + offline_location = LocationSerializer(read_only=True) + resources = ResourceSerializer(many=True, read_only=True) class Meta: model = Event @@ -49,20 +52,7 @@ class Meta: "created_by": {"read_only": True}, } - fields = [ - "id", - "created_by", - "name", - "tagline", - "icon_url", - "type", - "online_location_link", - "offline_location_id", - "is_private", - "start_time", - "end_time", - "event_text", - ] + fields = "__all__" 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 @@ -73,17 +63,19 @@ def validate(self, data: Dict[str, Union[str, int]]) -> Dict[str, Union[str, int validate_creation_and_deletion_dates(data) + if data.get("terms_checked") is False: + raise serializers.ValidationError( + "You must accept the terms of service to create an event." + ) + 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 = EventText.objects.create( - event_id=event, description=description - ) - event.event_text = event_text + if event: + event_text = EventText.objects.create(event_id=event) + event.texts = event_text return event diff --git a/frontend/components/card/CardDetails.vue b/frontend/components/card/CardDetails.vue index 077fc2b9d..d8468dce0 100644 --- a/frontend/components/card/CardDetails.vue +++ b/frontend/components/card/CardDetails.vue @@ -8,19 +8,9 @@ {{ $t("components.card_details.header") }} -
@@ -31,14 +21,14 @@ /> @@ -48,10 +38,10 @@ :label="$t('components.card_details.attending')" /> --> - +
@@ -60,16 +50,19 @@ diff --git a/frontend/components/card/about/CardAboutEvent.vue b/frontend/components/card/about/CardAboutEvent.vue index 4c52f1bb7..d37ce990c 100644 --- a/frontend/components/card/about/CardAboutEvent.vue +++ b/frontend/components/card/about/CardAboutEvent.vue @@ -21,7 +21,7 @@ 'line-clamp-2': !expandText, }" > - {{ event.description }} + {{ event.texts.description }}