diff --git a/docker-compose.yml b/docker-compose.yml index d806e06..60fa317 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: image: postgres env_file: ./.enviroments/.dev volumes: - - postgres_data:/var/lib/postgresql/data + - "./data/postgres:/var/lib/postgresql/data" acacia-back: container_name: acacia_backend @@ -25,7 +25,4 @@ services: volumes: - .:/code depends_on: - - db - -volumes: - postgres_data: + - db \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index c1ed842..adf7b8a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,7 @@

[![codecov](https://codecov.io/gh/fga-eps-mds/2019.2-Acacia/branch/develop/graph/badge.svg)](https://codecov.io/gh/fga-eps-mds/2019.2-Acacia) +[![Maintainability](https://api.codeclimate.com/v1/badges/9ceab9b0533182362c16/maintainability)](https://codeclimate.com/github/fga-eps-mds/2019.2-Acacia/maintainability) ## Visão geral @@ -68,4 +69,4 @@ Após esses passos a aplicação deverá estar acessível em: #### Front-end: -Para instalar a camada front-end da aplicação basta seguir os passos de instalação descritos [aqui](https://github.com/fga-eps-mds/2019.2-Acacia-Frontend) \ No newline at end of file +Para instalar a camada front-end da aplicação basta seguir os passos de instalação descritos [aqui](https://github.com/fga-eps-mds/2019.2-Acacia-Frontend) diff --git a/requirements.txt b/requirements.txt index b4aeb67..c6327f2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,12 @@ +Pillow +whitenoise +flake8==3.7.8 Django==2.2.4 -djangorestframework==3.10 -django-phonenumber-field==3.0 -phonenumbers==8.10 psycopg2==2.8.3 -django-cors-headers==3.1.0 -flake8==3.7.8 -djangorestframework-simplejwt==4.3.0 coverage==4.5.4 +phonenumbers==8.10 django-localflavor==2.2 -pillow -whitenoise +djangorestframework==3.10 +django-cors-headers==3.1.0 +django-phonenumber-field==3.0 +djangorestframework-simplejwt==4.3.0 diff --git a/src/acacia/settings.py b/src/acacia/settings.py index 8fd6d04..e0d5082 100644 --- a/src/acacia/settings.py +++ b/src/acacia/settings.py @@ -119,6 +119,8 @@ ('fr-CA', _('French Canadian')), ) +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # Password validation # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators @@ -178,8 +180,7 @@ "http://localhost:8080", "http://45.55.46.19:8080", "http://45.55.46.19:8081", - -] +] SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(days=5), diff --git a/src/acacia/urls.py b/src/acacia/urls.py index 8287cd0..c1ada09 100644 --- a/src/acacia/urls.py +++ b/src/acacia/urls.py @@ -1,7 +1,7 @@ from django.contrib import admin from django.urls import path, include from .helpers import list_all_endpoints -from harvest.viewsets import WeekHarvests +from harvest.viewsets import WeekHarvests, MonthlyHarvests urlpatterns = [ @@ -25,6 +25,12 @@ WeekHarvests.as_view({'get': 'list'}), name='weekly_harvests' ), + + path( + 'monthly_harvest///', + MonthlyHarvests.as_view({'get': 'list'}), + name='monthly_harvest' + ), ] urlpatterns = list_all_endpoints(urlpatterns) diff --git a/src/harvest/migrations/0001_initial.py b/src/harvest/migrations/0001_initial.py index 867ff59..5d00cde 100644 --- a/src/harvest/migrations/0001_initial.py +++ b/src/harvest/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.4 on 2019-11-16 15:35 +# Generated by Django 2.2.4 on 2019-11-19 23:47 from django.db import migrations, models import django.db.models.deletion diff --git a/src/harvest/models.py b/src/harvest/models.py index 605ed3a..2e98d5a 100644 --- a/src/harvest/models.py +++ b/src/harvest/models.py @@ -33,17 +33,6 @@ class Harvest(models.Model): max_volunteers = models.PositiveSmallIntegerField() min_volunteers = models.PositiveSmallIntegerField() - # TODO: check if this field will continue in this model - # ACCESS_TYPES = ( - # ('Restrict Access', 'Restrict Access'), - # ('Free Access', 'Free Access'), - # ) - - # access = models.CharField( - # choices=ACCESS_TYPES, - # max_length=15 - # ) - def __str__(self): return str(self.date) diff --git a/src/harvest/tests/test_tree_views.py b/src/harvest/tests/test_tree_views.py index b2dcb05..b7e1ead 100644 --- a/src/harvest/tests/test_tree_views.py +++ b/src/harvest/tests/test_tree_views.py @@ -394,3 +394,152 @@ def test_delete_harvest(self): 0, msg='Failed to delete the harvest' ) + + +class MonthlyHarvestsTestCase(APITestCase): + + def create_authentication_tokens(self, user_credentials): + url_token = reverse('users:token_obtain_pair') + + response = self.client.post( + url_token, + user_credentials, + format='json' + ) + + self.assertEqual( + response.status_code, + 200, + msg='Failed to create user tokens credentials' + ) + + self.credentials = { + 'HTTP_AUTHORIZATION': 'Bearer ' + response.data['access'] + } + + def create_user(self): + user_data = { + "username": 'vitas', + 'email': 'vitas@vitas.com', + 'password': 'vitasIsNice', + 'confirm_password': 'vitasIsNice' + } + + url_user_signup = reverse('users:register') + + response = self.client.post( + url_user_signup, + user_data, + format='json' + ) + + self.assertEqual( + response.status_code, + 201, + msg='Failed during user creation' + ) + + user_data.pop('username') + user_data.pop('confirm_password') + + self.create_authentication_tokens(user_data) + + def create_property(self): + property_data = { + 'type_of_address': 'House', + 'BRZipCode': '73021498', + 'state': 'DF', + 'city': 'Gama', + 'district': 'Leste', + 'address': "Quadra 4", + } + + url_property_creation = reverse( + 'property:property-list', + ) + + response = self.client.post( + path=url_property_creation, + data=property_data, + format='json', + **self.credentials, + ) + + self.assertEqual( + response.status_code, + 201, + msg='Failed to create property' + ) + + self.property = Property.objects.get(pk=1) + + def setUp(self): + self.create_user() + self.create_property() + + self.harvest_data = { + 'date': '2019-11-21', + 'description': 'Apple Harvest', + 'status': 'Open', + 'max_volunteers': 10, + 'min_volunteers': 5, + } + + self.url_list = reverse( + 'property:harvest:harvest-list', + kwargs={'property_pk': self.property.pk} + ) + + self.url_detail = reverse( + 'property:harvest:harvest-detail', + kwargs={ + 'property_pk': self.property.pk, + 'pk': '1' + } + ) + + def tearDown(self): + Property.objects.all().delete() + User.objects.all().delete() + Harvest.objects.all().delete() + + def test_get_harvests_of_the_week(self): + + today = datetime.date(datetime(2020, 1, 1)) + + for i in range(0, 12): + date = today + timedelta(days=i*31) + + self.harvest_data['date'] = date + + response = self.client.post( + path=self.url_list, + data=self.harvest_data, + format='json', + **self.credentials, + ) + + self.assertEqual( + response.status_code, + 201, + msg='Failed to create a harvest' + ) + + for month in range(1, 13): + weekly_harvests_url = reverse( + 'monthly_harvest', + kwargs={ + 'year': date.year, + 'month': month + } + ) + + response = self.client.get( + path=weekly_harvests_url, + ) + + self.assertEqual( + len(response.data), + 1, + msg='Failed return only 1 harvest' + ) diff --git a/src/harvest/viewsets.py b/src/harvest/viewsets.py index 70ba824..1635872 100644 --- a/src/harvest/viewsets.py +++ b/src/harvest/viewsets.py @@ -7,6 +7,7 @@ from . import serializers import datetime +import calendar class HarvestViewSet(viewsets.ModelViewSet): @@ -74,3 +75,26 @@ def get_queryset(self): ) return queryset + + +class MonthlyHarvests(ListModelMixin, viewsets.GenericViewSet): + + queryset = Harvest.objects.all() + serializer_class = serializers.HarvestSerializer + permission_classes = (permissions.AllowAny,) + + def get_queryset(self): + + month = self.kwargs['month'] + year = self.kwargs['year'] + + _, last_day = calendar.monthrange(year, month) + + start_date = datetime.datetime(year, month, 1) + end_date = datetime.datetime(year, month, last_day) + + queryset = Harvest.objects.filter( + date__range=(start_date, end_date) + ) + + return queryset diff --git a/src/property/migrations/0001_initial.py b/src/property/migrations/0001_initial.py index 250c635..43d46ae 100644 --- a/src/property/migrations/0001_initial.py +++ b/src/property/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.4 on 2019-11-16 15:35 +# Generated by Django 2.2.4 on 2019-11-19 23:47 from django.conf import settings from django.db import migrations, models diff --git a/src/property/permissions.py b/src/property/permissions.py index 3b99a55..ec32ade 100644 --- a/src/property/permissions.py +++ b/src/property/permissions.py @@ -7,7 +7,7 @@ class UserIsPropertyOwner(permissions.BasePermission): Assumes the model instance has an `user` attribute. """ - def has_object_permition(self, request, view, property): + def has_object_permission(self, request, view, property): return bool( request.method in permissions.SAFE_METHODS or request.user and diff --git a/src/property/tests/test_models.py b/src/property/tests/test_models.py index 72896b0..7dbed25 100644 --- a/src/property/tests/test_models.py +++ b/src/property/tests/test_models.py @@ -30,6 +30,13 @@ def test_verbose_name_plural(self): _('Properties') ) + def test_unique_together(self): + self.assertEqual( + Property._meta.unique_together, + (('BRZipCode', 'type_of_address', 'address'),), + msg='Property unique key is not being set properly' + ) + def test_property_creation(self): self.assertEqual( Property.objects.last(), diff --git a/src/property/tests/test_views.py b/src/property/tests/test_views.py index fa892a7..ab997c6 100644 --- a/src/property/tests/test_views.py +++ b/src/property/tests/test_views.py @@ -6,6 +6,7 @@ from property import viewsets from property.models import Property +from property.permissions import UserIsPropertyOwner from users.models import User @@ -64,6 +65,13 @@ def test_create_property(self): response = self.view_list(request) self.assertEqual(201, response.status_code) + def test_create_property_with_same_unique_key(self): + # changing zip code to create a unique property + request = self.factory.post(self.url_list, self.data) + force_authenticate(request, user=self.user) + response = self.view_list(request) + self.assertEqual(400, response.status_code) + def test_list_property(self): request = self.factory.get(self.url_list) force_authenticate(request, user=self.user) @@ -80,6 +88,14 @@ def test_delete_property(self): ) self.assertEqual(204, response.status_code) + def test_delete_property_without_authentication(self): + request = self.factory.delete(self.url_detail) + response = self.view_detail( + request, + pk=self.property.pk + ) + self.assertEqual(401, response.status_code) + def test_retrieve_property(self): request = self.factory.get(self.url_detail) force_authenticate(request, user=self.user) @@ -90,14 +106,65 @@ def test_retrieve_property(self): self.assertEqual(200, response.status_code) self.assertDictContainsSubset(self.data, response.data) - def test_update_property(self): + def test_patch_update_property(self): + request = self.factory.patch( + self.url_detail, + {'state': 'GO'} + ) + force_authenticate(request, user=self.user) + response = self.view_detail( + request, + pk=self.property.pk + ) + self.assertEqual(200, response.status_code) + + self.data['state'] = 'GO' + + self.assertEqual( + response.data['state'], + self.data['state'] + ) + + self.assertDictContainsSubset(self.data, response.data) + + def test_put_update_property(self): self.data['state'] = 'GO' - request = self.factory.patch(self.url_detail, self.data) + self.data['city'] = 'Damianópolis' + request = self.factory.put(self.url_detail, self.data) force_authenticate(request, user=self.user) + response = self.view_detail( request, pk=self.property.pk ) + self.assertEqual(200, response.status_code) - self.assertEqual(response.data['state'], self.data['state']) + + self.assertEqual( + response.data['state'], + self.data['state'] + ) + self.assertDictContainsSubset(self.data, response.data) + + def test_read_only_permission(self): + + another_user = User.objects.create( + username='MrRobot', + email='robot@mr.com', + password='hackerman' + ) + + request = self.factory.get(self.url_detail) + force_authenticate(request, user=another_user) + + permission = UserIsPropertyOwner() + ans = permission.has_object_permission( + request, + self.view_detail, + self.property + ) + + self.assertTrue( + ans + ) diff --git a/src/tree/migrations/0001_initial.py b/src/tree/migrations/0001_initial.py index cb87460..bd3a1eb 100644 --- a/src/tree/migrations/0001_initial.py +++ b/src/tree/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.4 on 2019-11-16 15:35 +# Generated by Django 2.2.4 on 2019-11-19 23:47 from django.db import migrations, models import django.db.models.deletion diff --git a/src/users/admin.py b/src/users/admin.py index 6f0a077..40609da 100644 --- a/src/users/admin.py +++ b/src/users/admin.py @@ -1,6 +1,6 @@ from django.contrib.auth.admin import UserAdmin from django.contrib import admin -from .models import User +from .models import User, Profile class MyUserAdmin(UserAdmin): @@ -27,4 +27,5 @@ class MyUserAdmin(UserAdmin): search_fields = ('email', 'username') -admin.site.register(User, MyUserAdmin) +admin.site.register(User, UserAdmin) +admin.site.register(Profile) diff --git a/src/users/migrations/0001_initial.py b/src/users/migrations/0001_initial.py index 4e1fb5c..a11bb75 100644 --- a/src/users/migrations/0001_initial.py +++ b/src/users/migrations/0001_initial.py @@ -1,8 +1,10 @@ -# Generated by Django 2.2.4 on 2019-11-16 15:35 +# Generated by Django 2.2.4 on 2019-11-19 23:47 +from django.conf import settings import django.contrib.auth.models import django.contrib.auth.validators from django.db import migrations, models +import django.db.models.deletion import django.utils.timezone import phonenumber_field.modelfields @@ -49,4 +51,18 @@ class Migration(migrations.Migration): ('objects', django.contrib.auth.models.UserManager()), ], ), + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('phone_number', models.CharField(blank=True, default='', max_length=15)), + ('bio', models.TextField(blank=True, default='')), + ('birthdate', models.DateField(null=True)), + ('photo', models.ImageField(blank=True, null=True, upload_to='media/profile_photo')), + ('is_owner', models.BooleanField(blank=True, default=False, help_text='Designates if user has a propriety')), + ('is_volunteer', models.BooleanField(blank=True, default=False, help_text='Designates if user is a volunteer')), + ('is_leader', models.BooleanField(blank=True, default=False, help_text='Designates if user is a haverst leader')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), ] diff --git a/src/users/models.py b/src/users/models.py index 03b03c0..9c53449 100644 --- a/src/users/models.py +++ b/src/users/models.py @@ -14,7 +14,6 @@ class User(AbstractUser): 'unique': 'A user with that email already exists.', } ) - phone_number = PhoneNumberField( blank=True, null=True @@ -60,3 +59,57 @@ class User(AbstractUser): EMAIL_FIELD = 'email' USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['username'] + + +class Profile(models.Model): + + objects = models.Manager() + + user = models.OneToOneField( + User, + on_delete=models.CASCADE, + ) + + phone_number = models.CharField( + blank=True, + null=False, + default="", + max_length=15 + ) + + bio = models.TextField( + blank=True, + null=False, + default="" + ) + + birthdate = models.DateField( + null=True, + ) + + photo = models.ImageField( + upload_to='media/profile_photo', + blank=True, + null=True + ) + + is_owner = models.BooleanField( + default=False, + help_text=_('Designates if user has a propriety'), + blank=True, + null=False + ) + + is_volunteer = models.BooleanField( + default=False, + help_text=_('Designates if user is a volunteer'), + blank=True, + null=False + ) + + is_leader = models.BooleanField( + default=False, + help_text=_('Designates if user is a haverst leader'), + blank=True, + null=False + ) diff --git a/src/users/permissions.py b/src/users/permissions.py new file mode 100644 index 0000000..91e8a83 --- /dev/null +++ b/src/users/permissions.py @@ -0,0 +1,10 @@ +from rest_framework import permissions + + +class IsOwner(permissions.BasePermission): + """ + Custom permisson to only allow profile owner to edit it + """ + + def has_object_permission(self, request, view, obj): + return obj.user == request.user diff --git a/src/users/serializers.py b/src/users/serializers.py index 7e04391..90376ce 100644 --- a/src/users/serializers.py +++ b/src/users/serializers.py @@ -1,11 +1,14 @@ from django.conf import settings from django.utils.translation import ugettext as _ -from .models import User +# Models +from .models import User, Profile from rest_framework import serializers from rest_framework.validators import UniqueValidator +from datetime import date + class UserSignUpSerializer(serializers.Serializer): @@ -80,6 +83,8 @@ def create(self, validated_data): is_verified=False ) + Profile.objects.create(user=user) + # TODO: SEND CONFIRMATION EMAIL return user @@ -92,14 +97,90 @@ class Meta: 'username', 'email', 'password', - 'phone_number', - 'bio', - 'birth', 'speaks_french', 'speaks_english', ] +class ProfileModelSerializer(serializers.ModelSerializer): + + email = serializers.EmailField(source='user.email') + username = serializers.CharField(source='user.username') + + class Meta: + model = Profile + fields = [ + 'photo', + 'birthdate', + 'bio', + 'phone_number', + 'email', + 'username', + ] + + def check_if_is_unique(self, field, field_name): + """ + This method uses class meta data to + checks if the field is unique on database + + Args: + field (string): field value from Profile Serializer + field_name (string): field name from Profile model + + Raises: + ValidationError: this field is already in use + + Returns: + string: same field passed as argument + """ + + current_field = self.instance.user.__dict__.get(field_name) + + filter_params = { + (field_name + '__iexact'): field + } + + exclude_params = { + (field_name + '__iexact'): current_field + } + + # lookin if there is any user registered + # with the new field + user = User.objects.filter( + **filter_params + ).exclude(**exclude_params) + + if user: + raise serializers.ValidationError( + f'This {field_name} is already in use.' + ) + + return field + + def validate_email(self, email): + return self.check_if_is_unique(email, 'email') + + def validate_username(self, username): + return self.check_if_is_unique(username, 'username') + + def validate_phone_number(self, phone_number): + if phone_number.isdigit(): + return phone_number + raise serializers.ValidationError('Invalid phone number') + + def validate_bio(self, bio): + if len(bio) <= 140: + return bio + raise serializers.ValidationError( + 'Bio field is longer than 140 characters' + ) + + def validate_birthdate(self, birthdate): + if birthdate < date.today(): + return birthdate + raise serializers.ValidationError('Invalid Date') + + class UserPreferedLanguage(serializers.ModelSerializer): class Meta: diff --git a/src/users/tests.py b/src/users/tests.py deleted file mode 100644 index 7dbcf8f..0000000 --- a/src/users/tests.py +++ /dev/null @@ -1,242 +0,0 @@ -from rest_framework.test import APITestCase -from django.urls import reverse -from django.db import IntegrityError - - -class UserRegistrationAPIViewTestCase(APITestCase): - url = reverse('users:register') - - def test_different_password_on_password_confirmation(self): - """ - Test to try to create a user with a - wrong verification password - """ - - user_data = { - "username": "vitas", - "email": "vitas@iAmGreat.com", - "password": "VitasIsNice", - "confirm_password": "VitasIsAwesome" - } - - response = self.client.post(self.url, user_data) - self.assertEqual(400, response.status_code) - - def test_password_less_than_8_characters(self): - """ - Test to try to create a user with a - password less than 8 characters - """ - - user_data = { - "username": "vitas", - "email": "vitas@iAmGreat.com", - "password": "HiVitas", - "confirm_password": "HiVitas" - } - - response = self.client.post(self.url, user_data) - self.assertEqual(400, response.status_code) - - def test_unique_email_validation(self): - """ - Test to try to create a user with a registered email - """ - - user1_data = { - "username": "vitas", - "email": "vitas@iAmGreat.com", - "password": "VitasIsAwesome", - "confirm_password": "VitasIsAwesome" - } - user2_data = { - "username": "Reanu_Reves", - "email": "vitas@iAmGreat.com", - "password": "cyberpunk2077", - "confirm_password": "cyberpunk2077" - } - response = self.client.post(self.url, user1_data) - - self.assertEqual(201, response.status_code) - - with self.assertRaises(IntegrityError): - response = self.client.post(self.url, user2_data) - - def test_unique_username_validation(self): - """ - Test to try to create a user with - a registered username - """ - - user_data = { - "username": "vitas", - "email": "vitas@iAmGreat.com", - "password": "VitasIsAwesome", - "confirm_password": "VitasIsAwesome" - } - - response = self.client.post(self.url, user_data) - self.assertEqual(201, response.status_code) - - user_data = { - "username": "vitas", - "email": "keanu@reeves.com", - "password": "cyberpunk2077", - "confirm_password": "cyberpunk2077" - } - - response = self.client.post(self.url, user_data) - self.assertEqual(400, response.status_code) - - def test_user_registration(self): - """ - Test to create a user with valid data - """ - - user_data = { - "username": "keanu_reeves", - "email": "keanu@reeves.com", - "password": "cyberpunk2077", - "confirm_password": "cyberpunk2077" - } - - response = self.client.post(self.url, user_data) - self.assertEqual(201, response.status_code) - - -class UserAuthenticationAPIViewTestCase(APITestCase): - signup_url = reverse('users:register') - token_url = reverse('users:token_obtain_pair') - refresh_url = reverse('users:token_refresh') - - user_cleber = { - "username": "cleber", - "email": "cleber@gmail.com", - "password": "cleber123", - "confirm_password": "cleber123" - } - - def create_user(self, user=user_cleber): - """ - Set's up user in database. - """ - - self.assertEqual( - 201, - self.client.post( - self.signup_url, - user - ).status_code, - msg='User setup failed' - ) - - def authenticate_user(self, user=user_cleber): - """ - Authenticates a set up user and returns it's tokens - """ - - response = self.client.post( - self.token_url, - { - "email": user['email'], - "password": user['password'] - } - ) - - self.assertEqual( - 200, - response.status_code, - msg='User authentication failed' - ) - - return response.data - - def test_authenticate_valid_user(self): - """ - Test authentication of valid user - """ - - self.create_user(self.user_cleber) - - authentication_data = { - "email": self.user_cleber['email'], - "password": self.user_cleber['password'], - } - - response = self.client.post( - self.token_url, - authentication_data - ) - - self.assertEqual(200, response.status_code) - - self.assertIsNotNone( - response.data['access'], - msg='No access token found' - ) - - self.assertIsNotNone( - response.data['refresh'], - msg='No refresh token found' - ) - - def test_failed_password_authentication(self): - """ - Tests if wrong password does now allow access to user - """ - - self.create_user(self.user_cleber) - - authentication_data = { - "email": self.user_cleber['email'], - "password": "asdfghjkl", # Wrong password - } - - response = self.client.post( - self.token_url, - authentication_data - ) - - self.assertEqual( - 401, - response.status_code, - ) - - def test_failed_email_authentication(self): - """ - Tests if wrong password does now allow access to user - """ - - self.create_user(self.user_cleber) - - authentication_data = { - "email": 'batata@gmail.com', # Wrong email - "password": self.user_cleber['password'], - } - - response = self.client.post( - self.token_url, - authentication_data - ) - - self.assertEqual(401, response.status_code) - - def test_get_access_token_from_valid_refresh_token(self): - """ - Tests if a valid refresh token can get - a new instance of a access token - """ - - self.create_user(self.user_cleber) - tokens = self.authenticate_user(self.user_cleber) - - response = self.client.post( - self.refresh_url, - {"refresh": tokens['refresh']} - ) - - self.assertEqual(200, response.status_code) - self.assertIsNotNone( - response.data['access'], - msg='No access token found' - ) diff --git a/src/users/tests/__init__.py b/src/users/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/users/tests/test_viewsets.py b/src/users/tests/test_viewsets.py new file mode 100644 index 0000000..614d7e3 --- /dev/null +++ b/src/users/tests/test_viewsets.py @@ -0,0 +1,541 @@ +from rest_framework.test import APITestCase +from django.urls import reverse + + +class UserRegistrationAPIViewTestCase(APITestCase): + url = reverse('users:register') + + def test_different_password_on_password_confirmation(self): + """ + Test to try to create a user with a + wrong verification password + """ + + user_data = { + "username": "vitas", + "email": "vitas@iAmGreat.com", + "password": "VitasIsNice", + "confirm_password": "VitasIsAwesome" + } + + response = self.client.post(self.url, user_data) + self.assertEqual(400, response.status_code) + + def test_password_less_than_8_characters(self): + """ + Test to try to create a user with a + password less than 8 characters + """ + + user_data = { + "username": "vitas", + "email": "vitas@iAmGreat.com", + "password": "HiVitas", + "confirm_password": "HiVitas" + } + + response = self.client.post(self.url, user_data) + self.assertEqual(400, response.status_code) + + def test_unique_username_validation(self): + """ + Test to try to create a user with + a registered username + """ + + user_data = { + "username": "vitas", + "email": "vitas@iAmGreat.com", + "password": "VitasIsAwesome", + "confirm_password": "VitasIsAwesome" + } + + response = self.client.post(self.url, user_data) + self.assertEqual(201, response.status_code) + + user_data = { + "username": "vitas", + "email": "keanu@reeves.com", + "password": "cyberpunk2077", + "confirm_password": "cyberpunk2077" + } + + response = self.client.post(self.url, user_data) + self.assertEqual(400, response.status_code) + + def test_user_registration(self): + """ + Test to create a user with valid data + """ + + user_data = { + "username": "keanu_reeves", + "email": "keanu@reeves.com", + "password": "cyberpunk2077", + "confirm_password": "cyberpunk2077" + } + + response = self.client.post(self.url, user_data) + self.assertEqual(201, response.status_code) + + +class UserAuthenticationAPIViewTestCase(APITestCase): + signup_url = reverse('users:register') + token_url = reverse('users:token_obtain_pair') + refresh_url = reverse('users:token_refresh') + + user_cleber = { + "username": "cleber", + "email": "cleber@gmail.com", + "password": "cleber123", + "confirm_password": "cleber123" + } + + def create_user(self, user=user_cleber): + """ + Set's up user in database. + """ + + response = self.client.post( + self.signup_url, + user + ) + + self.assertEqual( + response.status_code, + 201, + msg='User setup failed' + ) + + def authenticate_user(self, user=user_cleber): + """ + Authenticates a set up user and returns it's tokens + """ + + response = self.client.post( + self.token_url, + { + "email": user['email'], + "password": user['password'] + } + ) + + self.assertEqual( + 200, + response.status_code, + msg='User authentication failed' + ) + + return response.data + + def test_authenticate_valid_user(self): + """ + Test authentication of valid user + """ + + self.create_user(self.user_cleber) + + authentication_data = { + "email": self.user_cleber['email'], + "password": self.user_cleber['password'], + } + + response = self.client.post( + self.token_url, + authentication_data + ) + + self.assertEqual(200, response.status_code) + + self.assertIsNotNone( + response.data['access'], + msg='No access token found' + ) + + self.assertIsNotNone( + response.data['refresh'], + msg='No refresh token found' + ) + + def test_failed_password_authentication(self): + """ + Tests if wrong password does now allow access to user + """ + + self.create_user(self.user_cleber) + + authentication_data = { + "email": self.user_cleber['email'], + "password": "asdfghjkl", # Wrong password + } + + response = self.client.post( + self.token_url, + authentication_data + ) + + self.assertEqual( + 401, + response.status_code, + ) + + def test_failed_email_authentication(self): + """ + Tests if wrong password does now allow access to user + """ + + self.create_user(self.user_cleber) + + authentication_data = { + "email": 'batata@gmail.com', # Wrong email + "password": self.user_cleber['password'], + } + + response = self.client.post( + self.token_url, + authentication_data + ) + + self.assertEqual(401, response.status_code) + + def test_get_access_token_from_valid_refresh_token(self): + """ + Tests if a valid refresh token can get + a new instance of a access token + """ + + self.create_user(self.user_cleber) + tokens = self.authenticate_user(self.user_cleber) + + response = self.client.post( + self.refresh_url, + {"refresh": tokens['refresh']} + ) + + self.assertEqual(200, response.status_code) + self.assertIsNotNone( + response.data['access'], + msg='No access token found' + ) + + +class ProfileUpdateAPIViewTestCase(APITestCase): + + def create_authentication_tokens(self, user_credentials): + url_token = reverse('users:token_obtain_pair') + + response = self.client.post( + url_token, + user_credentials, + format='json' + ) + + self.assertEqual( + response.status_code, + 200, + msg='Failed to create user tokens credentials' + ) + + self.credentials = { + 'HTTP_AUTHORIZATION': 'Bearer ' + response.data['access'] + } + + def create_user(self, data=None): + user_data = { + 'username': 'vitas', + 'email': 'vitas@vitas.com', + 'password': 'vitasIsNice', + 'confirm_password': 'vitasIsNice' + } + + user_data = data if data else user_data + + url_user_signup = reverse('users:register') + + response = self.client.post( + url_user_signup, + user_data, + format='json' + ) + + self.assertEqual( + response.status_code, + 201, + msg='Failed during user creation' + ) + + user_data.pop('username') + user_data.pop('confirm_password') + + self.create_authentication_tokens(user_data) + + def setUp(self): + self.create_user() + self.url_user_profile = reverse('users:profile_update') + + def test_get_user_profile(self): + + response = self.client.get( + path=self.url_user_profile, + format='json', + **self.credentials, + ) + + self.assertEqual( + response.status_code, + 200, + msg='Failed to get user profile data' + ) + + data = response.data + + self.assertEqual( + data['bio'], + '', + msg='User profile bio isn\'t being set to empty by default' + ) + + self.assertEqual( + data['username'], + 'vitas', + msg='User username isn\'t being set correctly' + ) + + self.assertEqual( + data['email'], + 'vitas@vitas.com', + msg='User username isn\'t being set correctly' + ) + + self.assertIsNone( + data['photo'], + msg='Photo field not being set to None by default' + ) + + self.assertIsNone( + data['birthdate'], + msg='Birthdate field not being set to None by default' + ) + + def test_patch_update_user_profile(self): + + data = { + 'birthdate': '1990-01-01', + 'bio': ('Vitaliy Vladasovich Grachyov, better ' + + 'known by his stage name Vitas, is a ' + + 'Russian singer, author, composer and' + + ' actor.') + } + + response = self.client.patch( + path=self.url_user_profile, + data=data, + format='json', + **self.credentials, + ) + + self.assertEqual( + response.status_code, + 200, + msg='Failed to patch update user profile' + ) + + self.assertEqual( + response.data['birthdate'], + data['birthdate'], + msg='Failed set user birthdate' + ) + + self.assertEqual( + response.data['bio'], + data['bio'], + msg='Failed set user bio' + ) + + def test_change_username_to_one_already_in_use(self): + + user_data = { + 'username': 'LeBron James', + 'email': 'lebron@james.com', + 'password': 'IamTheGoat', + 'confirm_password': 'IamTheGoat' + } + + self.create_user(data=user_data) + + # that username is already in use + update_data = { + 'username': 'vitas' + } + + response = self.client.patch( + path=self.url_user_profile, + data=update_data, + format='json', + **self.credentials, + ) + + self.assertEqual( + response.status_code, + 400, + ) + + self.assertEqual( + str(response.data['username']), + ("[ErrorDetail(string='This username is " + + "already in use.', code='invalid')]"), + ) + + def test_get_prefered_language(self): + + url = reverse('users:set_prefered_language') + + response = self.client.get( + path=url, + **self.credentials, + ) + + self.assertEqual( + response.status_code, + 200, + ) + + self.assertEqual( + response.data['chosen_language'], + 'pt', + ) + + def test_update_prefered_language(self): + + url = reverse('users:set_prefered_language') + + data = { + 'chosen_language': 'en' + } + + response = self.client.patch( + path=url, + data=data, + format='json', + **self.credentials, + ) + + self.assertEqual( + response.status_code, + 200, + ) + + self.assertEqual( + response.data['chosen_language'], + 'en', + ) + + def test_access_token(self): + + url = reverse('users:test_access_token') + + response = self.client.get( + path=url, + **self.credentials, + ) + + self.assertEqual( + response.status_code, + 200, + ) + + response = self.client.get( + path=url, + ) + + self.assertEqual( + response.status_code, + 401, + ) + + def test_change_email_to_one_already_in_use(self): + + user_data = { + 'username': 'LeBron James', + 'email': 'lebron@james.com', + 'password': 'IamTheGoat', + 'confirm_password': 'IamTheGoat' + } + + self.create_user(data=user_data) + + # that email is already in use + update_data = { + 'email': 'vitas@vitas.com', + } + + response = self.client.patch( + path=self.url_user_profile, + data=update_data, + format='json', + **self.credentials, + ) + + self.assertEqual( + response.status_code, + 400, + ) + + self.assertEqual( + str(response.data['email']), + ("[ErrorDetail(string='This email is " + + "already in use.', code='invalid')]"), + ) + + def test_put_update_user_profile(self): + + data = { + 'username': 'LeBron James', + 'email': 'lebron@james.com', + 'birthdate': '1980-01-01', + 'bio': ('LeBron Raymone James Sr., is an ' + + 'American professional basketball ' + + 'player for the Los Angeles Lakers') + } + + self.client.put( + path=self.url_user_profile, + data=data, + format='json', + **self.credentials, + ) + + response = self.client.get( + path=self.url_user_profile, + **self.credentials, + ) + + self.assertEqual( + response.status_code, + 200, + msg='Failed to put update user profile' + ) + + self.assertEqual( + response.data['birthdate'], + data['birthdate'], + msg='Failed set user birthdate' + ) + + self.assertEqual( + response.data['username'], + data['username'], + msg='Failed set user username' + ) + + self.assertEqual( + response.data['email'], + data['email'], + msg='Failed set user email' + ) + + self.assertEqual( + response.data['bio'], + data['bio'], + msg='Failed set user bio' + ) diff --git a/src/users/urls.py b/src/users/urls.py index a0a0fb4..e20bbe8 100644 --- a/src/users/urls.py +++ b/src/users/urls.py @@ -5,6 +5,7 @@ UserRegistrationAPIView, RetrieveUpdatePreferedLanguageAPIView, test_access_token, + ProfileUpdateAPIView, CreateAccessToken, RefreshAccessToken, ) @@ -42,4 +43,9 @@ test_access_token, name='test_access_token' ), + path( + 'profile/', + ProfileUpdateAPIView.as_view(), + name='profile_update' + ), ] diff --git a/src/users/viewsets.py b/src/users/viewsets.py index d8de690..474c738 100644 --- a/src/users/viewsets.py +++ b/src/users/viewsets.py @@ -4,9 +4,12 @@ from rest_framework.generics import CreateAPIView, RetrieveUpdateAPIView from rest_framework.permissions import IsAuthenticated -from .models import User - -from .serializers import UserSignUpSerializer, UserPreferedLanguage +from .models import User, Profile +from .serializers import ( + UserSignUpSerializer, + UserPreferedLanguage, + ProfileModelSerializer +) from rest_framework_simplejwt.views import ( TokenObtainPairView, @@ -44,7 +47,7 @@ def get(self, request, *args, **kwargs): """ required_fields = { - 'meta': 'Refresh token sending`refresh token` in the request body', + 'meta': 'Send the `refresh token` in the request body', 'refresh': '', } @@ -62,6 +65,32 @@ class UserRegistrationAPIView(CreateAPIView): queryset = User.objects.all() +class ProfileUpdateAPIView(RetrieveUpdateAPIView): + """ + Endpoint for update profile info + """ + permission_classes = (IsAuthenticated, ) + serializer_class = ProfileModelSerializer + + def get_object(self): + return Profile.objects.get(user=self.request.user.id) + + def perform_update(self, serializer): + user_data = serializer.validated_data.pop('user', None) + + if user_data: + username = user_data.get('username', None) + email = user_data.get('email', None) + + user = self.request.user + + user.username = username if username else user.username + user.email = email if email else user.email + user.save() + + serializer.save() + + class RetrieveUpdatePreferedLanguageAPIView(RetrieveUpdateAPIView): """ Returns a signed in users's prefered language diff --git a/tox.ini b/tox.ini index 52d21f9..586680f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,2 +1,2 @@ [flake8] -exclude = .git,__pycache__,src/acacia/settings.py, */migrations/, src/users/migrations/,src/property/migrations/, src/harvest/migrations/ +exclude = __pycache__/ , settings.py, */migrations/ \ No newline at end of file