diff --git a/.github/workflows/webapp.yml b/.github/workflows/webapp.yml index 9efc742b..98e4bf60 100644 --- a/.github/workflows/webapp.yml +++ b/.github/workflows/webapp.yml @@ -47,6 +47,7 @@ jobs: run: | python manage.py makemigrations authentication python manage.py makemigrations eventposts + python manage.py makemigrations equipmentposts python manage.py migrate - name: Run tests - run: py.test \ No newline at end of file + run: py.test diff --git a/backend/app/settings.py b/backend/app/settings.py index 8ce40f53..65f9e8b0 100644 --- a/backend/app/settings.py +++ b/backend/app/settings.py @@ -41,9 +41,10 @@ 'rest_framework', 'authentication', 'django_rest_passwordreset', + 'equipmentposts', 'eventposts', 'profiles', - 'frontend' + 'frontend', ] SITE_ID = 1 diff --git a/backend/app/urls.py b/backend/app/urls.py index f1cbc85c..e624468f 100644 --- a/backend/app/urls.py +++ b/backend/app/urls.py @@ -21,5 +21,6 @@ path('api/', include('authentication.urls')), path('api/', include('eventposts.urls')), path('api/', include('profiles.urls')), - path('', include('frontend.urls')) + path('api/', include('equipmentposts.urls')), + path('', include('frontend.urls')), ] diff --git a/backend/authentication/models.py b/backend/authentication/models.py index 5fd3ef94..a073b920 100644 --- a/backend/authentication/models.py +++ b/backend/authentication/models.py @@ -1,4 +1,4 @@ -from datetime import datetime +import datetime from django.contrib.auth.models import AbstractUser from django.dispatch import receiver from django.db import models @@ -25,7 +25,8 @@ def password_reset_token_created(sender, instance, reset_password_token, *args, class User(AbstractUser): bio = models.TextField(default="") - birthday = models.DateField(default=datetime.now, blank=True) + birthday = models.DateField(default=datetime.date(datetime.date.today().year - 18, + datetime.date.today().month, datetime.date.today().day), blank=True) avatar = models.TextField(default="") location = models.TextField(default="") diff --git a/backend/equipmentposts/__init__.py b/backend/equipmentposts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/equipmentposts/admin.py b/backend/equipmentposts/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/backend/equipmentposts/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/equipmentposts/apps.py b/backend/equipmentposts/apps.py new file mode 100644 index 00000000..536690ef --- /dev/null +++ b/backend/equipmentposts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class EquipmentpostsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'equipmentposts' diff --git a/backend/equipmentposts/models.py b/backend/equipmentposts/models.py new file mode 100644 index 00000000..28c67359 --- /dev/null +++ b/backend/equipmentposts/models.py @@ -0,0 +1,11 @@ +from django.db import models +from eventposts.models import Post +# Create your models here. + + +class EquipmentPost(Post): + url = models.TextField(default="") + equipment_type = models.TextField() + + class Meta: + app_label = 'equipmentposts' \ No newline at end of file diff --git a/backend/equipmentposts/serializers.py b/backend/equipmentposts/serializers.py new file mode 100644 index 00000000..5f83d591 --- /dev/null +++ b/backend/equipmentposts/serializers.py @@ -0,0 +1,7 @@ +from rest_framework import serializers +from equipmentposts.models import EquipmentPost + +class EquipmentSerializer(serializers.ModelSerializer): + class Meta: + model = EquipmentPost + fields = "__all__" \ No newline at end of file diff --git a/backend/equipmentposts/tests.py b/backend/equipmentposts/tests.py new file mode 100644 index 00000000..5cd430a7 --- /dev/null +++ b/backend/equipmentposts/tests.py @@ -0,0 +1,105 @@ +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 equipmentposts.models import EquipmentPost +from authentication.models import User +from equipmentposts.serializers import EquipmentSerializer +from django.urls import reverse +from django.db.models import Q +from datetime import datetime, timedelta + + +class EquipmentPostSearchTests(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 equipments for testing + self.equipment_01 = EquipmentPost.objects.create(owner=self.user, title='Football for sale', + content='Selling WC 2010 Jabulani football', + location='Madrid', sport='Football', equipment_type="ball", + min_skill_level=3, max_skill_level=4, latitude=40.43103333341609, + longitude=-3.705507357022727) + + self.equipment_02 = EquipmentPost.objects.create(owner=self.user, title='Basketball shoes for sale', + content='Selling my Air Jordans', + location='levent', sport='Basketball', equipment_type="shoes", + min_skill_level=0, max_skill_level=5, latitude=41.08204996728227, + longitude=29.016445404346598) + + self.equipment_03 = EquipmentPost.objects.create(owner=self.user, title='NBA ball or something idk', + content='Selling basketball with NBA logo', + location='Washington', sport='Basketball', equipment_type="ball", + min_skill_level=3, max_skill_level=5, latitude=38.90785448747658, + longitude=-77.04329853399994) + + self.equipment_04 = EquipmentPost.objects.create(owner=self.user, title='Vintage Nike mercurial football cleats', + content='worn only once', + location='etiler', sport='Football', equipment_type="shoes", + min_skill_level=3, max_skill_level=4, latitude=41.13274943188016, + longitude=29.105688623416825) + + self.basketball_ads = EquipmentSerializer(EquipmentPost.objects.filter(sport="Basketball"), many=True).data + + self.football_ads = EquipmentSerializer(EquipmentPost.objects.filter(sport="Football"), many=True).data + + self.in_turkey_ads = EquipmentSerializer(EquipmentPost.objects.filter( + Q(location="levent") | Q(location="etiler")), many=True).data + + self.ads_created_by_user = EquipmentSerializer(EquipmentPost.objects.all(), many=True).data + self.creation_date_today = EquipmentSerializer(EquipmentPost.objects.all(), many=True).data + + self.skill_between_2_4 = self.football_ads + + def test_filter_by_query(self): + response = self.client.get(reverse('equipmentpost-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_ads) + + def test_filter_by_creation_date(self): + response = self.client.get(reverse('equipmentpost-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_location(self): + response = self.client.get(reverse('equipmentpost-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_ads) + + def test_filter_by_sport(self): + response = self.client.get(reverse('equipmentpost-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_ads) + + def test_filter_by_owner(self): + response = self.client.get(reverse('equipmentpost-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.ads_created_by_user) + + def test_filter_by_coordinates(self): + response = self.client.get(reverse('equipmentpost-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_ads) + + def test_filter_by_skill(self): + response = self.client.get(reverse('equipmentpost-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) + diff --git a/backend/equipmentposts/urls.py b/backend/equipmentposts/urls.py new file mode 100644 index 00000000..53d92e63 --- /dev/null +++ b/backend/equipmentposts/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from . import views +from rest_framework.routers import SimpleRouter + +router = SimpleRouter() +router.register(r'equipments', views.EquipmentViewSet) +urlpatterns = router.urls \ No newline at end of file diff --git a/backend/equipmentposts/views.py b/backend/equipmentposts/views.py new file mode 100644 index 00000000..062842d5 --- /dev/null +++ b/backend/equipmentposts/views.py @@ -0,0 +1,157 @@ +from equipmentposts.models import EquipmentPost +from authentication.models import User +from equipmentposts.serializers import EquipmentSerializer +from rest_framework.permissions import AllowAny, IsAuthenticated +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 import permissions +from rest_framework.decorators import action +from rest_framework.pagination import PageNumberPagination +from django.db.models import Q, F, Func, IntegerField +from datetime import datetime +# Create your views here. + +class EquipmentPostsPagination(PageNumberPagination): + page_size = 10 + page_size_query_param = 'page_size' + max_page_size = 1000 + +class EquipmentViewSet(viewsets.ModelViewSet): + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + authentication_classes = [JWTAuthentication] + pagination_class = EquipmentPostsPagination + queryset = EquipmentPost.objects.all() + serializer_class = EquipmentSerializer + JWTauth = JWTAuthentication() + lookup_field = "id" + + def wrap(self, request, data): + queryset = User.objects + owner = queryset.filter(id=data["owner"]).get().username + response = \ + { + "@context": "https://www.w3.org/ns/activitystreams", + "summary": owner + " posted an equipment", + "type": "Create", + "actor": + { + "type": "Person", + "name": owner + }, + "object": + { + "type": "Equipment", + "postId": data["id"], + "ownerId": data["owner"], + "content": data["content"], + "title": data["title"], + "creationDate": data["creation_date"], + "numberOfClicks": 0, + "location": + { + "name": data["location"], + "type": "Place", + "longitude": data["longitude"], + "latitude": data["latitude"], + "units": "m" + }, + "url": data["url"], + "sport": data["sport"], + "equipmentMinSkillLevel": data[ + "min_skill_level"], + "equipmentMaxSkillLevel": data[ + "max_skill_level"], + "equipmentType": data["equipment_type"] + } + } + + return response + + def authenticate(self): + user, _ = self.JWTauth.authenticate(self.request) + return user.id == int(self.request.data["owner"]) + + def get_queryset(self): + queryset = EquipmentPost.objects.all() + + # get all parameters for search + query = self.request.query_params.get('query') + + location = self.request.query_params.get('location') + + + sport = self.request.query_params.get('sport') + + owner_id = self.request.query_params.get('owner') + + equipment_type = self.request.query_params.get('equipment_type') + + 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 name of the location + if location is not None: + queryset = queryset.filter(Q(location__icontains=location)) + + if equipment_type is not None: + queryset = queryset.filter(Q(equipment_type__icontains=equipment_type)) + + # filter by event date + + # 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 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) + return Response(self.wrap(request, serializer.data)) + + def create(self, request, *args, **kwargs): + if self.authenticate(): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(self.wrap(request, serializer.data), status=status.HTTP_201_CREATED, headers=headers) + else: + return JsonResponse(status=401, data={'detail': 'Unauthorized.'}) \ No newline at end of file diff --git a/backend/eventposts/models.py b/backend/eventposts/models.py index b158bed5..ccab08fa 100644 --- a/backend/eventposts/models.py +++ b/backend/eventposts/models.py @@ -3,9 +3,11 @@ from django.contrib.postgres.fields import ArrayField from datetime import datetime + def empty_list(): return list([]) + class Post(models.Model): owner = models.ForeignKey(User, on_delete=models.CASCADE) @@ -15,6 +17,13 @@ class Post(models.Model): creation_date = models.DateTimeField(default=datetime.now()) location = models.TextField(default="") + sport = models.CharField(max_length=30) + + min_skill_level = models.IntegerField(default=0) + max_skill_level = models.IntegerField(default=0) + + latitude = models.FloatField(default=1.0) + longitude = models.FloatField(default=1.0) class Meta: abstract = True @@ -24,7 +33,6 @@ class EventPost(Post): date = models.DateTimeField(default=datetime.now()) duration = models.IntegerField(default=60) - sport = models.CharField(max_length=30) min_age = models.IntegerField(default=18) max_age = models.IntegerField(default=75) @@ -35,8 +43,5 @@ class EventPost(Post): spec_applicants = ArrayField(models.IntegerField(), default=empty_list) spectators = ArrayField(models.IntegerField(), default=empty_list) - min_skill_level = models.IntegerField(default=0) - max_skill_level = models.IntegerField(default=0) - - latitude = models.FloatField(default=1.0) - longitude = models.FloatField(default=1.0) \ No newline at end of file + class Meta: + app_label = 'eventposts' \ No newline at end of file diff --git a/backend/eventposts/views.py b/backend/eventposts/views.py index 962c8190..a2f19787 100644 --- a/backend/eventposts/views.py +++ b/backend/eventposts/views.py @@ -29,10 +29,12 @@ class EventViewSet(viewsets.ModelViewSet): lookup_field = "id" def wrap(self, request, data): + queryset = User.objects + owner = queryset.filter(id=data["owner"]).get().username response = \ - {"@context": "https://www.w3.org/ns/activitystreams", "summary": str(request.user) + " created an event", + {"@context": "https://www.w3.org/ns/activitystreams", "summary": owner + " created an event", "type": "Create", - "actor": {"type": "Person", "name": str(request.user)}, "object": {"type": "Event", + "actor": {"type": "Person", "name": owner}, "object": {"type": "Event", "name": "A Simple Event", "postId": data["id"], "ownerId": data["owner"],