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") }}
-