From a02bc5507c13615c20fda3546c5e1c88ec77974d Mon Sep 17 00:00:00 2001 From: Luis Miguel Del Valle Date: Mon, 18 Nov 2024 18:56:04 +0000 Subject: [PATCH 1/7] add technologies filter into eventTypes --- .../migrations/0060_eventtype_technologies.py | 24 +++++++++++++++++++ breathecode/events/models.py | 4 ++++ breathecode/events/serializers.py | 1 + .../tests/urls/tests_academy_eventype.py | 2 ++ .../tests/urls/tests_academy_eventype_slug.py | 10 +++++--- .../events/tests/urls/tests_eventype.py | 10 +++++--- breathecode/events/tests/urls/tests_me.py | 1 + .../events/tests/urls/tests_me_event_id.py | 1 + breathecode/events/views.py | 4 ++++ 9 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 breathecode/events/migrations/0060_eventtype_technologies.py diff --git a/breathecode/events/migrations/0060_eventtype_technologies.py b/breathecode/events/migrations/0060_eventtype_technologies.py new file mode 100644 index 000000000..e3ecbfba6 --- /dev/null +++ b/breathecode/events/migrations/0060_eventtype_technologies.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.2 on 2024-11-18 15:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("events", "0059_event_asset_slug"), + ] + + operations = [ + migrations.AddField( + model_name="eventtype", + name="technologies", + field=models.CharField( + blank=True, + default=None, + help_text="Add comma-separated list of technologies", + max_length=200, + null=True, + ), + ), + ] diff --git a/breathecode/events/models.py b/breathecode/events/models.py index 466dbe5f5..4e6f2d7de 100644 --- a/breathecode/events/models.py +++ b/breathecode/events/models.py @@ -134,6 +134,10 @@ class EventType(models.Model): default=True, help_text="Other academies are allowed to create events of this type" ) + technologies = models.CharField( + max_length=200, null=True, default=None, blank=True, help_text="Add comma-separated list of technologies" + ) + created_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) diff --git a/breathecode/events/serializers.py b/breathecode/events/serializers.py index d3d86812e..1a025c905 100644 --- a/breathecode/events/serializers.py +++ b/breathecode/events/serializers.py @@ -80,6 +80,7 @@ class EventTypeSmallSerializer(serpy.Serializer): id = serpy.Field() slug = serpy.Field() name = serpy.Field() + technologies = serpy.Field() class EventTypeSerializer(EventTypeSmallSerializer): diff --git a/breathecode/events/tests/urls/tests_academy_eventype.py b/breathecode/events/tests/urls/tests_academy_eventype.py index eb9936c3f..e123d4f2e 100644 --- a/breathecode/events/tests/urls/tests_academy_eventype.py +++ b/breathecode/events/tests/urls/tests_academy_eventype.py @@ -34,6 +34,7 @@ def get_serializer(event_type, academy=None, city=None, data={}): "slug": event_type.slug, "lang": event_type.lang, "description": event_type.description, + "technologies": event_type.technologies, **data, } @@ -288,6 +289,7 @@ def test_post_event_type(self): "description": "Potato", "icon_url": "https://www.google.com", "lang": "en", + "technologies": "", } url = reverse_lazy("events:academy_eventype") diff --git a/breathecode/events/tests/urls/tests_academy_eventype_slug.py b/breathecode/events/tests/urls/tests_academy_eventype_slug.py index 916d6fd73..f0fe91b41 100644 --- a/breathecode/events/tests/urls/tests_academy_eventype_slug.py +++ b/breathecode/events/tests/urls/tests_academy_eventype_slug.py @@ -1,11 +1,13 @@ from unittest.mock import MagicMock, call, patch -from breathecode.events.caches import EventCache + from django.urls.base import reverse_lazy +from django.utils import timezone +from breathecode.events.caches import EventCache +from breathecode.services import datetime_to_iso_format from breathecode.utils.api_view_extensions.api_view_extension_handlers import APIViewExtensionHandlers + from ..mixins.new_events_tests_case import EventTestCase -from breathecode.services import datetime_to_iso_format -from django.utils import timezone def get_serializer(event_type, academy=None, city=None, data={}): @@ -35,6 +37,7 @@ def get_serializer(event_type, academy=None, city=None, data={}): "allow_shared_creation": event_type.allow_shared_creation, "description": event_type.description, "visibility_settings": event_type.visibility_settings, + "technologies": event_type.technologies, **data, } @@ -51,6 +54,7 @@ def put_serializer(event_type, data={}): "allow_shared_creation": event_type.allow_shared_creation, "free_for_bootcamps": event_type.free_for_bootcamps, "description": event_type.description, + "technologies": event_type.technologies, **data, } diff --git a/breathecode/events/tests/urls/tests_eventype.py b/breathecode/events/tests/urls/tests_eventype.py index 9288d87f3..b31ccef5b 100644 --- a/breathecode/events/tests/urls/tests_eventype.py +++ b/breathecode/events/tests/urls/tests_eventype.py @@ -1,11 +1,13 @@ from unittest.mock import MagicMock, call, patch -from breathecode.events.caches import EventCache + from django.urls.base import reverse_lazy +from django.utils import timezone +from breathecode.events.caches import EventCache +from breathecode.services import datetime_to_iso_format from breathecode.utils.api_view_extensions.api_view_extension_handlers import APIViewExtensionHandlers + from ..mixins.new_events_tests_case import EventTestCase -from breathecode.services import datetime_to_iso_format -from django.utils import timezone def get_serializer(event_type, academy=None, city=None, data={}): @@ -32,6 +34,7 @@ def get_serializer(event_type, academy=None, city=None, data={}): "slug": event_type.slug, "lang": event_type.lang, "description": event_type.description, + "technologies": event_type.technologies, **data, } @@ -73,6 +76,7 @@ def test_academy_event_type_with_results(self): "created_at": timezone.now(), "updated_at": timezone.now(), "icon_url": "https://www.google.com", + "technologies": None, } model = self.generate_models( authenticate=True, event=True, event_type=True, event_type_kwargs=event_type_kwargs diff --git a/breathecode/events/tests/urls/tests_me.py b/breathecode/events/tests/urls/tests_me.py index fb8866420..5dfd0e330 100644 --- a/breathecode/events/tests/urls/tests_me.py +++ b/breathecode/events/tests/urls/tests_me.py @@ -114,6 +114,7 @@ def get_serializer( "allow_shared_creation": event_type.allow_shared_creation, "description": event_type.description, "visibility_settings": visibility_settings_serializer(event_type.visibility_settings), + "technologies": event_type.technologies, }, "eventbrite_id": event.eventbrite_id, "eventbrite_organizer_id": event.eventbrite_organizer_id, diff --git a/breathecode/events/tests/urls/tests_me_event_id.py b/breathecode/events/tests/urls/tests_me_event_id.py index d1ed7837a..913c33133 100644 --- a/breathecode/events/tests/urls/tests_me_event_id.py +++ b/breathecode/events/tests/urls/tests_me_event_id.py @@ -121,6 +121,7 @@ def get_serializer( "allow_shared_creation": event_type.allow_shared_creation, "description": event_type.description, "visibility_settings": visibility_settings_serializer(event_type.visibility_settings), + "technologies": event_type.technologies, }, "eventbrite_id": event.eventbrite_id, "eventbrite_organizer_id": event.eventbrite_organizer_id, diff --git a/breathecode/events/views.py b/breathecode/events/views.py index bf7625fcf..e6205eb5c 100644 --- a/breathecode/events/views.py +++ b/breathecode/events/views.py @@ -131,6 +131,10 @@ def get_events(request): elif online_event == "false": lookup["online_event"] = False + if "technology" in request.GET: + value = request.GET.get("technology") + lookup["event_type__technologies__icontains"] = value + lookup["ending_at__gte"] = timezone.now() if "past" in request.GET: if request.GET.get("past") == "true": From fd44e99992c347b1899d63bb2bbd4ca49b1819f0 Mon Sep 17 00:00:00 2001 From: Luis Miguel Del Valle Date: Thu, 21 Nov 2024 19:46:07 +0000 Subject: [PATCH 2/7] changes requested --- breathecode/events/views.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/breathecode/events/views.py b/breathecode/events/views.py index e6205eb5c..8ede45899 100644 --- a/breathecode/events/views.py +++ b/breathecode/events/views.py @@ -131,9 +131,12 @@ def get_events(request): elif online_event == "false": lookup["online_event"] = False - if "technology" in request.GET: - value = request.GET.get("technology") - lookup["event_type__technologies__icontains"] = value + if "technologies" in request.GET: + values = request.GET.get("technologies").split(",") + tech_query = Q() + for value in values: + tech_query |= Q(event_type__technologies__icontains=value.strip()) + items = items.filter(tech_query) lookup["ending_at__gte"] = timezone.now() if "past" in request.GET: From c2736fccbcd85b7841b702746af83ddcab4a8358 Mon Sep 17 00:00:00 2001 From: Luis Miguel Del Valle Date: Fri, 22 Nov 2024 05:24:51 +0000 Subject: [PATCH 3/7] changes requested --- breathecode/events/tests/urls/tests_all.py | 90 ++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 breathecode/events/tests/urls/tests_all.py diff --git a/breathecode/events/tests/urls/tests_all.py b/breathecode/events/tests/urls/tests_all.py new file mode 100644 index 000000000..751831a68 --- /dev/null +++ b/breathecode/events/tests/urls/tests_all.py @@ -0,0 +1,90 @@ +from datetime import datetime, timedelta + +import capyc.pytest as capy +from django.urls.base import reverse_lazy + +from breathecode.admissions.models import Academy, City, Country +from breathecode.events.models import EventType + + +def test_filter_by_status(client: capy.Client, database: capy.Database): + url = reverse_lazy("events:all") + + database.create( + country={ + "code": "ES", + "name": "Spain", + } + ) + country_instance = Country.objects.get(code="ES") + + database.create( + city={ + "name": "Madrid", + "country": country_instance, + } + ) + city_instance = City.objects.get(name="Madrid") + + academy = database.create( + academy={ + "slug": "academy-1", + "name": "My Academy", + "logo_url": "https://example.com/logo.jpg", + "street_address": "Address", + "city": city_instance, + "country": country_instance, + } + ) + academy_instance = Academy.objects.get(slug="academy-1") + + event_type1 = database.create( + event_type={ + "slug": "EventType1", + "name": "EventType 1", + "description": "description1", + "technologies": "python, flask", + "academy": academy_instance, + } + ) + event_type1_instance = EventType.objects.get(slug="EventType1") + event_type2 = database.create( + event_type={ + "slug": "EventType2", + "name": "EventType 2", + "description": "description2", + "technologies": "flask, pandas", + "academy": academy_instance, + } + ) + event_type2_instance = EventType.objects.get(slug="EventType2") + + event1 = database.create( + event={ + "title": "My First Event", + "capacity": 100, + "banner": "https://example.com/banner.jpg", + "starting_at": datetime.now(), + "ending_at": datetime.now() + timedelta(hours=2), + "status": "ACTIVE", + "event_type": event_type1_instance, + } + ) + event2 = database.create( + event={ + "title": "My Second Event", + "capacity": 100, + "banner": "https://example.com/banner.jpg", + "starting_at": datetime.now(), + "ending_at": datetime.now() + timedelta(hours=2), + "status": "ACTIVE", + "event_type": event_type2_instance, + } + ) + + response = client.get(f"{url}?technologies=python") + json = response.json() + + assert response.status_code == 200 + assert len(json) == 1 + assert json[0]["event_type"]["technologies"] == "python, flask" From c82afe3d961b0db37f78e6d61f91901498262d8a Mon Sep 17 00:00:00 2001 From: Luis Miguel Del Valle Date: Fri, 22 Nov 2024 17:04:31 +0000 Subject: [PATCH 4/7] test fixed --- breathecode/events/tests/urls/tests_all.py | 176 +++++++++++++-------- 1 file changed, 106 insertions(+), 70 deletions(-) diff --git a/breathecode/events/tests/urls/tests_all.py b/breathecode/events/tests/urls/tests_all.py index 751831a68..c0c0bab77 100644 --- a/breathecode/events/tests/urls/tests_all.py +++ b/breathecode/events/tests/urls/tests_all.py @@ -3,88 +3,124 @@ import capyc.pytest as capy from django.urls.base import reverse_lazy -from breathecode.admissions.models import Academy, City, Country -from breathecode.events.models import EventType +def serialize_event(event): + return { + "id": event.id, + "title": event.title, + "starting_at": event.starting_at.isoformat() if isinstance(event.starting_at, datetime) else event.starting_at, + "ending_at": event.ending_at.isoformat() if isinstance(event.ending_at, datetime) else event.ending_at, + "event_type": { + "id": event.event_type.id, + "slug": event.event_type.slug, + "name": event.event_type.name, + "technologies": event.event_type.technologies, + }, + "slug": event.slug, + "excerpt": event.excerpt or None, + "lang": event.lang or None, + "url": event.url or None, + "banner": event.banner, + "description": event.description or None, + "capacity": event.capacity, + "status": event.status, + "host": event.host or None, + "ended_at": event.ended_at.isoformat() if event.ended_at else None, + "online_event": event.online_event, + "venue": ( + { + "id": event.venue.id, + "title": event.venue.title, + "street_address": event.venue.street_address, + "city": event.venue.city.name, + "zip_code": event.venue.zip_code, + "state": event.venue.state, + "updated_at": event.venue.updated_at.isoformat(), + } + if event.venue + else None + ), + "academy": ( + { + "id": event.academy.id, + "slug": event.academy.slug, + "name": event.academy.name, + "city": {"name": event.academy.city.name} if event.academy.city else None, + } + if event.academy + else None + ), + "sync_with_eventbrite": event.sync_with_eventbrite, + "eventbrite_sync_status": event.eventbrite_sync_status or None, + "eventbrite_sync_description": event.eventbrite_sync_description or None, + "tags": event.tags or None, + "asset_slug": event.asset_slug, + "host_user": ( + None + if not event.host_user + else { + "id": event.host_user.id, + "first_name": event.host_user.first_name, + "last_name": event.host_user.last_name, + } + ), + "author": ( + None + if not event.author + else { + "id": event.author.id, + "first_name": event.author.first_name, + "last_name": event.author.last_name, + } + ), + } -def test_filter_by_status(client: capy.Client, database: capy.Database): - url = reverse_lazy("events:all") - - database.create( - country={ - "code": "ES", - "name": "Spain", - } - ) - country_instance = Country.objects.get(code="ES") - database.create( - city={ - "name": "Madrid", - "country": country_instance, - } - ) - city_instance = City.objects.get(name="Madrid") +def test_filter_by_status(client: capy.Client, database: capy.Database, fake: capy.Fake): + url = reverse_lazy("events:all") - academy = database.create( + model = database.create( + city=1, + country=1, academy={ - "slug": "academy-1", - "name": "My Academy", + "slug": fake.slug(), + "name": fake.name(), "logo_url": "https://example.com/logo.jpg", "street_address": "Address", - "city": city_instance, - "country": country_instance, - } - ) - academy_instance = Academy.objects.get(slug="academy-1") - - event_type1 = database.create( - event_type={ - "slug": "EventType1", - "name": "EventType 1", - "description": "description1", - "technologies": "python, flask", - "academy": academy_instance, - } - ) - event_type1_instance = EventType.objects.get(slug="EventType1") - event_type2 = database.create( - event_type={ - "slug": "EventType2", - "name": "EventType 2", - "description": "description2", - "technologies": "flask, pandas", - "academy": academy_instance, - } - ) - event_type2_instance = EventType.objects.get(slug="EventType2") - - event1 = database.create( - event={ - "title": "My First Event", - "capacity": 100, - "banner": "https://example.com/banner.jpg", - "starting_at": datetime.now(), - "ending_at": datetime.now() + timedelta(hours=2), - "status": "ACTIVE", - "event_type": event_type1_instance, - } - ) - event2 = database.create( - event={ - "title": "My Second Event", - "capacity": 100, - "banner": "https://example.com/banner.jpg", - "starting_at": datetime.now(), - "ending_at": datetime.now() + timedelta(hours=2), - "status": "ACTIVE", - "event_type": event_type2_instance, - } + }, + event_type=[ + { + "slug": fake.slug(), + "name": fake.name(), + "description": "description1", + "technologies": "python, flask", + }, + { + "slug": fake.slug(), + "name": fake.name(), + "description": "description2", + "technologies": "flask, pandas", + }, + ], + event=[ + { + "title": "My First Event", + "capacity": 100, + "banner": "https://example.com/banner.jpg", + "starting_at": datetime.now(), + "ending_at": datetime.now() + timedelta(hours=2), + "status": "ACTIVE", + "event_type_id": n + 1, + } + for n in range(0, 2) + ], ) response = client.get(f"{url}?technologies=python") json = response.json() + expected = [serialize_event(event) for event in model.event if "python" in event.event_type.technologies] + assert response.status_code == 200 assert len(json) == 1 assert json[0]["event_type"]["technologies"] == "python, flask" From 1c879718d64e88ef9e7401bcff310e8da63d57b1 Mon Sep 17 00:00:00 2001 From: Luis Miguel Del Valle Date: Fri, 22 Nov 2024 18:02:32 +0000 Subject: [PATCH 5/7] changes in test --- breathecode/events/tests/urls/tests_all.py | 49 +++++++++++++--------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/breathecode/events/tests/urls/tests_all.py b/breathecode/events/tests/urls/tests_all.py index c0c0bab77..4fe0d032c 100644 --- a/breathecode/events/tests/urls/tests_all.py +++ b/breathecode/events/tests/urls/tests_all.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import capyc.pytest as capy from django.urls.base import reverse_lazy @@ -8,8 +8,16 @@ def serialize_event(event): return { "id": event.id, "title": event.title, - "starting_at": event.starting_at.isoformat() if isinstance(event.starting_at, datetime) else event.starting_at, - "ending_at": event.ending_at.isoformat() if isinstance(event.ending_at, datetime) else event.ending_at, + "starting_at": ( + event.starting_at.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z" + if isinstance(event.starting_at, datetime) + else None + ), + "ending_at": ( + event.ending_at.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z" + if isinstance(event.ending_at, datetime) + else None + ), "event_type": { "id": event.event_type.id, "slug": event.event_type.slug, @@ -17,18 +25,22 @@ def serialize_event(event): "technologies": event.event_type.technologies, }, "slug": event.slug, - "excerpt": event.excerpt or None, - "lang": event.lang or None, - "url": event.url or None, + "excerpt": event.excerpt, + "lang": event.lang, + "url": event.url, "banner": event.banner, - "description": event.description or None, + "description": event.description, "capacity": event.capacity, "status": event.status, - "host": event.host or None, - "ended_at": event.ended_at.isoformat() if event.ended_at else None, + "host": event.host, + "ended_at": ( + event.ended_at.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f") + "Z" if event.ended_at else None + ), "online_event": event.online_event, "venue": ( - { + None + if not event.venue + else { "id": event.venue.id, "title": event.venue.title, "street_address": event.venue.street_address, @@ -37,23 +49,21 @@ def serialize_event(event): "state": event.venue.state, "updated_at": event.venue.updated_at.isoformat(), } - if event.venue - else None ), "academy": ( - { + None + if not event.academy + else { "id": event.academy.id, "slug": event.academy.slug, "name": event.academy.name, "city": {"name": event.academy.city.name} if event.academy.city else None, } - if event.academy - else None ), "sync_with_eventbrite": event.sync_with_eventbrite, - "eventbrite_sync_status": event.eventbrite_sync_status or None, - "eventbrite_sync_description": event.eventbrite_sync_description or None, - "tags": event.tags or None, + "eventbrite_sync_status": event.eventbrite_sync_status, + "eventbrite_sync_description": event.eventbrite_sync_description, + "tags": event.tags, "asset_slug": event.asset_slug, "host_user": ( None @@ -73,6 +83,7 @@ def serialize_event(event): "last_name": event.author.last_name, } ), + "asset": None, } @@ -123,4 +134,4 @@ def test_filter_by_status(client: capy.Client, database: capy.Database, fake: ca assert response.status_code == 200 assert len(json) == 1 - assert json[0]["event_type"]["technologies"] == "python, flask" + assert expected == json From 8db17e34f4e70b1936b33fdfb2df27fcbdfe8cc1 Mon Sep 17 00:00:00 2001 From: Luis Miguel Del Valle Date: Fri, 22 Nov 2024 18:04:25 +0000 Subject: [PATCH 6/7] change --- breathecode/events/tests/urls/tests_all.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/events/tests/urls/tests_all.py b/breathecode/events/tests/urls/tests_all.py index 4fe0d032c..2cf47bdfd 100644 --- a/breathecode/events/tests/urls/tests_all.py +++ b/breathecode/events/tests/urls/tests_all.py @@ -87,7 +87,7 @@ def serialize_event(event): } -def test_filter_by_status(client: capy.Client, database: capy.Database, fake: capy.Fake): +def test_filter_by_technologies(client: capy.Client, database: capy.Database, fake: capy.Fake): url = reverse_lazy("events:all") model = database.create( From 6dab58edecea954a6b58e58a55b78698803d23c9 Mon Sep 17 00:00:00 2001 From: Luis Miguel Del Valle Date: Fri, 22 Nov 2024 18:48:07 +0000 Subject: [PATCH 7/7] adding another test using , --- breathecode/events/tests/urls/tests_all.py | 61 ++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/breathecode/events/tests/urls/tests_all.py b/breathecode/events/tests/urls/tests_all.py index 2cf47bdfd..3201ce064 100644 --- a/breathecode/events/tests/urls/tests_all.py +++ b/breathecode/events/tests/urls/tests_all.py @@ -135,3 +135,64 @@ def test_filter_by_technologies(client: capy.Client, database: capy.Database, fa assert response.status_code == 200 assert len(json) == 1 assert expected == json + + +def test_filter_by_technologies_obtain_two(client: capy.Client, database: capy.Database, fake: capy.Fake): + url = reverse_lazy("events:all") + + model = database.create( + city=1, + country=1, + academy={ + "slug": fake.slug(), + "name": fake.name(), + "logo_url": "https://example.com/logo.jpg", + "street_address": "Address", + }, + event_type=[ + { + "slug": fake.slug(), + "name": fake.name(), + "description": "description1", + "technologies": "python, flask", + }, + { + "slug": fake.slug(), + "name": fake.name(), + "description": "description2", + "technologies": "flask, pandas", + }, + { + "slug": fake.slug(), + "name": fake.name(), + "description": "description3", + "technologies": "javascript, java", + }, + ], + event=[ + { + "title": f"My Event {n + 1}", + "capacity": 100, + "banner": "https://example.com/banner.jpg", + "starting_at": datetime.now(), + "ending_at": datetime.now() + timedelta(hours=2), + "status": "ACTIVE", + "event_type_id": n + 1, + } + for n in range(3) + ], + ) + + response = client.get(f"{url}?technologies=python,java") + json = response.json() + + technologies_to_filter = {"python", "java"} + expected = [ + serialize_event(event) + for event in model.event + if any(tech in event.event_type.technologies.split(", ") for tech in technologies_to_filter) + ] + + assert response.status_code == 200 + assert len(json) == 2 + assert expected == json