Commit a3637e4b25f232de516268173288ac4e9b82e0c8

Authored by Andrew Buss
1 parent f6068531bb
Exists in master

Added some tests to start

Showing 4 changed files with 44 additions and 4 deletions Inline Diff

flashcards/api.py View file @ a3637e4
from django.core.mail import send_mail 1 1 from django.core.mail import send_mail
from django.contrib.auth import authenticate, login 2 2 from django.contrib.auth import authenticate, login
from django.contrib.auth.tokens import default_token_generator 3 3 from django.contrib.auth.tokens import default_token_generator
4 from rest_framework.status import HTTP_201_CREATED, HTTP_204_NO_CONTENT
from rest_framework.views import APIView 4 5 from rest_framework.views import APIView
from rest_framework.response import Response 5 6 from rest_framework.response import Response
from rest_framework import status 6 7 from rest_framework import status
from rest_framework.exceptions import ValidationError, NotFound 7 8 from rest_framework.exceptions import ValidationError, NotFound
from flashcards.serializers import * 8 9 from flashcards.serializers import *
9 10
10 11
class UserDetail(APIView): 11 12 class UserDetail(APIView):
def patch(self, request, format=None): 12 13 def patch(self, request, format=None):
""" 13 14 """
This method checks either the email or the password passed in 14 15 This method checks either the email or the password passed in
is valid. If confirmation key is correct, it validates the 15 16 is valid. If confirmation key is correct, it validates the
user. It updates the password if the new password 16 17 user. It updates the password if the new password
is valid. 17 18 is valid.
18 19
""" 19 20 """
currentuser = request.user 20 21 currentuser = request.user
21 22
if 'confirmation_key' in request.data: 22 23 if 'confirmation_key' in request.data:
if not currentuser.confirm_email( request.data['confirmation_key'] ): 23 24 if not currentuser.confirm_email( request.data['confirmation_key'] ):
raise ValidationError('confirmation_key is invalid') 24 25 raise ValidationError('confirmation_key is invalid')
25 26
if 'new_password' in request.data: 26 27 if 'new_password' in request.data:
if not currentuser.check_password(request.data['old_password']): 27 28 if not currentuser.check_password(request.data['old_password']):
raise ValidationError('Invalid old password') 28 29 raise ValidationError('Invalid old password')
if not request.data['new_password']: 29 30 if not request.data['new_password']:
raise ValidationError('Password cannot be blank') 30 31 raise ValidationError('Password cannot be blank')
currentuser.set_password(request.data['new_password']) 31 32 currentuser.set_password(request.data['new_password'])
currentuser.save() 32 33 currentuser.save()
33 34
return Response(status=status.HTTP_204_NO_CONTENT) 34 35 return Response(status=status.HTTP_204_NO_CONTENT)
35 36
def get(self, request, format=None): 36 37 def get(self, request, format=None):
serializer = UserSerializer(request.user) 37 38 serializer = UserSerializer(request.user)
return Response(serializer.data) 38 39 return Response(serializer.data)
39 40
def post(self, request, format=None): 40 41 def post(self, request, format=None):
if 'email' not in request.data: 41 42 if 'email' not in request.data:
raise ValidationError('Email is required') 42 43 raise ValidationError('Email is required')
if 'password' not in request.data: 43 44 if 'password' not in request.data:
raise ValidationError('Password is required') 44 45 raise ValidationError('Password is required')
45 46
email = request.data['email'] 46 47 email = request.data['email']
user = User.objects.create_user(email, email=email, password=request.data['password']) 47 48 user = User.objects.create_user(email, email=email, password=request.data['password'])
48 49
body = ''' 49 50 body = '''
Visit the following link to confirm your email address: 50 51 Visit the following link to confirm your email address:
http://flashy.cards/app/verify_email/%s 51 52 http://flashy.cards/app/verify_email/%s
52 53
If you did not register for Flashy, no action is required. 53 54 If you did not register for Flashy, no action is required.
''' 54 55 '''
55 56
user = authenticate(email=email, password=request.data['password']) 56 57 user = authenticate(email=email, password=request.data['password'])
login(request, user) 57 58 login(request, user)
return Response(UserSerializer(user).data) 58 59 return Response(UserSerializer(user).data, status=HTTP_201_CREATED)
59 60
def delete(self, request, format=None): 60 61 def delete(self, request, format=None):
request.user.delete() 61 62 request.user.delete()
return Response(status=status.HTTP_204_NO_CONTENT) 62 63 return Response(status=HTTP_204_NO_CONTENT)
63 64
64 65
class UserLogin(APIView): 65 66 class UserLogin(APIView):
""" 66 67 """
Authenticates user and returns user data if valid. Handles invalid 67 68 Authenticates user and returns user data if valid. Handles invalid
users. 68 69 users.
""" 69 70 """
70 71
def post(self, request, format=None): 71 72 def post(self, request, format=None):
""" 72 73 """
Returns user data if valid. 73 74 Returns user data if valid.
""" 74 75 """
if 'email' not in request.data: 75 76 if 'email' not in request.data:
raise ValidationError('Email is required') 76 77 raise ValidationError('Email is required')
if 'password' not in request.data: 77 78 if 'password' not in request.data:
raise ValidationError('Password is required') 78 79 raise ValidationError('Password is required')
79 80
email = request.data['email'] 80 81 email = request.data['email']
password = request.data['password'] 81 82 password = request.data['password']
user = authenticate(username=email, password=password) 82 83 user = authenticate(email=email, password=password)
83 84
if user is None: 84 85 if user is None:
raise ValidationError('Invalid email or password') 85 86 raise ValidationError('Invalid email or password')
if not user.is_active: 86 87 if not user.is_active:
raise ValidationError('Account is disabled') 87 88 raise ValidationError('Account is disabled')
login(request, user) 88 89 login(request, user)
return Response(UserSerializer(user).data) 89 90 return Response(UserSerializer(user).data)
90 91
91 92
class PasswordReset(APIView): 92 93 class PasswordReset(APIView):
""" 93 94 """
Allows user to reset their password. 94 95 Allows user to reset their password.
""" 95 96 """
96 97
def post(self, request, format=None): 97 98 def post(self, request, format=None):
""" 98 99 """
Send a password reset token/link to the provided email. 99 100 Send a password reset token/link to the provided email.
""" 100 101 """
if 'email' not in request.data: 101 102 if 'email' not in request.data:
raise ValidationError('Email is required') 102 103 raise ValidationError('Email is required')
103 104
email = request.data['email'] 104 105 email = request.data['email']
105 106
# Find the user since they are not logged in. 106 107 # Find the user since they are not logged in.
try: 107 108 try:
user = User.objects.get(email=email) 108 109 user = User.objects.get(email=email)
except User.DoesNotExist: 109 110 except User.DoesNotExist:
raise NotFound('Email does not exist') 110 111 raise NotFound('Email does not exist')
111 112
token = default_token_generator.make_token(user) 112 113 token = default_token_generator.make_token(user)
113 114
flashcards/models.py View file @ a3637e4
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin, AbstractUser 1 1 from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin, AbstractUser
from django.contrib.auth.tests.custom_user import CustomUser 2
from django.db.models import * 3 2 from django.db.models import *
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 4 3 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
5 4
# Hack to fix AbstractUser before subclassing it 6 5 # Hack to fix AbstractUser before subclassing it
AbstractUser._meta.get_field('email')._unique = True 7 6 AbstractUser._meta.get_field('email')._unique = True
8 7
class User(AbstractUser, SimpleEmailConfirmationUserMixin, ): 9 8 class User(AbstractUser, SimpleEmailConfirmationUserMixin, ):
USERNAME_FIELD = 'email' 10 9 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 11 10 REQUIRED_FIELDS = []
sections = ManyToManyField('Section') 12 11 sections = ManyToManyField('Section')
13 12
14 13
class UserFlashcard(Model): 15 14 class UserFlashcard(Model):
""" 16 15 """
Represents the relationship between a user and a flashcard by: 17 16 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 18 17 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 19 18 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 20 19 3. A user has a flashcard hidden from them
""" 21 20 """
user = ForeignKey('User') 22 21 user = ForeignKey('User')
mask = ForeignKey('FlashcardMask', help_text="A mask which overrides the card's mask") 23 22 mask = ForeignKey('FlashcardMask', help_text="A mask which overrides the card's mask")
pulled = DateTimeField(blank=True, null=True, help_text="When the user pulled the card") 24 23 pulled = DateTimeField(blank=True, null=True, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 25 24 flashcard = ForeignKey('Flashcard')
unpulled = DateTimeField(blank=True, null=True, help_text="When the user unpulled this card") 26 25 unpulled = DateTimeField(blank=True, null=True, help_text="When the user unpulled this card")
27 26
class Meta: 28 27 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 29 28 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 30 29 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 31 30 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 32 31 # By default, order by most recently pulled
ordering = ['-pulled'] 33 32 ordering = ['-pulled']
34 33
def is_hidden(self): 35 34 def is_hidden(self):
""" 36 35 """
A card is hidden only if a user has not ever added it to their deck. 37 36 A card is hidden only if a user has not ever added it to their deck.
:return: Whether the flashcard is hidden from the user 38 37 :return: Whether the flashcard is hidden from the user
""" 39 38 """
return not self.pulled 40 39 return not self.pulled
41 40
def is_in_deck(self): 42 41 def is_in_deck(self):
""" 43 42 """
:return:Whether the flashcard is in the user's deck 44 43 :return:Whether the flashcard is in the user's deck
""" 45 44 """
return self.pulled and not self.unpulled 46 45 return self.pulled and not self.unpulled
47 46
48 47
class FlashcardMask(Model): 49 48 class FlashcardMask(Model):
""" 50 49 """
A serialized list of character ranges that can be blanked out during review. 51 50 A serialized list of character ranges that can be blanked out during review.
This is encoded as '13-145,150-195' 52 51 This is encoded as '13-145,150-195'
""" 53 52 """
ranges = CharField(max_length=255) 54 53 ranges = CharField(max_length=255)
55 54
56 55
class Flashcard(Model): 57 56 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 58 57 text = CharField(max_length=255, help_text='The text on the card')
section = ForeignKey('Section', help_text='The section with which the card is associated') 59 58 section = ForeignKey('Section', help_text='The section with which the card is associated')
pushed = DateTimeField(auto_now_add=True, help_text="When the card was first pushed") 60 59 pushed = DateTimeField(auto_now_add=True, help_text="When the card was first pushed")
material_date = DateTimeField(help_text="The date with which the card is associated") 61 60 material_date = DateTimeField(help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, 62 61 previous = ForeignKey('Flashcard', null=True, blank=True,
help_text="The previous version of this card, if one exists") 63 62 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 64 63 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 65 64 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card") 66 65 hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card")
mask = ForeignKey(FlashcardMask, blank=True, null=True, help_text="The default mask for this card") 67 66 mask = ForeignKey(FlashcardMask, blank=True, null=True, help_text="The default mask for this card")
68 67
class Meta: 69 68 class Meta:
# By default, order by most recently pushed 70 69 # By default, order by most recently pushed
ordering = ['-pushed'] 71 70 ordering = ['-pushed']
72 71
def is_hidden_from(self, user): 73 72 def is_hidden_from(self, user):
""" 74 73 """
A card can be hidden globally, but if a user has the card in their deck, 75 74 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 76 75 this visibility overrides a global hide.
:param user: 77 76 :param user:
:return: Whether the card is hidden from the user. 78 77 :return: Whether the card is hidden from the user.
""" 79 78 """
result = user.userflashcard_set.filter(flashcard=self) 80 79 result = user.userflashcard_set.filter(flashcard=self)
if not result.exists(): return self.is_hidden 81 80 if not result.exists(): return self.is_hidden
return result[0].is_hidden() 82 81 return result[0].is_hidden()
83 82
84 83
@classmethod 85 84 @classmethod
def cards_visible_to(cls, user): 86 85 def cards_visible_to(cls, user):
""" 87 86 """
:param user: 88 87 :param user:
:return: A queryset with all cards that should be visible to a user. 89 88 :return: A queryset with all cards that should be visible to a user.
""" 90 89 """
return cls.objects.filter(hidden=False).exclude(userflashcard=user, userflashcard__pulled=None) 91 90 return cls.objects.filter(hidden=False).exclude(userflashcard=user, userflashcard__pulled=None)
92 91
93 92
class UserFlashcardReview(Model): 94 93 class UserFlashcardReview(Model):
""" 95 94 """
An event of a user reviewing a flashcard. 96 95 An event of a user reviewing a flashcard.
""" 97 96 """
user_flashcard = ForeignKey(UserFlashcard) 98 97 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField() 99 98 when = DateTimeField()
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 100 99 blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked")
response = CharField(max_length=255, blank=True, null=True, help_text="The user's response") 101 100 response = CharField(max_length=255, blank=True, null=True, help_text="The user's response")
correct = NullBooleanField(help_text="The user's self-evaluation of their response") 102 101 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
103 102
def status(self): 104 103 def status(self):
""" 105 104 """
There are three stages of a review object: 106 105 There are three stages of a review object:
1. the user has been shown the card 107 106 1. the user has been shown the card
2. the user has answered the card 108 107 2. the user has answered the card
3. the user has self-evaluated their response's correctness 109 108 3. the user has self-evaluated their response's correctness
110 109
:return: string (evaluated, answered, viewed) 111 110 :return: string (evaluated, answered, viewed)
""" 112 111 """
if self.correct is not None: return "evaluated" 113 112 if self.correct is not None: return "evaluated"
if self.response: return "answered" 114 113 if self.response: return "answered"
return "viewed" 115 114 return "viewed"
116 115
117 116
class Section(Model): 118 117 class Section(Model):
""" 119 118 """
flashcards/tests/test_api.py View file @ a3637e4
File was created 1 from flashcards.models import User
2 from rest_framework.status import HTTP_201_CREATED, HTTP_200_OK
3 from rest_framework.test import APITestCase
4
5
6 class LoginTests(APITestCase):
7 def setUp(self):
8 User.objects.create(email="test@flashy.cards", password='1234')
9
10 def test_login(self):
11 url = '/api/login'
12 data = {'email': 'test@flashy.cards', 'password': '1234'}
13 response = self.client.post(url, data, format='json')
14 self.assertEqual(response.status_code, HTTP_200_OK)
15
16
17 class RegistrationTest(APITestCase):
18 def test_create_account(self):
19 url = '/api/users/me'
20 data = {'email': 'none@none.com', 'password': '1234'}
21 response = self.client.post(url, data, format='json')
22 self.assertEqual(response.status_code, HTTP_201_CREATED)
flashcards/tests/test_models.py View file @ a3637e4
File was created 1 from django.test import TestCase
2 from flashcards.models import User
3
4
5 class UserTests(TestCase):
6 def setUp(self):
7 u = User.objects.create(email="none@none.com",password="1234")
8