diff --git a/.github/workflows/webapp.yml b/.github/workflows/webapp.yml new file mode 100644 index 00000000..4fde9b3c --- /dev/null +++ b/.github/workflows/webapp.yml @@ -0,0 +1,51 @@ +name: Django application + +#on: [push] +on: + push: + branches: + - gh-test + - development + pull_request: + - development + +jobs: + build: + + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./backend + + + services: + postgres: + image: postgres:latest + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: github_actions + ports: + - 5432:5432 + # needed because the postgres container does not provide a healthcheck + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.10 + uses: actions/setup-python@v2 + with: + python-version: '3.10' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest-django + - name: Run migrations + run: | + python manage.py makemigrations authentication + python manage.py makemigrations eventposts + python manage.py migrate + - name: Run tests + run: py.test \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index 7ec8808d..beddd857 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -7,6 +7,9 @@ RUN pip install -r requirements.txt ADD . / WORKDIR / -CMD python manage.py makemigrations authentication && python manage.py makemigrations eventposts && python manage.py migrate && python manage.py runserver 0.0.0.0:9000 +## POSTGRES WAIT SCRIPT FROM https://github.com/ufoscout/docker-compose-wait +ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.9.0/wait /wait +RUN chmod +x /wait +CMD /wait && python manage.py makemigrations authentication && python manage.py makemigrations eventposts && python manage.py migrate && python manage.py test eventposts && python manage.py runserver 0.0.0.0:9000 EXPOSE 9000 \ No newline at end of file diff --git a/backend/app/settings.py b/backend/app/settings.py index bfca77c3..48b90cd5 100644 --- a/backend/app/settings.py +++ b/backend/app/settings.py @@ -98,9 +98,25 @@ 'PORT': '5432', + 'TEST': { + 'NAME': 'mytestdatabase' + } + } } +if os.environ.get('GITHUB_WORKFLOW'): + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'github_actions', + 'USER': 'postgres', + 'PASSWORD': 'postgres', + 'HOST': '127.0.0.1', + 'PORT': '5432', + } + } + # Password validation # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators diff --git a/backend/eventposts/models.py b/backend/eventposts/models.py index 7f0c3176..52763ece 100644 --- a/backend/eventposts/models.py +++ b/backend/eventposts/models.py @@ -1,6 +1,7 @@ from django.db import models from authentication.models import User from django.contrib.postgres.fields import ArrayField +from datetime import datetime def empty_list(): return list([]) @@ -12,7 +13,7 @@ class Post(models.Model): content = models.TextField(default="") title = models.TextField(default="") - creation_date = models.DateTimeField(auto_now_add=True) + creation_date = models.DateTimeField(default=datetime.now()) location = models.TextField(default="") class Meta: @@ -20,7 +21,7 @@ class Meta: class EventPost(Post): - date = models.DateTimeField(auto_now_add=True) + date = models.DateTimeField(default=datetime.now()) duration = models.IntegerField(default=60) sport = models.CharField(max_length=30) diff --git a/backend/eventposts/serializers.py b/backend/eventposts/serializers.py index a9b0653b..c27d66e2 100644 --- a/backend/eventposts/serializers.py +++ b/backend/eventposts/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import EventPost, Post +from eventposts.models import EventPost, Post class EventSerializer(serializers.ModelSerializer): class Meta: diff --git a/backend/eventposts/tests.py b/backend/eventposts/tests.py index 7ce503c2..63d69626 100644 --- a/backend/eventposts/tests.py +++ b/backend/eventposts/tests.py @@ -1,3 +1,169 @@ from django.test import TestCase +from rest_framework.test import force_authenticate +from rest_framework.test import APITestCase, APIRequestFactory +from rest_framework import status +from eventposts.models import EventPost +from authentication.models import User +from eventposts.serializers import EventSerializer +from django.urls import reverse +from django.db.models import Q +from datetime import datetime, timedelta + + +class EventPostTests(APITestCase): + def setUp(self): + # create user and get auth token + self.user = User.objects.create_user(email="user@user.com", password="1234567", username="user") + resp = self.client.post(reverse('token_create'), {'username': 'user', 'password': '1234567'}) + self.token = resp.data['access'] + + # create additional users for spectators and players + self.user2 = User.objects.create_user(email="user2@user.com", password="1234567", username="user2") + self.user3 = User.objects.create_user(email="user3@user.com", password="1234567", username="user3") + + # create mock events for testing + self.event_01 = EventPost.objects.create(owner=self.user, title='Football Event', + content='We are organizing a football match with FC Barcelona fans.', + location='Madrid', date=datetime(2021, 9, 3), sport='Football', + min_age=13, max_age=45, player_capacity=14, spec_capacity=20, + min_skill_level=3, max_skill_level=4, latitude=40.43103333341609, + longitude=-3.705507357022727, duration=90, players=[self.user.id], + spectators=[self.user2.id, self.user3.id]) + + self.event_02 = EventPost.objects.create(owner=self.user, title='basketball event', + content='We are organizing a basketball match with Anadolu Efes fans.', + location='levent', date=datetime(2022, 5, 5), sport='Basketball', + min_age=20, max_age=33, player_capacity=6, spec_capacity=10, + min_skill_level=0, max_skill_level=5, latitude=41.08204996728227, + longitude=29.016445404346598, duration=120, + spectators=[self.user.id, self.user2.id, self.user3.id]) + + self.event_03 = EventPost.objects.create(owner=self.user, title="Let's show some NBA skillz", + content='Guys, today we are gathering to play basketball, join us!', + location='Washington', date=datetime(2022, 4, 4), sport='Basketball', + min_age=18, max_age=30, player_capacity=8, spec_capacity=10, + min_skill_level=3, max_skill_level=5, latitude=38.90785448747658, + longitude=-77.04329853399994, duration=120) + + self.event_04 = EventPost.objects.create(owner=self.user, title="Tek kale aylık", + content='hep maç olmaz bu sefer tek kale aylık ayarlıyoruz,' + ' katılım sınırlıdır', + location='etiler', date=datetime(2021, 1, 13), sport='Football', + min_age=13, max_age=17, player_capacity=5, spec_capacity=18, + min_skill_level=3, max_skill_level=4, latitude=41.13274943188016, + longitude=29.105688623416825, duration=60, + players=[self.user.id, self.user2.id], spectators=[self.user3.id]) + + self.basketball_games = EventSerializer(EventPost.objects.filter(sport="Basketball"), many=True).data + + self.football_games = EventSerializer(EventPost.objects.filter(sport="Football"), many=True).data + + self.player_capacity_between_6_10_games = EventSerializer( + EventPost.objects.filter(Q(player_capacity=6) | Q(player_capacity=8)), many=True).data + + self.spec_capacity_between_15_25_games = self.football_games + + self.age_between_18_35_games = self.basketball_games + + self.in_turkey_games = EventSerializer(EventPost.objects.filter( + Q(location="levent") | Q(location="etiler")), many=True).data + + self.date_inside_2022_games = self.basketball_games + + self.duration_between_45_100_games = self.football_games + self.games_created_by_user = EventSerializer(EventPost.objects.all(), many=True).data + self.creation_date_today = EventSerializer(EventPost.objects.all(), many=True).data + self.num_players_between_1_5_games = self.football_games + self.num_spectators_between_3_5_games = EventSerializer(EventPost.objects.filter(title="basketball event"), + many=True).data + + self.skill_between_2_4 = self.football_games + + def test_filter_by_query(self): + response = self.client.get(reverse('eventpost-list'), {'query': 'basketball'}, HTTP_AUTHORIZATION=f'JWT {self.token}') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['results'], self.basketball_games) + + def test_filter_by_date(self): + response = self.client.get(reverse('eventpost-list'), {'min_date': datetime(2021, 12, 12), + 'max_date': datetime(2023, 1, 1)}, + HTTP_AUTHORIZATION=f'JWT {self.token}') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['results'], self.date_inside_2022_games) + + def test_filter_by_creation_date(self): + response = self.client.get(reverse('eventpost-list'), {'min_creation_date': (datetime.today() - timedelta(3)), + 'max_creation_date': (datetime.today() + timedelta(1))}, + HTTP_AUTHORIZATION=f'JWT {self.token}') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['results'], self.creation_date_today) + + def test_filter_by_player_capacity(self): + response = self.client.get(reverse('eventpost-list'), {'min_player_capacity': 6, 'max_player_capacity': 10}, + HTTP_AUTHORIZATION=f'JWT {self.token}') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['results'], self.player_capacity_between_6_10_games) + + def test_filter_by_spec_capacity(self): + response = self.client.get(reverse('eventpost-list'), {'min_spectator_capacity': 15, 'max_spectator_capacity': 25}, + HTTP_AUTHORIZATION=f'JWT {self.token}') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['results'], self.spec_capacity_between_15_25_games) + + def test_filter_by_location(self): + response = self.client.get(reverse('eventpost-list'), {'location': 'le'}, + HTTP_AUTHORIZATION=f'JWT {self.token}') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['results'], self.in_turkey_games) + + def test_filter_by_duration(self): + response = self.client.get(reverse('eventpost-list'), {'min_duration': 45, 'max_duration': 100}, + HTTP_AUTHORIZATION=f'JWT {self.token}') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['results'], self.duration_between_45_100_games) + + def test_filter_by_sport(self): + response = self.client.get(reverse('eventpost-list'), {'sport': 'Basketball'}, + HTTP_AUTHORIZATION=f'JWT {self.token}') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['results'], self.basketball_games) + + def test_filter_by_owner(self): + response = self.client.get(reverse('eventpost-list'), {'owner_id': self.user.id}, + HTTP_AUTHORIZATION=f'JWT {self.token}') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['results'], self.games_created_by_user) + + def test_filter_by_age(self): + response = self.client.get(reverse('eventpost-list'), {'min_age': 18, 'max_age': 35}, + HTTP_AUTHORIZATION=f'JWT {self.token}') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['results'], self.age_between_18_35_games) + + def test_filter_by_coordinates(self): + response = self.client.get(reverse('eventpost-list'), {'min_latitude': 36.23763062438484, + 'max_latitude': 42.01901802424485, + 'min_longitude': 26.732105369671633, + 'max_longitude': 44.3513027746188}, + HTTP_AUTHORIZATION=f'JWT {self.token}') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['results'], self.in_turkey_games) + + def test_filter_by_player_numbers(self): + response = self.client.get(reverse('eventpost-list'), {'min_players': 1, 'max_players': 5}, + HTTP_AUTHORIZATION=f'JWT {self.token}') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['results'], self.num_players_between_1_5_games) + + def test_filter_by_spectator_numbers(self): + response = self.client.get(reverse('eventpost-list'), {'min_spectators': 3, 'max_spectators': 5}, + HTTP_AUTHORIZATION=f'JWT {self.token}') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['results'], self.num_spectators_between_3_5_games) + + def test_filter_by_skill(self): + response = self.client.get(reverse('eventpost-list'), {'min_skill_level': 2, 'max_skill_level': 4}, + HTTP_AUTHORIZATION=f'JWT {self.token}') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['results'], self.skill_between_2_4) -# Create your tests here. diff --git a/backend/eventposts/views.py b/backend/eventposts/views.py index 3de12036..316dedd4 100644 --- a/backend/eventposts/views.py +++ b/backend/eventposts/views.py @@ -1,11 +1,13 @@ -from .models import EventPost, Post -from .serializers import EventSerializer +from eventposts.models import EventPost, Post +from authentication.models import User +from eventposts.serializers import EventSerializer from rest_framework_simplejwt.authentication import JWTAuthentication from django.http import JsonResponse from rest_framework import status from rest_framework.response import Response from rest_framework import viewsets from rest_framework.pagination import PageNumberPagination +from django.db.models import Q, F, Func, IntegerField class EventPostsPagination(PageNumberPagination): @@ -50,6 +52,139 @@ def authenticate(self): user, _ = self.JWTauth.authenticate(self.request) return user.id == self.request.data["owner"] + def get_queryset(self): + queryset = EventPost.objects.all() + + # get all parameters for search + query = self.request.query_params.get('query') + + min_player_capacity = self.request.query_params.get('min_player_capacity') + max_player_capacity = self.request.query_params.get('max_player_capacity') + + min_spec_capacity = self.request.query_params.get('min_spectator_capacity') + max_spec_capacity = self.request.query_params.get('max_spectator_capacity') + + min_players = self.request.query_params.get('min_players') + max_players = self.request.query_params.get('max_players') + + min_specs = self.request.query_params.get('min_spectators') + max_specs = self.request.query_params.get('max_spectators') + + location = self.request.query_params.get('location') + + min_date = self.request.query_params.get('min_date') + max_date = self.request.query_params.get('max_date') + + min_duration = self.request.query_params.get('min_duration') + max_duration = self.request.query_params.get('max_duration') + + sport = self.request.query_params.get('sport') + + owner_id = self.request.query_params.get('owner') + + min_age = self.request.query_params.get('min_age') + max_age = self.request.query_params.get('max_age') + + min_latitude = self.request.query_params.get('min_latitude') + max_latitude = self.request.query_params.get('max_latitude') + min_longitude = self.request.query_params.get('min_longitude') + max_longitude = self.request.query_params.get('max_longitude') + + min_skill = self.request.query_params.get('min_skill_level') + max_skill = self.request.query_params.get('max_skill_level') + + min_creation_date = self.request.query_params.get('min_creation_date') + max_creation_date = self.request.query_params.get('max_creation_date') + + # filter by query by searching in both title and description + if query is not None: + queryset = queryset.filter(Q(title__icontains=query) | Q(content__icontains=query)) + + # filter by player capacity + if min_player_capacity is not None: + queryset = queryset.filter(player_capacity__gte=min_player_capacity) + if max_player_capacity is not None: + queryset = queryset.filter(player_capacity__lte=max_player_capacity) + + # filter by number of players registered to event + if min_players is not None: + queryset = queryset.annotate(player_len=Func(F('players'), + 1, function='array_length', + output_field=IntegerField())).filter( + player_len__gte=min_players) + + if max_players is not None: + queryset = queryset.annotate(player_len=Func(F('players'), + 1, function='array_length', + output_field=IntegerField())).filter( + player_len__lte=max_players) + + # filter by spectator capacity + if min_spec_capacity is not None: + queryset = queryset.filter(spec_capacity__gte=min_spec_capacity) + if max_spec_capacity is not None: + queryset = queryset.filter(spec_capacity__lte=max_spec_capacity) + + # filter by number of spectators registered to event + if min_specs is not None: + queryset = queryset.annotate(spec_len=Func(F('spectators'), + 1, function='array_length', + output_field=IntegerField())).filter( + spec_len__gte=min_specs) + if max_specs is not None: + queryset = queryset.annotate(spec_len=Func(F('spectators'), 1, function='array_length', + output_field=IntegerField())).filter(spec_len__lte=max_specs) + + # filter by name of the location + if location is not None: + queryset = queryset.filter(Q(location__icontains=location)) + + # filter by event date + if min_date is not None: + queryset = queryset.filter(date__gte=min_date) + if max_date is not None: + queryset = queryset.filter(date__lte=max_date) + + # filter by duration of the event + if min_duration is not None: + queryset = queryset.filter(duration__gte=min_duration) + if max_duration is not None: + queryset = queryset.filter(duration__lte=max_duration) + + # filter by sport category + if sport is not None: + queryset = queryset.filter(sport=sport) + + # filter by owner of the event + if owner_id is not None: + queryset = queryset.filter(owner=User.objects.get(id=owner_id)) + + # filter by age interval + if min_age is not None: + queryset = queryset.filter(min_age__gte=min_age) + if max_age is not None: + queryset = queryset.filter(max_age__lte=max_age) + + # filter by coordinates whether the locations are inside the rectangle + if min_latitude is not None and max_latitude is not None: + queryset = queryset.filter(Q(latitude__lte=max_latitude) & Q(latitude__gte=min_latitude)) + if min_longitude is not None and max_longitude is not None: + queryset = queryset.filter(Q(longitude__lte=max_longitude) & Q(longitude__gte=min_longitude)) + + # filter by skill levels + if min_skill is not None: + queryset = queryset.filter(min_skill_level__gte=min_skill) + if max_skill is not None: + queryset = queryset.filter(max_skill_level__lte=max_skill) + + # filter by creation date + if min_creation_date is not None: + queryset = queryset.filter(creation_date__gte=min_creation_date) + if max_creation_date is not None: + queryset = queryset.filter(creation_date__lte=max_creation_date) + + return queryset + def retrieve(self, request, *args, **kwargs): instance = self.get_object() serializer = self.get_serializer(instance) diff --git a/backend/pytest.ini b/backend/pytest.ini new file mode 100644 index 00000000..5cc53ab0 --- /dev/null +++ b/backend/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +DJANGO_SETTINGS_MODULE = app.settings + +python_files = tests.py test_*.py *_tests.py \ No newline at end of file