diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 45a702c9d..7c977ce57 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,6 +17,7 @@ If you have questions or would like to communicate with the team, please [join u - [Learning the tech stack](#learning-the-tech) - [Development environment](#dev-env) - [Style guide](#style-guide) +- [Testing](#testing) - [Linting](#linting) - [Issues and projects](#issues-projects) - [Bug reports](#bug-reports) @@ -303,6 +304,35 @@ From there you'll be able to visit http://localhost:6006/ to view the documentat Please see the [activist style guide](https://github.com/activist-org/activist/blob/main/STYLEGUIDE.md) for details about how to follow the code style for the project. We made these guidelines to assure that we as a community write clean, cohesive code that's easy to write and review. Suggestions for the style guide are welcome. + + +## Testing [`⇧`](#contents) + +### Backend + +Please run the following commands from the project root to test the backend: + +```bash +# Start the Docker container: +docker compose --env-file .env.dev up backend --build -d # -d to hide logs + +# Enter the backend container: +docker exec -it django_backend sh + +# Run backend tests: +pytest + +# Once tests are finished: +exit +``` + +### Frontend + +Running frontend tests locally is currently WIP. + +> [!NOTE] +> When working on the frontend, activist recommends manual typechecking. From within the `frontend` directory run `yarn run postinstall` followed by `yarn nuxi typecheck` to confirm your changes are type-safe. Existing TS errors may be ignored. PRs to fix these are always welcome! + ## Linting [`⇧`](#contents) @@ -405,9 +435,6 @@ When making a contribution, adhering to the [GitHub flow](https://docs.github.co git pull --rebase upstream ``` -> [!NOTE] -> When working on the frontend, activist recommends manual typechecking. From within the `frontend` directory run `yarn run postinstall` followed by `yarn nuxi typecheck` to confirm your changes are type-safe. Existing TS errors may be ignored. PRs to fix these are always welcome! - 6. Push your topic branch up to your fork: ```bash diff --git a/README.md b/README.md index 60c948dfa..db66a4ce4 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ git remote add upstream https://github.com/activist-org/activist.git ```bash docker compose --env-file .env.dev up - # Or with new dependencies: + # Or with new dependencies or backend model changes: # docker compose --env-file .env.dev up --build # And to stop the containers when you're done working: diff --git a/backend/authentication/factories.py b/backend/authentication/factories.py index fd8a9fcd0..e833d3782 100644 --- a/backend/authentication/factories.py +++ b/backend/authentication/factories.py @@ -60,8 +60,6 @@ def verification_partner( if not create: # Simple build, do nothing. return - if extracted: - pass # MARK: Bridge Tables diff --git a/backend/authentication/models.py b/backend/authentication/models.py index fec4f9e72..5f1f3d18b 100644 --- a/backend/authentication/models.py +++ b/backend/authentication/models.py @@ -56,7 +56,7 @@ def create_user( class SupportEntityType(models.Model): - id = models.IntegerField(primary_key=True) + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) name = models.CharField(max_length=255) def __str__(self) -> str: diff --git a/backend/authentication/tests.py b/backend/authentication/tests.py index 824549ab2..5756943bd 100644 --- a/backend/authentication/tests.py +++ b/backend/authentication/tests.py @@ -24,7 +24,9 @@ import uuid -@pytest.mark.django_db +pytestmark = pytest.mark.django_db + + def test_str_methods() -> None: support_entity_type = SupportEntityTypeFactory.build() support = SupportFactory.build() @@ -41,7 +43,6 @@ def test_str_methods() -> None: assert str(user_topic) == str(user_topic.id) -@pytest.mark.django_db def test_signup(client: Client) -> None: """ Test the signup function. @@ -152,14 +153,13 @@ def test_signup(client: Client) -> None: assert user.verification_code is None -@pytest.mark.django_db def test_login(client: Client) -> None: """ Test login view. Scenarios: 1. User that signed up with email, that has not confirmed their email - 2. User that signed up with email, confimred email address. Is logged in successfully + 2. User that signed up with email, confirmed email address. Is logged in successfully 3. User exists but password is incorrect 4. User does not exists and tries to login """ @@ -174,7 +174,7 @@ def test_login(client: Client) -> None: ) assert response.status_code == 400 - # 2. User that signed up with email, confimred email address. Is logged in successfully + # 2. User that signed up with email, confirmed email address. Is logged in successfully user.is_confirmed = True user.save() response = client.post( @@ -204,7 +204,6 @@ def test_login(client: Client) -> None: assert response.status_code == 400 -@pytest.mark.django_db def test_pwreset(client: Client) -> None: """ Test password reset view. diff --git a/backend/backend/custom_settings.py b/backend/backend/custom_settings.py index 79419fc02..7e2e62e40 100644 --- a/backend/backend/custom_settings.py +++ b/backend/backend/custom_settings.py @@ -4,6 +4,7 @@ please check the path: backend/backend/settings.py """ -# Pagination settings +# MARK: Pagination + PAGINATION_PAGE_SIZE = 20 PAGINATION_MAX_PAGE_SIZE = 100 diff --git a/backend/backend/exception_handler.py b/backend/backend/exception_handler.py index aeaca091b..e74bd8d0c 100644 --- a/backend/backend/exception_handler.py +++ b/backend/backend/exception_handler.py @@ -8,7 +8,7 @@ def bad_request_logger(exception: Any, context: dict[str, Any]) -> Response | None: - # Get the DRF exception handler standard error response + # Get the DRF exception handler standard error response. response = exception_handler(exception, context) if response is not None: diff --git a/backend/backend/management/commands/populate_db.py b/backend/backend/management/commands/populate_db.py index e9f78415f..4791ec627 100644 --- a/backend/backend/management/commands/populate_db.py +++ b/backend/backend/management/commands/populate_db.py @@ -4,28 +4,58 @@ from django.core.management.base import BaseCommand from authentication.factories import UserFactory +from authentication.models import UserModel +from entities.factories import GroupFactory, OrganizationFactory +from entities.models import Group, Organization +from events.factories import EventFactory +from events.models import Event class Options(TypedDict): users: int + orgs: int + groups: int + events: int +# ATTN: We're not actually putting texts into the DB. class Command(BaseCommand): help = "Populate the database with dummy data" def add_arguments(self, parser: ArgumentParser) -> None: - parser.add_argument("--users", type=int, default=100) + parser.add_argument("--users", type=int, default=10) + parser.add_argument("--orgs", type=int, default=10) + parser.add_argument("--groups", type=int, default=10) + parser.add_argument("--events", type=int, default=10) def handle(self, *args: str, **options: Unpack[Options]) -> None: - number_of_users = options["users"] + number_of_users = options.get("users") + number_of_orgs = options.get("orgs") + number_of_groups = options.get("groups") + number_of_events = options.get("events") + + # Clear all tables before creating new data. + UserModel.objects.exclude(username="admin").delete() + Organization.objects.all().delete() + Group.objects.all().delete() + Event.objects.all().delete() + try: - UserFactory.create_batch(number_of_users) + UserFactory.create_batch(size=number_of_users) + OrganizationFactory.create_batch(size=number_of_orgs) + GroupFactory.create_batch(size=number_of_groups) + EventFactory.create_batch(size=number_of_events) self.stdout.write( - self.style.ERROR(f"Number of users created: {number_of_users}") + self.style.ERROR( + f"Number of users created: {number_of_users}\n" + f"Number of organizations created: {number_of_orgs}\n" + f"Number of groups created: {number_of_groups}\n" + f"Number of events created: {number_of_events}\n" + ) ) except Exception as error: self.stdout.write( self.style.ERROR( - f"An error occured during the creation of dummy data: {error}" + f"An error occurred during the creation of dummy data: {error}" ) ) diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 0d7b8a701..c2f8f7237 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -165,7 +165,7 @@ EMAIL_PORT = os.getenv("EMAIL_PORT") EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER") EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") -EMAIL_USE_TLS = bool(os.getenv("EMAIL_USE_TLS") == "True") +EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS") == "True" # DEVELOPMENT ONLY EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" diff --git a/backend/backend/tests/unit/conftest.py b/backend/backend/tests/unit/conftest.py index 12e287b8f..aa891de02 100644 --- a/backend/backend/tests/unit/conftest.py +++ b/backend/backend/tests/unit/conftest.py @@ -4,5 +4,4 @@ @pytest.fixture def api_client() -> APIClient: - client = APIClient() - return client + return APIClient() diff --git a/backend/content/factories.py b/backend/content/factories.py index 7b6885af4..be8638570 100644 --- a/backend/content/factories.py +++ b/backend/content/factories.py @@ -24,8 +24,12 @@ class Meta: url = factory.Faker("url") is_private = factory.Faker("boolean") created_by = factory.SubFactory("authentication.factories.UserFactory") - creation_date = factory.LazyFunction(datetime.datetime.now) - last_updated = factory.LazyFunction(datetime.datetime.now) + creation_date = factory.LazyFunction( + lambda: datetime.datetime.now(tz=datetime.timezone.utc) + ) + last_updated = factory.LazyFunction( + lambda: datetime.datetime.now(tz=datetime.timezone.utc) + ) class TaskFactory(factory.django.DjangoModelFactory): @@ -35,8 +39,12 @@ class Meta: name = factory.Faker("word") description = factory.Faker("text") tags = factory.List([factory.Faker("word") for _ in range(10)]) - creation_date = factory.LazyFunction(datetime.datetime.now) - deletion_date = factory.LazyFunction(datetime.datetime.now) + creation_date = factory.LazyFunction( + lambda: datetime.datetime.now(tz=datetime.timezone.utc) + ) + deletion_date = factory.LazyFunction( + lambda: datetime.datetime.now(tz=datetime.timezone.utc) + ) class TopicFactory(factory.django.DjangoModelFactory): @@ -46,7 +54,9 @@ class Meta: name = factory.Faker("word") active = factory.Faker("boolean") description = factory.Faker("text") - creation_date = factory.LazyFunction(datetime.datetime.now) + creation_date = factory.LazyFunction( + lambda: datetime.datetime.now(tz=datetime.timezone.utc) + ) deprecation_date = factory.Faker("date") diff --git a/backend/content/models.py b/backend/content/models.py index dd9452701..a3ae69940 100644 --- a/backend/content/models.py +++ b/backend/content/models.py @@ -70,7 +70,7 @@ def __str__(self) -> str: class Tag(models.Model): - id = models.IntegerField(primary_key=True) + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) text = models.CharField(max_length=255) creation_date = models.DateTimeField(auto_now_add=True) diff --git a/backend/content/serializers.py b/backend/content/serializers.py index 8c972ee19..7e9fb7000 100644 --- a/backend/content/serializers.py +++ b/backend/content/serializers.py @@ -66,10 +66,10 @@ def validate(self, data: Dict[str, Union[str, int]]) -> Dict[str, Union[str, int with PilImage.open(data["image_location"]) as img: img.verify() img_format = img.format.lower() - except Exception: + except Exception as e: raise serializers.ValidationError( _("The image is not valid."), code="corrupted_file" - ) + ) from e if img_format not in image_extensions: raise serializers.ValidationError( diff --git a/backend/content/tests.py b/backend/content/tests.py index fa76025bb..c4c345567 100644 --- a/backend/content/tests.py +++ b/backend/content/tests.py @@ -7,8 +7,9 @@ import pytest +pytestmark = pytest.mark.django_db + -@pytest.mark.django_db def test_str_methods() -> None: resource = ResourceFactory.build() task = TaskFactory.build() diff --git a/backend/entities/factories.py b/backend/entities/factories.py index 5ccc310f9..2bcf69b1b 100644 --- a/backend/entities/factories.py +++ b/backend/entities/factories.py @@ -28,12 +28,26 @@ class OrganizationFactory(factory.django.DjangoModelFactory): class Meta: model = Organization + django_get_or_create = ("created_by",) name = factory.Faker("word") tagline = factory.Faker("word") - social_links = factory.List([factory.Faker("word") for _ in range(10)]) + social_links = ["https://www.instagram.com/activist_org/"] + # Note: Version that accesses the database so we don't create new each time. + # created_by = factory.LazyAttribute( + # lambda x: ( + # UserModel.objects.exclude(username="admin").first() + # if UserModel.objects.exclude(username="admin").exists() + # else factory.SubFactory("authentication.factories.UserFactory") + # ) + # ) created_by = factory.SubFactory("authentication.factories.UserFactory") + status = factory.SubFactory("entities.factories.StatusTypeFactory", name="Active") is_high_risk = factory.Faker("boolean") + location = factory.Faker("city") + acceptance_date = factory.LazyFunction( + lambda: datetime.datetime.now(tz=datetime.timezone.utc) + ) class GroupFactory(factory.django.DjangoModelFactory): @@ -43,9 +57,13 @@ class Meta: org_id = factory.SubFactory(OrganizationFactory) name = factory.Faker("word") tagline = factory.Faker("word") - social_links = factory.List([factory.Faker("word") for _ in range(10)]) + social_links = ["https://www.instagram.com/activist_org/"] created_by = factory.SubFactory("authentication.factories.UserFactory") - creation_date = factory.LazyFunction(datetime.datetime.now) + creation_date = factory.LazyFunction( + lambda: datetime.datetime.now(tz=datetime.timezone.utc) + ) + category = factory.Faker("word") + location = factory.Faker("city") # MARK: Bridge Tables @@ -119,8 +137,12 @@ class Meta: status = factory.SubFactory(OrganizationApplicationStatusFactory) orgs_in_favor = factory.List([factory.Faker("word") for _ in range(10)]) orgs_against = factory.List([factory.Faker("word") for _ in range(10)]) - creation_date = factory.LazyFunction(datetime.datetime.now) - status_updated = factory.LazyFunction(datetime.datetime.now) + creation_date = factory.LazyFunction( + lambda: datetime.datetime.now(tz=datetime.timezone.utc) + ) + status_updated = factory.LazyFunction( + lambda: datetime.datetime.now(tz=datetime.timezone.utc) + ) class OrganizationEventFactory(factory.django.DjangoModelFactory): @@ -185,3 +207,11 @@ class Meta: org_id = factory.SubFactory(OrganizationFactory) topic_id = factory.SubFactory("content.factories.TopicFactory") + + +class StatusTypeFactory(factory.django.DjangoModelFactory): + class Meta: + model = "entities.StatusType" + django_get_or_create = ("name",) + + name = "Active" diff --git a/backend/entities/models.py b/backend/entities/models.py index d366d3d29..56bbd64b5 100644 --- a/backend/entities/models.py +++ b/backend/entities/models.py @@ -158,7 +158,7 @@ def __str__(self) -> str: class OrganizationApplicationStatus(models.Model): - id = models.IntegerField(primary_key=True) + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) status_name = models.CharField(max_length=255) def __str__(self) -> str: diff --git a/backend/entities/tests.py b/backend/entities/tests.py index 0f794d75c..d3abe41cb 100644 --- a/backend/entities/tests.py +++ b/backend/entities/tests.py @@ -3,7 +3,7 @@ """ # mypy: ignore-errors -from django.urls import reverse +import pytest from .factories import ( OrganizationFactory, @@ -21,21 +21,23 @@ GroupTopicFactory, ) +pytestmark = pytest.mark.django_db + def test_str_methods() -> None: - organization = OrganizationFactory.build() - # Needs to be updated to reflect the recent changes - # organization_application = OrganizationApplicationFactory.build() - organization_event = OrganizationEventFactory.build() - organization_member = OrganizationMemberFactory.build() - organization_resource = OrganizationResourceFactory.build() - organization_task = OrganizationTaskFactory.build() - organization_topic = OrganizationTopicFactory.build() - group = GroupFactory.build() - group_event = GroupEventFactory.build() - group_member = GroupMemberFactory.build() - group_resource = GroupResourceFactory.build() - group_topic = GroupTopicFactory.build() + organization = OrganizationFactory.create() + # Note: Needs to be updated to reflect the recent changes. + # organization_application = OrganizationApplicationFactory.create() + organization_event = OrganizationEventFactory.create() + organization_member = OrganizationMemberFactory.create() + organization_resource = OrganizationResourceFactory.create() + organization_task = OrganizationTaskFactory.create() + organization_topic = OrganizationTopicFactory.create() + group = GroupFactory.create() + group_event = GroupEventFactory.create() + group_member = GroupMemberFactory.create() + group_resource = GroupResourceFactory.create() + group_topic = GroupTopicFactory.create() assert str(organization) == organization.name # assert str(organization_application) == str(organization_application.creation_date) diff --git a/backend/events/factories.py b/backend/events/factories.py index 8b8e87f17..d23368c9f 100644 --- a/backend/events/factories.py +++ b/backend/events/factories.py @@ -1,4 +1,5 @@ import datetime +import random import factory @@ -28,11 +29,26 @@ class Meta: online_location_link = factory.Faker("url") offline_location_lat = factory.Faker("latitude") offline_location_long = factory.Faker("longitude") - start_time = factory.LazyFunction(datetime.datetime.now) - end_time = factory.Faker("future_date", end_date="+15d") + start_time = factory.LazyFunction( + lambda: datetime.datetime.now(tz=datetime.timezone.utc) + ) + end_time = factory.LazyAttribute( + lambda x: ( + datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=1) + ) + ) created_by = factory.SubFactory("authentication.factories.UserFactory") - creation_date = factory.LazyFunction(datetime.datetime.now) - deletion_date = factory.Faker("future_date", end_date="+30d") + creation_date = factory.LazyFunction( + lambda: datetime.datetime.now(tz=datetime.timezone.utc) + ) + deletion_date = random.choice( + [ + None, + datetime.datetime.now(tz=datetime.timezone.utc) + + datetime.timedelta(days=30), + ] + ) + is_private = factory.Faker("boolean") class FormatFactory(factory.django.DjangoModelFactory): @@ -41,8 +57,12 @@ class Meta: name = factory.Faker("word") description = factory.Faker("text") - creation_date = factory.LazyFunction(datetime.datetime.now) - last_updated = factory.LazyFunction(datetime.datetime.now) + creation_date = factory.LazyFunction( + lambda: datetime.datetime.now(tz=datetime.timezone.utc) + ) + last_updated = factory.LazyFunction( + lambda: datetime.datetime.now(tz=datetime.timezone.utc) + ) deprecation_date = factory.Faker("future_date", end_date="+30d") @@ -53,8 +73,12 @@ class Meta: name = factory.Faker("word") is_custom = factory.Faker("boolean") description = factory.Faker("text") - creation_date = factory.LazyFunction(datetime.datetime.now) - last_updated = factory.LazyFunction(datetime.datetime.now) + creation_date = factory.LazyFunction( + lambda: datetime.datetime.now(tz=datetime.timezone.utc) + ) + last_updated = factory.LazyFunction( + lambda: datetime.datetime.now(tz=datetime.timezone.utc) + ) deprecation_date = factory.Faker("future_date", end_date="+30d") diff --git a/backend/events/models.py b/backend/events/models.py index 8317857a3..7814b0a98 100644 --- a/backend/events/models.py +++ b/backend/events/models.py @@ -41,7 +41,7 @@ def __str__(self) -> str: class Format(models.Model): - id = models.IntegerField(primary_key=True) + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) name = models.CharField(max_length=255) description = models.TextField(max_length=500) creation_date = models.DateTimeField(auto_now_add=True) @@ -53,7 +53,7 @@ def __str__(self) -> str: class Role(models.Model): - id = models.IntegerField(primary_key=True) + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) name = models.CharField(max_length=255) is_custom = models.BooleanField(default=False) description = models.TextField(max_length=500) @@ -81,7 +81,7 @@ def __str__(self) -> str: class EventAttendeeStatus(models.Model): - id = models.IntegerField(primary_key=True) + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) status_name = models.CharField(max_length=255) def __str__(self) -> str: diff --git a/backend/events/tests.py b/backend/events/tests.py index 969fb27da..87dfe9423 100644 --- a/backend/events/tests.py +++ b/backend/events/tests.py @@ -3,6 +3,7 @@ """ # mypy: ignore-errors +import pytest from .factories import ( EventFactory, @@ -14,15 +15,18 @@ RoleFactory, ) +pytestmark = pytest.mark.django_db + def test_str_methods() -> None: - event = EventFactory.build() - event_attendee = EventAttendeeFactory.build() - event_format = EventFormatFactory.build() - event_attendee_status = EventAttendeeStatusFactory.build() - event_resource = EventResourceFactory.build() - _format = FormatFactory.build() - role = RoleFactory.build() + event = EventFactory.create() + event_attendee = EventAttendeeFactory.create() + event_format = EventFormatFactory.create() + event_attendee_status = EventAttendeeStatusFactory.create() + event_resource = EventResourceFactory.create() + _format = FormatFactory.create() + role = RoleFactory.create() + assert str(event) == event.name assert ( str(event_attendee) == f"{event_attendee.user_id} - {event_attendee.event_id}" diff --git a/docker-compose.yml b/docker-compose.yml index 74c399df2..3a6551258 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.7" - services: backend: env_file: @@ -14,7 +12,7 @@ services: 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 populate_db --users=100 && + python manage.py populate_db --users 10 --orgs 10 --groups 10 --events 10 && python manage.py runserver 0.0.0.0:${BACKEND_PORT}" ports: - "${BACKEND_PORT}:${BACKEND_PORT}" diff --git a/frontend/components/sidebar/left/SidebarLeft.vue b/frontend/components/sidebar/left/SidebarLeft.vue index 1f665941e..4311bacee 100644 --- a/frontend/components/sidebar/left/SidebarLeft.vue +++ b/frontend/components/sidebar/left/SidebarLeft.vue @@ -251,14 +251,16 @@ const applyTopShadow = ref(false); function setSidebarContentScrollable(): void { setTimeout(() => { - sidebarContentScrollable.value = - content.value.scrollHeight > content.value.clientHeight ? true : false; + if (content && content.value) { + sidebarContentScrollable.value = + content.value.scrollHeight > content.value.clientHeight ? true : false; + } }, 50); isAtTop(); } function isAtTop(): void { - if (sidebarContentScrollable) { + if (sidebarContentScrollable && content && content.value) { applyTopShadow.value = !(content.value.scrollTop === 0); } }