From f33d93c960a5b35a3954d83c8bbfd249f347d030 Mon Sep 17 00:00:00 2001 From: Adriano Todaro Date: Tue, 6 Sep 2016 18:32:06 +0200 Subject: [PATCH 1/6] Add user and user profile management Through the 'accounts' application, we provide a backend capable of managing user acounts registration and activation, user profile editing and user's password updating. An user can be created by a POST request to the 'user-list'. The creation of a user automaticaly triggers the creation of his corresponding profile (which right now contains information such as: gender, birthdate, location and nationality,and is easily upgradable in the future with new fields), which can be updated with PUT or PATCH requests to 'user-update-profile' endpoint. Furthermore at the end of the registration process, an activation mail will be sent to the user, providing a link that accepts a GET request, which if correctly delivered will set to "True" the is_active flag of the corresponding user object. Finally a user can change is own password instantiating a POST request to the 'user-change-password' endpoint. --- accounts/admin.py | 5 + accounts/models.py | 32 ++ accounts/permissions.py | 18 + accounts/serializers.py | 51 +++ accounts/tests.py | 339 ++++++++++++++++++ accounts/urls.py | 15 + accounts/utils.py | 44 +++ accounts/views.py | 45 +++ accounts/viewsets.py | 67 ++++ api/migrations/__init__.py | 0 .../activation_mail_body_template.txt | 4 + .../activation_mail_subject_template.txt | 1 + api/urls.py | 1 + xea_core/settings.py | 12 + 14 files changed, 634 insertions(+) create mode 100644 accounts/admin.py create mode 100644 accounts/models.py create mode 100644 accounts/permissions.py create mode 100644 accounts/serializers.py create mode 100644 accounts/tests.py create mode 100644 accounts/urls.py create mode 100644 accounts/utils.py create mode 100644 accounts/views.py create mode 100644 accounts/viewsets.py delete mode 100644 api/migrations/__init__.py create mode 100644 api/templates/activation_mail_body_template.txt create mode 100644 api/templates/activation_mail_subject_template.txt diff --git a/accounts/admin.py b/accounts/admin.py new file mode 100644 index 0000000..f5bccb6 --- /dev/null +++ b/accounts/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin + +from .models import UserProfile + +admin.site.register(UserProfile) diff --git a/accounts/models.py b/accounts/models.py new file mode 100644 index 0000000..ea9df08 --- /dev/null +++ b/accounts/models.py @@ -0,0 +1,32 @@ +from django.contrib.auth.models import User +from django.db import models +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.utils.translation import ugettext_lazy as _ + + + +class UserProfile(models.Model): + class Meta: + app_label = 'accounts' + + # Gender strings + MALE = 'M' + FEMALE = 'F' + OTHER = 'O' + + GENDER_CHOICES = ((MALE, _('Male')), (FEMALE, _('Female')), (OTHER, _('Other'))) + + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') + gender = models.CharField(max_length=1, choices=GENDER_CHOICES, blank=True) + birthday = models.DateField(null=True) + nationality = models.CharField(max_length=140, blank=True) # Or use django_countries? + location = models.CharField(max_length=300, blank=True) + + @receiver(post_save, sender=User) + def create_user_profile(sender, instance, created, **kwargs): + if created: + UserProfile.objects.create(user=instance) + + def __str__(self): + return u'Profile of user: %s' % self.user.username diff --git a/accounts/permissions.py b/accounts/permissions.py new file mode 100644 index 0000000..874c88c --- /dev/null +++ b/accounts/permissions.py @@ -0,0 +1,18 @@ +from rest_framework import permissions + + +class IsAdminOrSelf(permissions.BasePermission): + def get_owner(self, obj): + return obj + + def has_object_permission(self, request, view, obj): + if request.user.is_superuser: + return True + if request.user == self.get_owner(obj): + return True + return False + + +class IsProfileOwnerOrStaff(IsAdminOrSelf): + def get_owner(self, obj): + return obj.user diff --git a/accounts/serializers.py b/accounts/serializers.py new file mode 100644 index 0000000..7be075b --- /dev/null +++ b/accounts/serializers.py @@ -0,0 +1,51 @@ +from django.contrib.auth import get_user_model +from rest_framework import serializers +from .utils import send_activation_mail +from .models import UserProfile + + +class UserProfileSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = UserProfile + fields = ('id', 'gender', 'birthday', 'nationality', 'location') + + def update(self, instance, validated_data): + instance.gender = validated_data.get('gender', instance.gender) + instance.birthday = validated_data.get('birthday', instance.birthday) + instance.nationality = validated_data.get('nationality', instance.nationality) + instance.location = validated_data.get('location', instance.location) + instance.save() + return instance + +class UserSerializer(serializers.HyperlinkedModelSerializer): + profile = UserProfileSerializer() + + class Meta: + model = get_user_model() + fields = ('url', 'username', 'password', 'email', 'first_name', 'last_name', 'profile') + write_only_fields = ('password',) + + def create(self, validated_data): + """ + Here the new User object is created. + At the end of the process we will send and activation mail to the user's email address + """ + user = get_user_model().objects.create( + username=validated_data['username'], + email=validated_data['email'], + first_name=validated_data['first_name'], + last_name=validated_data['last_name'], + is_active=False + ) + + user.set_password(validated_data['password']) + user.save() + send_activation_mail(user) + return user + + +class UserPasswordSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = get_user_model() + fields = ('url', 'password') + diff --git a/accounts/tests.py b/accounts/tests.py new file mode 100644 index 0000000..a4f58e3 --- /dev/null +++ b/accounts/tests.py @@ -0,0 +1,339 @@ + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import User +from django.core import mail +from django.core.urlresolvers import reverse +from rest_framework import status +from rest_framework.test import APITestCase +from .models import UserProfile + +# Default test user data +username = 'user' +first_name = 'Firstname' +last_name = 'Lastname' +email = 'user@mydomain.com' +password = 'user123' + +# Urls we are working with +registration_url = reverse('user-list') + +DEFAULT_NUMBER_OF_CLIENTS_FOR_TESTS = 3 + + +def get_default_user(): + user = User(username=username, + password=password, + first_name=first_name, + last_name=last_name, + email=email) + return user + + +def get_n_users(n): + """ + This generates n users with their data + :param n: The number of users we want to create + :return: + """ + user = [] + for i in range(1, n + 1): + myusername = username + str(i) + myfirst_name = first_name + str(i) + mylast_name = last_name + str(i) + myemail = 'user' + str(i) + '@mydomain.com' + mypassword = password + newuser = User(username=myusername, + password=mypassword, + first_name=myfirst_name, + last_name=mylast_name, + email=myemail) + user.append(newuser) + return user + + +def find_between(string, substring1, substring2): + try: + start = string.index(substring1) + len(substring1) + end = string.index(substring2, start) + return string[start:end] + except ValueError: + return "" + + +def get_userid_from_url(url): + """ + This function is user id to obtain the user id from the url provided by the API" + """ + + return find_between(url, 'users/', '/') + + +def create_n_users(n): + username_base = 'user' + for i in range(0, n): + user = get_user_model().objects.create(username=username_base + str(i)) + + +def get_signup_payload_from_user(user): + """ + This function builds the payload used for registration requests from the user's data + :param user: + :return: + """ + + profile_payload = {'gender': '', + 'birthdate': '', + 'nationality': '', + 'location': ''} + + payload = {'username': user.username, + 'first_name': user.first_name, + 'last_name': user.last_name, + 'email': user.email, + 'password': user.password, + 'profile': profile_payload + } + + return payload + + +def get_activation_url_from_email(email): + """ + here we parse the activation email to get the activation URL + :param email: the activation email + :return: The URL we parsed + """ + url = "http" + email.body.split("http", 1)[1] + return url + + +class RegistrationTest(APITestCase): + def register_user(self): + """ + A simple function used to register a new user by sending a POST to the registration endpoint + :return: The id (pk) of the newly registered user + """ + id = self.register_n_users(1)[0] + return id + + def register_n_users(self, n): + user = get_n_users(n) + user_url = [] + for i in range(0, n): + response = self.client.post(registration_url, get_signup_payload_from_user(user[i]), format='json') + self.assertEquals(response.status_code, status.HTTP_201_CREATED) + user_url.append(response.data['url']) + return user_url + + def get_activation_email(self): + """ + This function retrieves the activation mail from the mailbox + :return: """ + self.assertEquals(len(mail.outbox), 1) + activation_email = mail.outbox[0] + return activation_email + + def get_n_activation_mails(self, n): + self.assertEquals(len(mail.outbox), n) + return mail.outbox + + def test_register_user(self): + """ + In this test we verify that after a new user is registered we can find him by querying the database + We also verify that his "is_active" flag is set to False (he needs to activate trough the email yet) + :return: + """ + url = self.register_n_users(1)[0] + uid = get_userid_from_url(url) + queryset = User.objects.all().filter(pk=uid) + self.assertTrue(queryset.exists()) + user = queryset[0] + self.assertFalse(user.is_active) + return uid + + def test_activate_user(self): + """ + Here we verify that a user can be activated through a request to the URL we provide + :return: + """ + self.register_user() + activation_email = self.get_activation_email() + url = get_activation_url_from_email(activation_email) + response = self.client.get(url) + self.assertEquals(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_reusing_activation_link(self): + """ + If a client tries to access the activation URL but the user is already active the response should be a HTTP 403 + :return: + """ + self.register_user() + activation_email = self.get_activation_email() + url = get_activation_url_from_email(activation_email) + response1 = self.client.get(url) + self.assertEquals(response1.status_code, status.HTTP_204_NO_CONTENT) + response2 = self.client.get(url) + self.assertEquals(response2.status_code, status.HTTP_403_FORBIDDEN) + + def test_not_existing_entry_activation_link(self): + """ + Here we prove that we forbid the access to a URL that is not associated with a registered user + :return: + """ + self.register_user() + url = reverse('activation', kwargs={'upk': 8, + 'token': '4eh-xxxxxxxxxxxxxxxx'}) + response = self.client.get(url) + self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_not_valid_token(self): + """ + Similar to the previous one, now we will try to use a valid uidb64 with a not valid token + :return: + """ + url = self.register_user() + upk = get_userid_from_url(url) + url = reverse('activation', kwargs={'upk': upk, 'token': '4eh-xxxxxxxxxxxxxxxx'}) + response = self.client.get(url) + self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_n_clients_activation_reverse_order(self): + """ + in this test we register multiple clients and activate them in reverse order to prove that using an activation + link doesn't invalidate another one + :return: + """ + self.register_n_users(DEFAULT_NUMBER_OF_CLIENTS_FOR_TESTS) + mails = self.get_n_activation_mails(DEFAULT_NUMBER_OF_CLIENTS_FOR_TESTS) + for i in range(DEFAULT_NUMBER_OF_CLIENTS_FOR_TESTS - 1, -1, -1): + activation_url = get_activation_url_from_email(mails[i]) + response = self.client.get(activation_url) + self.assertEquals(response.status_code, status.HTTP_204_NO_CONTENT) + + +class ChangePasswordTest(APITestCase): + def setUp(self): + create_n_users(DEFAULT_NUMBER_OF_CLIENTS_FOR_TESTS) + + def get_change_password_detail_url(self, pk): + url = reverse('user-change-password', kwargs={'pk': pk}) + return url + + def test_user_changes_his_password(self): + """ + In this scenario the user tries to update his password. We also prove that a GET request to the url will result + in a 405 response (we only allow POST) + :return: + """ + user = get_user_model().objects.get(pk=1) + self.client.force_login(user) + url = self.get_change_password_detail_url(1) + response1 = self.client.get(url) + self.assertEquals(response1.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + response2 = self.client.post(url, {'password': 'newpassword'}) + self.assertEquals(response2.status_code, status.HTTP_200_OK) + + def test_admin_changes_user_password(self): + """ + Here we verify that also a staff user can modify the user's password + :return: + """ + url = self.get_change_password_detail_url(1) + admin = User.objects.create_superuser('admin', 'admin@domain.test', 'password') + self.client.force_login(admin) + response = self.client.post(url, {'password': 'newpassword'}) + self.assertEquals(response.status_code, status.HTTP_200_OK) + + def test_user_changes_other_account_password(self): + """ + Here a user tries to change someone else's password + :return: + """ + url = self.get_change_password_detail_url(1) + user = get_user_model().objects.get(pk=2) + self.client.force_login(user) + response = self.client.post(url, {'password': 'newpassword'}) + self.assertEquals(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_unauthenticated_changes_password(self): + """ + Here an unauthenticated user tries to change someone's password + :return: + """ + url = self.get_change_password_detail_url(1) + response = self.client.post(url, {'password': 'newpassword'}) + self.assertEquals(response.status_code, status.HTTP_401_UNAUTHORIZED) + + +class UserProfileTest(APITestCase): + default_test_payload = {"gender": "M", "nationality": "Argosdfdegfsey", "location": "palermo"} + + def setUp(self): + create_n_users(DEFAULT_NUMBER_OF_CLIENTS_FOR_TESTS) + + def test_profile_creation(self): + """ + Here we verify that creating a new user we automatically generate for him a new user profile + :return: + """ + queryset = UserProfile.objects.all() + self.assertEquals(len(queryset), DEFAULT_NUMBER_OF_CLIENTS_FOR_TESTS) + + def test_profile_updated_by_owner(self): + queryset = User.objects.all() + user = queryset[0] + self.client.force_login(user) + url = reverse('user-update-profile', kwargs={'pk': user.pk}) + response = self.client.put(path=url, data=self.default_test_payload) + self.assertEquals(response.status_code, status.HTTP_200_OK) + profile = UserProfile.objects.get(pk=user.pk) + self.assertEquals(profile.gender, self.default_test_payload.get('gender')) + + def test_profile_updated_by_staff(self): + """ + Here we prove that a superuser can modify anyone else's profile + :return: + """ + admin = User.objects.create_superuser('admin', 'admin@domain.test', 'password') + self.client.force_login(admin) + url = reverse('user-update-profile', kwargs={'pk': 1}) + response = self.client.put(path=url, data=self.default_test_payload) + self.assertEquals(response.status_code, status.HTTP_200_OK) + + def test_profile_updated_by_another_logged_user(self): + """ + Here a logged user tries to modify someone else's profile. The server answers with a 403 + :return: + """ + + user = get_user_model().objects.get(pk=2) + self.client.force_login(user) + url = reverse('user-update-profile', kwargs={'pk': 1}) + response = self.client.put(path=url, data=self.default_test_payload) + self.assertEquals(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_profile_updated_by_guest(self): + url = reverse('user-update-profile', kwargs={'pk': 1}) + response = self.client.put(path=url, data=self.default_test_payload) + self.assertEquals(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_get_user_profile(self): + user = get_user_model().objects.get(pk=1) + self.client.force_login(user) + url = reverse('user-detail', kwargs={'pk': user.pk}) + response = self.client.get(url) + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertTrue('profile' in response.data) + + def test_profile_update_after_password_change(self): + user = get_user_model().objects.get(pk=1) + change_password_url = reverse('user-change-password', kwargs={'pk': user.pk}) + profile_url = reverse('user-update-profile', kwargs={'pk': user.pk}) + self.client.force_login(user) + response1 = self.client.put(path=profile_url, data=self.default_test_payload) + self.assertEquals(response1.status_code, status.HTTP_200_OK) + response2 = self.client.post(path=change_password_url, data={'password': 'password2'}) + self.assertEqual(response2.status_code, status.HTTP_200_OK) + response3 = self.client.put(path=profile_url, data=self.default_test_payload) + self.assertEqual(response2.status_code, status.HTTP_401_UNAUTHORIZED) + diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000..9c2e8cd --- /dev/null +++ b/accounts/urls.py @@ -0,0 +1,15 @@ +from django.conf.urls import url, include +from rest_framework import routers +from .viewsets import UserViewSet +from .views import ActivateUserView + +router = routers.SimpleRouter() +router.register(r'users', UserViewSet) + + +urlpatterns = [ + url(r'^activation/(?P[0-9]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + ActivateUserView.as_view(), name='activation'), + + url(r'', include(router.urls)), +] \ No newline at end of file diff --git a/accounts/utils.py b/accounts/utils.py new file mode 100644 index 0000000..83bcff9 --- /dev/null +++ b/accounts/utils.py @@ -0,0 +1,44 @@ +from django.core.mail import send_mail +from django.conf import settings +from django.core.urlresolvers import reverse +from django.template.loader import get_template +from django.contrib.auth.tokens import default_token_generator + + + + +def send_activation_mail(user): + """ + This funcion is used to send a mail containing the activation link to the new user's email address + :param user: The user object related to the new user account + :return: + """ + url = build_activation_url(user) + username = user.username + to_email = user.email + body_context = {'username': username, 'host': settings.SITE_HOST, 'url': url} + subj_template = get_template('activation_mail_subject_template.txt') + subj = subj_template.render() + msg_template = get_template('activation_mail_body_template.txt') + msg = msg_template.render(body_context) + print(msg) + send_mail(subject=subj, message=msg, + from_email=settings.EMAIL_HOST_USER, recipient_list=[to_email], fail_silently=False) + + + +def get_token(user): + token = default_token_generator.make_token(user) + return token + + +def validate_token(user, token): + is_valid = default_token_generator.check_token(user, token) + return is_valid + + +def build_activation_url(user): + upk = user.pk + token = get_token(user) + url = reverse('activation', kwargs={'upk': upk, 'token': token}) + return url diff --git a/accounts/views.py b/accounts/views.py new file mode 100644 index 0000000..394a35e --- /dev/null +++ b/accounts/views.py @@ -0,0 +1,45 @@ +from rest_framework import permissions +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView +from django.contrib.auth import get_user_model +from . import serializers +from . import utils + + +class ActivateUserView(APIView): + model = get_user_model() + permission_classes = (permissions.AllowAny,) + serializer_class = serializers.UserSerializer + + def get(self, request, upk=None, token=None): + # First, if client is accessing to the activation url's root we have to refuse his request + if upk is None or token is None: + return Response({'msg': 'This activation link is not valid'}, status=status.HTTP_403_FORBIDDEN) + # Then we start validating those two fields + try: + user = self.get_user_to_activate(upk) + except get_user_model().DoesNotExist: + return Response({'msg': 'This activation link is not valid'}, status=status.HTTP_400_BAD_REQUEST) + + # Now validate the token + if utils.validate_token(user, token): # If it's valid we can start activation process + return self.activate_user(user) + + # Otherwise we send a 400 + return Response({}, + status=status.HTTP_400_BAD_REQUEST) + + def activate_user(self, user): + if user.is_active: # If the user is using an activation link being already active we send a 403 + return Response({}, + status=status.HTTP_403_FORBIDDEN) + else: # Otherwise we can activate the account + user.is_active = True + user.save() + return Response({'msg': 'The account had been activated correctly'}, + status=status.HTTP_204_NO_CONTENT) + + def get_user_to_activate(self, upk): + user = get_user_model().objects.get(pk=upk) + return user diff --git a/accounts/viewsets.py b/accounts/viewsets.py new file mode 100644 index 0000000..4a08f63 --- /dev/null +++ b/accounts/viewsets.py @@ -0,0 +1,67 @@ +from django.contrib.auth import get_user_model +from rest_framework import status +from rest_framework import viewsets +from rest_framework.decorators import detail_route +from rest_framework.response import Response + +from .serializers import UserSerializer, UserPasswordSerializer +from .permissions import IsAdminOrSelf +from .models import UserProfile +from .serializers import UserProfileSerializer + + +class UserViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows users to be viewed or edited. + """ + queryset = get_user_model().objects.all() + serializer_class = UserSerializer + + def get_serializer_class(self): + if self.action == 'change_password': + return UserPasswordSerializer + if self.action == 'update_profile': + return UserProfileSerializer + else: + return UserSerializer + + def update(self, request, *args, **kwargs): + user = self.get_object() + serializer = UserSerializer(data=request.data, context={'request': request}) + if serializer.is_valid(): + user.save() + return Response(status=status.HTTP_200_OK) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @detail_route(methods=['post'], permission_classes=[IsAdminOrSelf]) + def change_password(self, request, pk=None): + if not pk: + return Response(status=status.HTTP_400_BAD_REQUEST) + user = get_user_model().objects.get(pk=pk) + self.check_object_permissions(request, user) + serializer = UserPasswordSerializer(data=request.data, context={'request': request}) + + if serializer.is_valid(): + user.set_password(serializer.validated_data['password']) + + user.save() + return Response(status=status.HTTP_200_OK) + else: + return Response(serializer.errors, + status=status.HTTP_400_BAD_REQUEST) + + @detail_route(methods=['put', 'patch'], permission_classes=[IsAdminOrSelf]) + def update_profile(self, request, pk=None): + if not pk: + return Response(status=status.HTTP_400_BAD_REQUEST) + user = self.get_object() + profile = UserProfile.objects.get(user=user) + self.check_object_permissions(request, user) + serializer = UserProfileSerializer(data=request.data, context={'request': request}, partial=True) + if serializer.is_valid(): + serializer.update(profile, serializer.validated_data) + return Response(status=status.HTTP_200_OK) + else: + return Response(serializer.errors, + status=status.HTTP_400_BAD_REQUEST) diff --git a/api/migrations/__init__.py b/api/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/api/templates/activation_mail_body_template.txt b/api/templates/activation_mail_body_template.txt new file mode 100644 index 0000000..94489c4 --- /dev/null +++ b/api/templates/activation_mail_body_template.txt @@ -0,0 +1,4 @@ +Hello {{username}}! Your account needs to be activated. +Click on the following URL and follow the instructions to get started: + +{{host}}{{url}} \ No newline at end of file diff --git a/api/templates/activation_mail_subject_template.txt b/api/templates/activation_mail_subject_template.txt new file mode 100644 index 0000000..4c83bd0 --- /dev/null +++ b/api/templates/activation_mail_subject_template.txt @@ -0,0 +1 @@ +Your account needs to be activated \ No newline at end of file diff --git a/api/urls.py b/api/urls.py index e3e2aad..803900f 100644 --- a/api/urls.py +++ b/api/urls.py @@ -23,4 +23,5 @@ urlpatterns = [ url(r'', include(router.urls)), + url(r'accounts/', include('accounts.urls')), ] diff --git a/xea_core/settings.py b/xea_core/settings.py index bdad574..c5ee688 100644 --- a/xea_core/settings.py +++ b/xea_core/settings.py @@ -43,6 +43,7 @@ 'knox', 'jwt_knox', 'api', + 'accounts', ] MIDDLEWARE_CLASSES = [ @@ -140,3 +141,14 @@ # https://docs.djangoproject.com/en/1.9/howto/static-files/ STATIC_URL = '/static/' + +# Email settings + +EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') +EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_HOST_USER') +EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_HOST_PASSWORD') +EMAIL_USE_TLS = os.environ.get('DJANGO_EMAIL_USE_TLS', True) + +# Our site's HOST + +SITE_HOST = 'http://127.0.0.1:8000' From 07a6c88e7563257f6815965cd0ace47721120a48 Mon Sep 17 00:00:00 2001 From: Adriano Todaro Date: Thu, 8 Sep 2016 09:57:50 +0200 Subject: [PATCH 2/6] Places can be updated --- accounts/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accounts/serializers.py b/accounts/serializers.py index 7be075b..1a0aec4 100644 --- a/accounts/serializers.py +++ b/accounts/serializers.py @@ -17,6 +17,7 @@ def update(self, instance, validated_data): instance.save() return instance + class UserSerializer(serializers.HyperlinkedModelSerializer): profile = UserProfileSerializer() @@ -48,4 +49,3 @@ class UserPasswordSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = get_user_model() fields = ('url', 'password') - From 2ae2f2781dbc5658ebbb3be2132198c2ab08fd6e Mon Sep 17 00:00:00 2001 From: Adriano Todaro Date: Thu, 8 Sep 2016 10:57:01 +0200 Subject: [PATCH 3/6] Add user and user profile management Through the 'accounts' application, we provide a backend capable of managing user accounts registration and activation, user profile editing and user's password updating. An user can be created by a POST request to the 'user-list'. The creation of a user automatically triggers the creation of his corresponding profile (which right now contains information such as: gender, birth date, location and nationality,and is easily upgradable in the future with new fields), which can be updated with PUT or PATCH requests to 'user-update-profile' endpoint. Furthermore at the end of the registration process, an activation mail will be sent to the user, providing a link that accepts a GET request, which if correctly delivered will set to "True" the is_active flag of the corresponding user object. Finally a user can change is own password instantiating a POST request to the 'user-change-password' endpoint. --- accounts/urls.py | 8 +++++--- accounts/utils.py | 6 +++--- accounts/views.py | 45 -------------------------------------------- accounts/viewsets.py | 35 +++++++++++++++++++++++++++++++++- 4 files changed, 42 insertions(+), 52 deletions(-) delete mode 100644 accounts/views.py diff --git a/accounts/urls.py b/accounts/urls.py index 9c2e8cd..b899cf6 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -1,15 +1,17 @@ from django.conf.urls import url, include from rest_framework import routers from .viewsets import UserViewSet -from .views import ActivateUserView router = routers.SimpleRouter() router.register(r'users', UserViewSet) +activation = UserViewSet.as_view({ + 'get': 'activate' +}) urlpatterns = [ - url(r'^activation/(?P[0-9]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', - ActivateUserView.as_view(), name='activation'), + url(r'^users/(?P[0-9]+)/activation/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + activation, name='activation'), url(r'', include(router.urls)), ] \ No newline at end of file diff --git a/accounts/utils.py b/accounts/utils.py index 83bcff9..ae83c26 100644 --- a/accounts/utils.py +++ b/accounts/utils.py @@ -1,10 +1,11 @@ +import logging from django.core.mail import send_mail from django.conf import settings from django.core.urlresolvers import reverse from django.template.loader import get_template from django.contrib.auth.tokens import default_token_generator - +logger = logging.getLogger(__name__) def send_activation_mail(user): @@ -21,12 +22,11 @@ def send_activation_mail(user): subj = subj_template.render() msg_template = get_template('activation_mail_body_template.txt') msg = msg_template.render(body_context) - print(msg) + logger.info('Sending activation mail to \n' + to_email + '\n' + msg) send_mail(subject=subj, message=msg, from_email=settings.EMAIL_HOST_USER, recipient_list=[to_email], fail_silently=False) - def get_token(user): token = default_token_generator.make_token(user) return token diff --git a/accounts/views.py b/accounts/views.py deleted file mode 100644 index 394a35e..0000000 --- a/accounts/views.py +++ /dev/null @@ -1,45 +0,0 @@ -from rest_framework import permissions -from rest_framework import status -from rest_framework.response import Response -from rest_framework.views import APIView -from django.contrib.auth import get_user_model -from . import serializers -from . import utils - - -class ActivateUserView(APIView): - model = get_user_model() - permission_classes = (permissions.AllowAny,) - serializer_class = serializers.UserSerializer - - def get(self, request, upk=None, token=None): - # First, if client is accessing to the activation url's root we have to refuse his request - if upk is None or token is None: - return Response({'msg': 'This activation link is not valid'}, status=status.HTTP_403_FORBIDDEN) - # Then we start validating those two fields - try: - user = self.get_user_to_activate(upk) - except get_user_model().DoesNotExist: - return Response({'msg': 'This activation link is not valid'}, status=status.HTTP_400_BAD_REQUEST) - - # Now validate the token - if utils.validate_token(user, token): # If it's valid we can start activation process - return self.activate_user(user) - - # Otherwise we send a 400 - return Response({}, - status=status.HTTP_400_BAD_REQUEST) - - def activate_user(self, user): - if user.is_active: # If the user is using an activation link being already active we send a 403 - return Response({}, - status=status.HTTP_403_FORBIDDEN) - else: # Otherwise we can activate the account - user.is_active = True - user.save() - return Response({'msg': 'The account had been activated correctly'}, - status=status.HTTP_204_NO_CONTENT) - - def get_user_to_activate(self, upk): - user = get_user_model().objects.get(pk=upk) - return user diff --git a/accounts/viewsets.py b/accounts/viewsets.py index 4a08f63..fcf2833 100644 --- a/accounts/viewsets.py +++ b/accounts/viewsets.py @@ -8,6 +8,7 @@ from .permissions import IsAdminOrSelf from .models import UserProfile from .serializers import UserProfileSerializer +from . import utils class UserViewSet(viewsets.ModelViewSet): @@ -44,7 +45,6 @@ def change_password(self, request, pk=None): if serializer.is_valid(): user.set_password(serializer.validated_data['password']) - user.save() return Response(status=status.HTTP_200_OK) else: @@ -65,3 +65,36 @@ def update_profile(self, request, pk=None): else: return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @detail_route(methods=['get'], permission_classes=[IsAdminOrSelf]) + def activate(self, request, upk=None, token=None): + # First, if client is accessing to the activation url's root we have to refuse his request + if upk is None or token is None: + return Response({'msg': 'This activation link is not valid'}, status=status.HTTP_403_FORBIDDEN) + # Then we start validating those two fields + try: + user = self.get_user_to_activate(upk) + except get_user_model().DoesNotExist: + return Response({'msg': 'This activation link is not valid'}, status=status.HTTP_400_BAD_REQUEST) + + # Now validate the token + if utils.validate_token(user, token): # If it's valid we can start activation process + return self.activate_user(user) + + # Otherwise we send a 400 + return Response({}, + status=status.HTTP_400_BAD_REQUEST) + + def activate_user(self, user): + if user.is_active: # If the user is using an activation link being already active we send a 403 + return Response({}, + status=status.HTTP_403_FORBIDDEN) + else: # Otherwise we can activate the account + user.is_active = True + user.save() + return Response({'msg': 'The account had been activated correctly'}, + status=status.HTTP_204_NO_CONTENT) + + def get_user_to_activate(self, upk): + user = get_user_model().objects.get(pk=upk) + return user From 30e645f5f47983ccf65c9e35f2c288cbd6420fd4 Mon Sep 17 00:00:00 2001 From: Adriano Todaro Date: Thu, 8 Sep 2016 15:52:53 +0200 Subject: [PATCH 4/6] Add spaces management The code introduces a new model called "Place" that represents a physical site that can host events. It features fields that can store general information (name, description and owner) plus an Address field, which is actually a separate model that encapsulates all the infos about the physical location of the Place. Common CRUD operations are supported. A specific @detail_route had been created for the update operation to deal with the nested kind of structure of the "Place" entity. --- accounts/permissions.py | 5 ++- api/urls.py | 1 + spaces/admin.py | 5 +++ spaces/models.py | 23 ++++++++++ spaces/serializers.py | 47 ++++++++++++++++++++ spaces/tests.py | 95 +++++++++++++++++++++++++++++++++++++++++ spaces/urls.py | 13 ++++++ spaces/viewsets.py | 32 ++++++++++++++ xea_core/settings.py | 1 + 9 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 spaces/admin.py create mode 100644 spaces/models.py create mode 100644 spaces/serializers.py create mode 100644 spaces/tests.py create mode 100644 spaces/urls.py create mode 100644 spaces/viewsets.py diff --git a/accounts/permissions.py b/accounts/permissions.py index 874c88c..0486099 100644 --- a/accounts/permissions.py +++ b/accounts/permissions.py @@ -13,6 +13,7 @@ def has_object_permission(self, request, view, obj): return False -class IsProfileOwnerOrStaff(IsAdminOrSelf): +class IsPlaceOwnerOrStaff(IsAdminOrSelf): def get_owner(self, obj): - return obj.user + return obj.owner + diff --git a/api/urls.py b/api/urls.py index 803900f..6e2f3c5 100644 --- a/api/urls.py +++ b/api/urls.py @@ -24,4 +24,5 @@ urlpatterns = [ url(r'', include(router.urls)), url(r'accounts/', include('accounts.urls')), + url(r'spaces/', include('spaces.urls')), ] diff --git a/spaces/admin.py b/spaces/admin.py new file mode 100644 index 0000000..5e69c09 --- /dev/null +++ b/spaces/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin + +from .models import Place + +admin.site.register(Place) diff --git a/spaces/models.py b/spaces/models.py new file mode 100644 index 0000000..39a6afb --- /dev/null +++ b/spaces/models.py @@ -0,0 +1,23 @@ +from django.db import models +from django.contrib.auth.models import User + + +class Address(models.Model): + street_name = models.CharField(max_length=140) + civic = models.CharField(max_length=20) + floor = models.IntegerField(blank=True) + door = models.CharField(max_length=2, blank=True) + city = models.CharField(max_length=140) + postal_code = models.IntegerField(blank=True) + region = models.CharField(max_length=140) + nation = models.CharField(max_length=140) + + +class Place(models.Model): + class Meta: + app_label = 'spaces' + + name = models.CharField(max_length=140) + owner = models.ForeignKey(User, on_delete=models.CASCADE) + description = models.TextField(max_length=500) + address = models.ForeignKey(Address) diff --git a/spaces/serializers.py b/spaces/serializers.py new file mode 100644 index 0000000..978fabb --- /dev/null +++ b/spaces/serializers.py @@ -0,0 +1,47 @@ +from rest_framework import serializers + +from .models import Address, Place + + +class AddressSerializer(serializers.ModelSerializer): + class Meta: + model = Address + fields = ('street_name', 'civic', 'floor', 'door', 'city', 'postal_code', 'region', 'nation') + + +class PlaceSerializer(serializers.HyperlinkedModelSerializer): + address = AddressSerializer() + + class Meta: + model = Place + fields = ('url', 'name', 'owner', 'description', 'address') + + def update(self, instance, validated_data): + instance.name = validated_data.get('name', instance.name) + instance.owner = validated_data.get('owner', instance.owner) + instance.description = validated_data.get('nationality', instance.description) + instance.address = validated_data.get('address', instance.address) + instance.save() + return instance + + def create(self, validated_data): + address_data = validated_data.get('address') + address = Address.objects.create(street_name=address_data.get('street_name'), + civic=address_data.get('civic'), + floor=address_data.get('floor'), + door=address_data.get('door'), + city=address_data.get('city'), + postal_code=address_data.get('postal_code'), + region=address_data.get('region'), + nation=address_data.get('nation') + ) + address.save() + place = Place.objects.create( + name=validated_data.get('name'), + owner=validated_data.get('owner'), + description=validated_data.get('description'), + address=address + ) + + place.save() + return place diff --git a/spaces/tests.py b/spaces/tests.py new file mode 100644 index 0000000..60d2490 --- /dev/null +++ b/spaces/tests.py @@ -0,0 +1,95 @@ +from django.contrib.auth import get_user_model +from django.core.urlresolvers import reverse +from rest_framework import status +from rest_framework.test import APITestCase +from accounts.tests import create_n_users +from .models import Place + + +class PlacesTest(APITestCase): + # Default place data + + name = 'place' + description = 'A very nice place' + + # Default address data + + street = 'streetname' + civic = '123' + floor = '10' + door = 'c' + city = 'La Coruña' + postal_code = '15005' + region = 'Galicia' + nation = 'Spain' + main_url = reverse('place-list') + + address_payload = {'street_name': street, + 'civic': civic, + 'floor': floor, + 'door': door, + 'city': city, + 'postal_code': postal_code, + 'region': region, + 'nation': nation + } + + place_payload = {'name': name, + 'description': description, + 'address': address_payload + } + + DEFAULT_USERS_NUMBER = 3 + DEFAULT_PLACES_NUMBER = 3 + + def setUp(self): + create_n_users(self.DEFAULT_USERS_NUMBER) + + def create_n_places(self, n): + payload = self.place_payload + for i in range(0, n): + owner_url = reverse('user-detail', kwargs={'pk': n % self.DEFAULT_USERS_NUMBER + 1}) + payload.update({'owner': owner_url, 'name': self.name + str(n)}) + response = self.client.post(self.main_url, payload, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_create_place(self): + payload = self.place_payload + payload.update({'owner': reverse('user-detail', kwargs={'pk': 1})}) + response = self.client.post(self.main_url, payload, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_create_n_places(self): + self.create_n_places(self.DEFAULT_PLACES_NUMBER) + queryset = Place.objects.all() + self.assertEqual(len(queryset), self.DEFAULT_PLACES_NUMBER) + + def test_owner_updates_place(self): + self.create_n_places(self.DEFAULT_PLACES_NUMBER) + url = reverse('place-update-place-data', kwargs={'pk': 1}) + place = Place.objects.get(pk=1) + owner = place.owner + self.client.force_login(owner) + new_place_name = 'newname' + response = self.client.patch(path=url, data={'name': new_place_name}, format='json') + place = Place.objects.get(pk=1) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(place.name, new_place_name) + + def test_anonym_updates_place(self): + self.create_n_places(self.DEFAULT_PLACES_NUMBER) + new_place_name = 'newname' + url = reverse('place-update-place-data', kwargs={'pk': 1}) + response = self.client.patch(path=url, data={'name': new_place_name}) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def logged_user_updates_not_owned_place(self): + self.create_n_places(self.DEFAULT_PLACES_NUMBER) + url = reverse('place-update-place-data', kwargs={'pk': 1}) + owner = get_user_model().objects.get(pk=2) + self.client.force_login(owner) + new_place_name = 'newname' + response = self.client.patch(path=url, data={'name': new_place_name}, format='json') + place = Place.objects.get(pk=1) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(place.name, new_place_name) diff --git a/spaces/urls.py b/spaces/urls.py new file mode 100644 index 0000000..9584782 --- /dev/null +++ b/spaces/urls.py @@ -0,0 +1,13 @@ +from django.conf.urls import url, include +from rest_framework import routers + +from .viewsets import PlaceViewSet + +router = routers.SimpleRouter(trailing_slash=False) +router.register(r'', PlaceViewSet) + + +urlpatterns = [ + + url(r'', include(router.urls)), +] diff --git a/spaces/viewsets.py b/spaces/viewsets.py new file mode 100644 index 0000000..dc19840 --- /dev/null +++ b/spaces/viewsets.py @@ -0,0 +1,32 @@ + +from rest_framework import status +from rest_framework import viewsets +from rest_framework.decorators import detail_route +from rest_framework.response import Response + +from .models import Place +from .serializers import PlaceSerializer +from accounts.permissions import IsPlaceOwnerOrStaff + + +class PlaceViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows 'spaces' to be viewed or edited. + """ + queryset = Place.objects.all() + serializer_class = PlaceSerializer + + @detail_route(methods=['put','patch'], permission_classes=[IsPlaceOwnerOrStaff]) + def update_place_data(self, request, pk=None): + if not pk: + return Response(status=status.HTTP_400_BAD_REQUEST) + user = self.get_object() + place = Place.objects.get(pk=pk) + self.check_object_permissions(request, user) + serializer = PlaceSerializer(data=request.data, context={'request': request}, partial=True) + if serializer.is_valid(): + serializer.update(place, serializer.validated_data) + return Response(status=status.HTTP_200_OK) + else: + return Response(serializer.errors, + status=status.HTTP_400_BAD_REQUEST) diff --git a/xea_core/settings.py b/xea_core/settings.py index c5ee688..e9bede3 100644 --- a/xea_core/settings.py +++ b/xea_core/settings.py @@ -44,6 +44,7 @@ 'jwt_knox', 'api', 'accounts', + 'spaces', ] MIDDLEWARE_CLASSES = [ From c5fef9c016da2a5bc20037c481066926e1385ca7 Mon Sep 17 00:00:00 2001 From: Adriano Todaro Date: Thu, 8 Sep 2016 16:13:52 +0200 Subject: [PATCH 5/6] moved accounts.permissions to 'api' app --- accounts/viewsets.py | 6 +++--- {accounts => api}/permissions.py | 0 spaces/viewsets.py | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) rename {accounts => api}/permissions.py (100%) diff --git a/accounts/viewsets.py b/accounts/viewsets.py index fcf2833..fce5493 100644 --- a/accounts/viewsets.py +++ b/accounts/viewsets.py @@ -4,11 +4,11 @@ from rest_framework.decorators import detail_route from rest_framework.response import Response -from .serializers import UserSerializer, UserPasswordSerializer -from .permissions import IsAdminOrSelf +from api.permissions import IsAdminOrSelf +from . import utils from .models import UserProfile from .serializers import UserProfileSerializer -from . import utils +from .serializers import UserSerializer, UserPasswordSerializer class UserViewSet(viewsets.ModelViewSet): diff --git a/accounts/permissions.py b/api/permissions.py similarity index 100% rename from accounts/permissions.py rename to api/permissions.py diff --git a/spaces/viewsets.py b/spaces/viewsets.py index dc19840..2364c8f 100644 --- a/spaces/viewsets.py +++ b/spaces/viewsets.py @@ -1,12 +1,11 @@ - from rest_framework import status from rest_framework import viewsets from rest_framework.decorators import detail_route from rest_framework.response import Response +from api.permissions import IsPlaceOwnerOrStaff from .models import Place from .serializers import PlaceSerializer -from accounts.permissions import IsPlaceOwnerOrStaff class PlaceViewSet(viewsets.ModelViewSet): From 46cdc437c6ecbc71def0f438ebc8948105ddbf7a Mon Sep 17 00:00:00 2001 From: Adriano Todaro Date: Fri, 9 Sep 2016 15:12:26 +0200 Subject: [PATCH 6/6] Add spaces management The code introduces a new model called "Place" that represents a physical site that can host events. It features fields that can store general information (name, description and owner) plus an Address field, which is actually a separate model Common CRUD operations are supported. A specific @detail_route had been created for the update operation to deal with the nested kind of structure of the "Place" entity. --- api/urls.py | 2 +- places/__init__.py | 0 {spaces => places}/admin.py | 0 places/models.py | 19 +++++++++++++++++++ {spaces => places}/serializers.py | 23 ++++++++++++----------- {spaces => places}/tests.py | 29 +++++++++++++++++++---------- {spaces => places}/urls.py | 6 +----- {spaces => places}/viewsets.py | 4 ++-- spaces/models.py | 23 ----------------------- xea_core/settings.py | 2 +- 10 files changed, 55 insertions(+), 53 deletions(-) create mode 100644 places/__init__.py rename {spaces => places}/admin.py (100%) create mode 100644 places/models.py rename {spaces => places}/serializers.py (56%) rename {spaces => places}/tests.py (79%) rename {spaces => places}/urls.py (62%) rename {spaces => places}/viewsets.py (87%) delete mode 100644 spaces/models.py diff --git a/api/urls.py b/api/urls.py index 6e2f3c5..d1c43aa 100644 --- a/api/urls.py +++ b/api/urls.py @@ -24,5 +24,5 @@ urlpatterns = [ url(r'', include(router.urls)), url(r'accounts/', include('accounts.urls')), - url(r'spaces/', include('spaces.urls')), + url(r'places/', include('places.urls')), ] diff --git a/places/__init__.py b/places/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/spaces/admin.py b/places/admin.py similarity index 100% rename from spaces/admin.py rename to places/admin.py diff --git a/places/models.py b/places/models.py new file mode 100644 index 0000000..7d28434 --- /dev/null +++ b/places/models.py @@ -0,0 +1,19 @@ +from django.db import models +from django.contrib.auth.models import User + + +class Address(models.Model): + line1 = models.CharField(max_length=70) + line2 = models.CharField(max_length=70) + + +class Place(models.Model): + class Meta: + app_label = 'places' + + name = models.CharField(max_length=140) + owner = models.ForeignKey(User, on_delete=models.CASCADE) + description = models.TextField(max_length=500) + address = models.ForeignKey(Address) + latitude = models.DecimalField(max_digits=9, decimal_places=6) + longitude = models.DecimalField(max_digits=9, decimal_places=6) diff --git a/spaces/serializers.py b/places/serializers.py similarity index 56% rename from spaces/serializers.py rename to places/serializers.py index 978fabb..738a874 100644 --- a/spaces/serializers.py +++ b/places/serializers.py @@ -6,7 +6,7 @@ class AddressSerializer(serializers.ModelSerializer): class Meta: model = Address - fields = ('street_name', 'civic', 'floor', 'door', 'city', 'postal_code', 'region', 'nation') + fields = ('line1', 'line2') class PlaceSerializer(serializers.HyperlinkedModelSerializer): @@ -20,21 +20,22 @@ def update(self, instance, validated_data): instance.name = validated_data.get('name', instance.name) instance.owner = validated_data.get('owner', instance.owner) instance.description = validated_data.get('nationality', instance.description) - instance.address = validated_data.get('address', instance.address) + address_data = validated_data.get('address', instance.address) + address = Address.objects.create( + + line1=address_data.get('line1',''), + line2=address_data.get('line2','') + ) + instance.address = address instance.save() return instance def create(self, validated_data): address_data = validated_data.get('address') - address = Address.objects.create(street_name=address_data.get('street_name'), - civic=address_data.get('civic'), - floor=address_data.get('floor'), - door=address_data.get('door'), - city=address_data.get('city'), - postal_code=address_data.get('postal_code'), - region=address_data.get('region'), - nation=address_data.get('nation') - ) + address = Address.objects.create( + line1=address_data.get('line1'), + line2=address_data.get('line2') + ) address.save() place = Place.objects.create( name=validated_data.get('name'), diff --git a/spaces/tests.py b/places/tests.py similarity index 79% rename from spaces/tests.py rename to places/tests.py index 60d2490..6d75ff2 100644 --- a/spaces/tests.py +++ b/places/tests.py @@ -1,5 +1,5 @@ from django.contrib.auth import get_user_model -from django.core.urlresolvers import reverse +from rest_framework.reverse import reverse from rest_framework import status from rest_framework.test import APITestCase from accounts.tests import create_n_users @@ -22,16 +22,13 @@ class PlacesTest(APITestCase): postal_code = '15005' region = 'Galicia' nation = 'Spain' + + line1 = street + ' ' + civic + ' ' + floor + door + line2 = postal_code + ' ' + city + ', ' + nation main_url = reverse('place-list') - address_payload = {'street_name': street, - 'civic': civic, - 'floor': floor, - 'door': door, - 'city': city, - 'postal_code': postal_code, - 'region': region, - 'nation': nation + address_payload = {'line1': line1, + 'line2': line2, } place_payload = {'name': name, @@ -92,4 +89,16 @@ def logged_user_updates_not_owned_place(self): response = self.client.patch(path=url, data={'name': new_place_name}, format='json') place = Place.objects.get(pk=1) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(place.name, new_place_name) + + def test_update_place_address(self): + self.create_n_places(self.DEFAULT_PLACES_NUMBER) + url = reverse('place-update-place-data', kwargs={'pk': 1}) + place = Place.objects.get(pk=1) + owner = place.owner + self.client.force_login(owner) + line1 = 'newstreetname 5b' + address_payload = {'line1': line1} + response = self.client.patch(path=url, data={'address': address_payload}, format='json') + place = Place.objects.get(pk=1) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(place.address.line1, line1) diff --git a/spaces/urls.py b/places/urls.py similarity index 62% rename from spaces/urls.py rename to places/urls.py index 9584782..7e765b5 100644 --- a/spaces/urls.py +++ b/places/urls.py @@ -1,4 +1,3 @@ -from django.conf.urls import url, include from rest_framework import routers from .viewsets import PlaceViewSet @@ -7,7 +6,4 @@ router.register(r'', PlaceViewSet) -urlpatterns = [ - - url(r'', include(router.urls)), -] +urlpatterns = router.urls diff --git a/spaces/viewsets.py b/places/viewsets.py similarity index 87% rename from spaces/viewsets.py rename to places/viewsets.py index 2364c8f..86099f2 100644 --- a/spaces/viewsets.py +++ b/places/viewsets.py @@ -10,12 +10,12 @@ class PlaceViewSet(viewsets.ModelViewSet): """ - API endpoint that allows 'spaces' to be viewed or edited. + API endpoint that allows 'places' to be viewed or edited. """ queryset = Place.objects.all() serializer_class = PlaceSerializer - @detail_route(methods=['put','patch'], permission_classes=[IsPlaceOwnerOrStaff]) + @detail_route(methods=['put', 'patch'], permission_classes=[IsPlaceOwnerOrStaff]) def update_place_data(self, request, pk=None): if not pk: return Response(status=status.HTTP_400_BAD_REQUEST) diff --git a/spaces/models.py b/spaces/models.py deleted file mode 100644 index 39a6afb..0000000 --- a/spaces/models.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.db import models -from django.contrib.auth.models import User - - -class Address(models.Model): - street_name = models.CharField(max_length=140) - civic = models.CharField(max_length=20) - floor = models.IntegerField(blank=True) - door = models.CharField(max_length=2, blank=True) - city = models.CharField(max_length=140) - postal_code = models.IntegerField(blank=True) - region = models.CharField(max_length=140) - nation = models.CharField(max_length=140) - - -class Place(models.Model): - class Meta: - app_label = 'spaces' - - name = models.CharField(max_length=140) - owner = models.ForeignKey(User, on_delete=models.CASCADE) - description = models.TextField(max_length=500) - address = models.ForeignKey(Address) diff --git a/xea_core/settings.py b/xea_core/settings.py index e9bede3..08ca8af 100644 --- a/xea_core/settings.py +++ b/xea_core/settings.py @@ -44,7 +44,7 @@ 'jwt_knox', 'api', 'accounts', - 'spaces', + 'places', ] MIDDLEWARE_CLASSES = [