diff --git a/beers/migrations/0030_userfavoritebeer.py b/beers/migrations/0030_userfavoritebeer.py new file mode 100644 index 00000000..579a574b --- /dev/null +++ b/beers/migrations/0030_userfavoritebeer.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.1 on 2019-05-19 13:34 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('beers', '0029_merge_20190519_1259'), + ] + + operations = [ + migrations.CreateModel( + name='UserFavoriteBeer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('notifications_enabled', models.BooleanField(default=False)), + ('beer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favored_by_users', to='beers.Beer')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_beers', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('beer', 'user')}, + }, + ), + ] diff --git a/beers/models.py b/beers/models.py index 61cac586..9e70f0a8 100644 --- a/beers/models.py +++ b/beers/models.py @@ -1,5 +1,6 @@ import logging +from django.conf import settings from django.contrib.postgres.fields import JSONField, CITextField from django.db import models, transaction from django.db.utils import IntegrityError @@ -301,3 +302,19 @@ class UntappdMetadata(models.Model): beer = models.OneToOneField( Beer, models.CASCADE, related_name='untappd_metadata', ) + + +class UserFavoriteBeer(models.Model): + user = models.ForeignKey( + settings.AUTH_USER_MODEL, models.CASCADE, + related_name='favorite_beers', + ) + beer = models.ForeignKey( + Beer, models.CASCADE, related_name='favored_by_users', + ) + notifications_enabled = models.BooleanField(default=False) + + class Meta: + unique_together = ( + ('beer', 'user'), + ) diff --git a/beers/serializers.py b/beers/serializers.py index 0708fa1d..c7a1cb2a 100644 --- a/beers/serializers.py +++ b/beers/serializers.py @@ -127,3 +127,17 @@ class OtherPKSerializer(serializers.Serializer): # we'll take care of validating during the view id = serializers.IntegerField(min_value=0) + + +class UserFavoriteBeerSerializer(serializers.ModelSerializer): + + class Meta: + model = models.UserFavoriteBeer + fields = '__all__' + validators = [ + UniqueTogetherValidator( + fields=['user', 'beer'], + queryset=models.UserFavoriteBeer.objects.all(), + message='User is already subscribed to this beer', + ), + ] diff --git a/hsv_dot_beer/urls.py b/hsv_dot_beer/urls.py index c5b05ce1..742ba118 100755 --- a/hsv_dot_beer/urls.py +++ b/hsv_dot_beer/urls.py @@ -7,12 +7,11 @@ from rest_framework.authtoken import views from beers.views import StyleMergeView -from .users.views import UserViewSet, UserCreateViewSet +from .users.views import UserViewSet router = DefaultRouter() router.register(r'users', UserViewSet) -router.register(r'users', UserCreateViewSet) urlpatterns = [ path('admin/', admin.site.urls), diff --git a/hsv_dot_beer/users/permissions.py b/hsv_dot_beer/users/permissions.py index 0596a5e4..9e80ce4c 100644 --- a/hsv_dot_beer/users/permissions.py +++ b/hsv_dot_beer/users/permissions.py @@ -1,14 +1,33 @@ +from django.urls import reverse from rest_framework import permissions -class IsUserOrReadOnly(permissions.BasePermission): +class UserPermission(permissions.BasePermission): """ - Object-level permission to only allow owners of an object to edit it. + Permissions for the user model: + + 1. Admins can do everything + 2. Normal users can only read/write themselves """ + def has_permission(self, request, view): + if request.user.is_staff: + print('it staff') + return True + print('request method', request.method) + if request.method == 'POST' and 'subscribe' in request.path: + print('letting detail trhough') + return True + return request.method in permissions.SAFE_METHODS + ('PUT', 'PATCH') + def has_object_permission(self, request, view, obj): - if request.method in permissions.SAFE_METHODS: + if request.user == obj: + print('user matches') + return True + if request.user.is_staff: + print('user is staff') return True - return obj == request.user + print('fall through') + return request.method in permissions.SAFE_METHODS diff --git a/hsv_dot_beer/users/test/test_views.py b/hsv_dot_beer/users/test/test_views.py index 4f9796ed..398972a6 100644 --- a/hsv_dot_beer/users/test/test_views.py +++ b/hsv_dot_beer/users/test/test_views.py @@ -7,6 +7,9 @@ from rest_framework.test import APITestCase from rest_framework import status from faker import Faker + +from beers.test.factories import BeerFactory +from beers.models import UserFavoriteBeer from ..models import User from .factories import UserFactory @@ -43,6 +46,7 @@ def test_post_request_with_valid_data_succeeds(self): ok_(check_password(self.user_data.get('password'), user.password)) def test_post_request_unauthorized(self): + self.client.credentials(HTTP_AUTHORIZATION='') response = self.client.post( self.url, json.dumps(self.user_data), content_type='application/json', @@ -61,10 +65,15 @@ def setUp(self): self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.user.auth_token}') def test_get_request_returns_a_given_user(self): + response = self.client.get(self.url) eq_(response.status_code, status.HTTP_200_OK) def test_put_request_updates_a_user(self): + self.client.credentials( + HTTP_AUTHORIZATION=f'Token {self.user.auth_token}', + ) + new_first_name = fake.first_name() payload = {'first_name': new_first_name} response = self.client.put(self.url, payload) @@ -72,3 +81,59 @@ def test_put_request_updates_a_user(self): user = User.objects.get(pk=self.user.id) eq_(user.first_name, new_first_name) + + def test_subscribe_to_beer(self): + beer = BeerFactory() + url = reverse('user-subscribetobeer', kwargs={'pk': self.user.pk}) + print(url) + payload = { + 'beer': beer.id, + 'notifications_enabled': True, + } + self.client.credentials( + HTTP_AUTHORIZATION=f'Token {self.user.auth_token}', + ) + + response = self.client.post(url, payload) + eq_(response.status_code, status.HTTP_200_OK, response.data) + eq_(UserFavoriteBeer.objects.count(), 1) + + def test_update_subscription_to_beer(self): + beer = BeerFactory() + sub = UserFavoriteBeer.objects.create( + beer=beer, user=self.user, notifications_enabled=True, + ) + url = reverse('user-subscribetobeer', kwargs={'pk': self.user.pk}) + + payload = { + 'beer': beer.id, + 'notifications_enabled': False, + } + self.client.credentials( + HTTP_AUTHORIZATION=f'Token {self.user.auth_token}', + ) + + response = self.client.post(url, payload) + eq_(response.status_code, status.HTTP_200_OK, response.content) + eq_(UserFavoriteBeer.objects.count(), 1) + sub.refresh_from_db() + self.assertFalse(sub.notifications_enabled) + + def test_unsubscribe_from_beer(self): + beer = BeerFactory() + sub = UserFavoriteBeer.objects.create( + beer=beer, user=self.user, notifications_enabled=True, + ) + url = reverse('user-unsubscribefrombeer', kwargs={'pk': self.user.pk}) + + payload = { + 'beer': beer.id, + 'notifications_enabled': False, + } + self.client.credentials( + HTTP_AUTHORIZATION=f'Token {self.user.auth_token}', + ) + + response = self.client.post(url, payload) + eq_(response.status_code, status.HTTP_204_NO_CONTENT, response.content) + eq_(UserFavoriteBeer.objects.count(), 0) diff --git a/hsv_dot_beer/users/views.py b/hsv_dot_beer/users/views.py index 3deac918..43d5aa4b 100644 --- a/hsv_dot_beer/users/views.py +++ b/hsv_dot_beer/users/views.py @@ -1,26 +1,79 @@ -from rest_framework import viewsets, mixins +from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.generics import get_object_or_404 from rest_framework.permissions import IsAdminUser +from rest_framework.response import Response +from rest_framework.serializers import ValidationError + +from beers.models import UserFavoriteBeer +from beers.serializers import UserFavoriteBeerSerializer from .models import User -from .permissions import IsUserOrReadOnly +from .permissions import UserPermission from .serializers import CreateUserSerializer, UserSerializer -class UserViewSet(mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - viewsets.GenericViewSet): +class UserViewSet(viewsets.ModelViewSet): """ Updates and retrieves user accounts """ - queryset = User.objects.all() - serializer_class = UserSerializer - permission_classes = (IsUserOrReadOnly,) + @action(detail=True, methods=['POST']) + def subscribetobeer(self, request, pk): + print('wheeeeee') + user = get_object_or_404(self.get_queryset(), id=pk) + body = request.data.copy() + body['user'] = user.id + print(body) + serializer = UserFavoriteBeerSerializer( + data=body, context={'request': request}, + ) + try: + # validate it as if it's a new subscription + serializer.is_valid(raise_exception=True) + except ValidationError as exc: + print('womp', exc) + if 'beer' in request.data and 'notifications_enabled' in request.data: + # is the user trying to update the existing subscription? + try: + fav = UserFavoriteBeer.objects.get( + user=user, + beer=request.data['beer'] + ) + except UserFavoriteBeer.DoesNotExist: + # nope, doesn't exist; raise the error + raise exc + # we do have a favorite instance + serializer = UserFavoriteBeerSerializer( + instance=fav, data=body, context={'request', request}, + ) + if not serializer.is_valid(): + # nope, still not valid + raise exc + serializer.save() + return Response(serializer.data) + # serializer is missing required fields + raise exc + instance = serializer.save() + return Response(serializer.data) + + @action(detail=True, methods=['POST']) + def unsubscribefrombeer(self, request, pk): + user = get_object_or_404(self.get_queryset(), id=pk) + if 'beer' not in request.data: + raise ValidationError({'beer': ['This field is required.']}) + instance = get_object_or_404( + UserFavoriteBeer.objects.all(), + user=user, + beer=request.data['beer'], + ) + instance.delete() + return Response('', status=204) + + def get_serializer_class(self): + if self.request.method == 'POST': + return CreateUserSerializer + return super().get_serializer_class() -class UserCreateViewSet(mixins.CreateModelMixin, - viewsets.GenericViewSet): - """ - Creates user accounts - """ queryset = User.objects.all() - serializer_class = CreateUserSerializer - permission_classes = (IsAdminUser,) + serializer_class = UserSerializer + permission_classes = (UserPermission, )