Skip to content

Commit

Permalink
Add user favorite beer model
Browse files Browse the repository at this point in the history
Fixes #190
  • Loading branch information
drewbrew committed Jul 23, 2019
1 parent 3a978b8 commit bdfa4c1
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 21 deletions.
28 changes: 28 additions & 0 deletions beers/migrations/0030_userfavoritebeer.py
Original file line number Diff line number Diff line change
@@ -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')},
},
),
]
17 changes: 17 additions & 0 deletions beers/models.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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'),
)
14 changes: 14 additions & 0 deletions beers/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
),
]
3 changes: 1 addition & 2 deletions hsv_dot_beer/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
27 changes: 23 additions & 4 deletions hsv_dot_beer/users/permissions.py
Original file line number Diff line number Diff line change
@@ -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
65 changes: 65 additions & 0 deletions hsv_dot_beer/users/test/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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',
Expand All @@ -61,14 +65,75 @@ 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)
eq_(response.status_code, status.HTTP_200_OK)

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)
83 changes: 68 additions & 15 deletions hsv_dot_beer/users/views.py
Original file line number Diff line number Diff line change
@@ -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, )

0 comments on commit bdfa4c1

Please sign in to comment.