Commit 1890b5a76b6f9c26b60fd2c674ad453f19a98b5d

Authored by Rohan Rangray
Exists in master

Resolved conflict

Showing 9 changed files Side-by-side Diff

flashcards/admin.py View file @ 1890b5a
1 1 from django.contrib import admin
2 2 from django.contrib.auth.admin import UserAdmin
3 3 from flashcards.models import Flashcard, UserFlashcard, Section, \
4   - LecturePeriod, User, UserFlashcardQuiz
  4 + LecturePeriod, User, UserFlashcardQuiz, WhitelistedAddress, FlashcardHide
5 5  
6 6 admin.site.register([
7 7 Flashcard,
8 8 UserFlashcard,
9 9 UserFlashcardQuiz,
10 10 Section,
11   - LecturePeriod
  11 + LecturePeriod,
  12 + FlashcardHide,
  13 + WhitelistedAddress
12 14 ])
13 15 admin.site.register(User, UserAdmin)
flashcards/models.py View file @ 1890b5a
1 1 from math import log1p
2 2 from math import exp
  3 +from math import e
3 4  
4 5 from django.contrib.auth.models import AbstractUser, UserManager
5 6 from django.contrib.auth.tokens import default_token_generator
6 7  
7 8  
8 9  
... ... @@ -10,15 +11,17 @@
10 11 from django.core.validators import MinLengthValidator
11 12 from django.db import IntegrityError
12 13 from django.db.models import *
13   -from django.utils.log import getLogger
14 14 from django.utils.timezone import now, make_aware
15   -from flashy.settings import QUARTER_START, QUARTER_END
  15 +from flashy.settings import QUARTER_START
16 16 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
17 17 from fields import MaskField
18 18 from cached_property import cached_property
19 19 from flashy.settings import IN_PRODUCTION
20   -from math import e
21 20  
  21 +
  22 +
  23 +
  24 +
22 25 # Hack to fix AbstractUser before subclassing it
23 26  
24 27 AbstractUser._meta.get_field('email')._unique = True
25 28  
26 29  
... ... @@ -42,21 +45,12 @@
42 45 date_joined=now(), **extra_fields)
43 46 user.set_password(password)
44 47 user.save(using=self._db)
  48 + user.send_confirmation_email()
45 49 return user
46 50  
47 51 def create_user(self, email, password=None, **extra_fields):
48 52 user = self._create_user(email, password, False, False, **extra_fields)
49   - body = '''
50   - Visit the following link to confirm your email address:
51   - https://flashy.cards/app/verifyemail/%s
52 53  
53   - If you did not register for Flashy, no action is required.
54   - '''
55   -
56   - assert send_mail("Flashy email verification",
57   - body % user.confirmation_key,
58   - "noreply@flashy.cards",
59   - [user.email])
60 54 return user
61 55  
62 56 def create_superuser(self, email, password, **extra_fields):
... ... @@ -67,7 +61,6 @@
67 61 pass
68 62  
69 63  
70   -
71 64 class FlashcardNotInDeckException(Exception):
72 65 pass
73 66  
... ... @@ -83,6 +76,15 @@
83 76 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
84 77 confirmed_email = BooleanField(default=False)
85 78  
  79 + def send_confirmation_email(self):
  80 + body = '''
  81 + Visit the following link to confirm your email address:
  82 + https://flashy.cards/app/verifyemail/%s
  83 +
  84 + If you did not register for Flashy, no action is required.
  85 + '''
  86 + send_mail("Flashy email verification", body % self.confirmation_key, "noreply@flashy.cards", [self.email])
  87 +
86 88 def is_in_section(self, section):
87 89 return self.sections.filter(pk=section.pk).exists()
88 90  
... ... @@ -107,6 +109,7 @@
107 109  
108 110 try:
109 111 import flashcards.notifications
  112 +
110 113 user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard)
111 114 user_card.delete()
112 115 flashcards.notifications.notify_score_change(flashcard)
... ... @@ -140,7 +143,7 @@
140 143 self.save()
141 144  
142 145 def by_retention(self, sections, material_date_begin, material_date_end):
143   - section_pks = map(lambda i: i['pk'], sections.values('pk'))
  146 + section_pks = sections.values_list('pk')
144 147 user_flashcard_filter = UserFlashcard.objects.filter(
145 148 user=self, flashcard__section__pk__in=section_pks,
146 149 flashcard__material_date__gte=material_date_begin,
... ... @@ -158,7 +161,7 @@
158 161 output_field=FloatField()
159 162 ),
160 163 retention_score=Case(
161   - default=Value(e, output_field=FloatField()) ** (F('days_since')*(-0.1/(F('study_count')+1))),
  164 + default=Value(e, output_field=FloatField()) ** (F('days_since') * (-0.1 / (F('study_count') + 1))),
162 165 output_field=FloatField()
163 166 )
164 167 ).order_by('retention_score')
... ... @@ -413,6 +416,9 @@
413 416 :return: whether the user is on the waitlist for this section
414 417 """
415 418 return self.whitelist.filter(email=user.email).exists()
  419 +
  420 + def is_user_enrolled(self, user):
  421 + return self.user_set.filter(pk=user.pk).exists()
416 422  
417 423 def enroll(self, user):
418 424 if user.sections.filter(pk=self.pk).exists():
flashcards/notifications.py View file @ 1890b5a
... ... @@ -26,4 +26,11 @@
26 26 {'event_type': 'pull_card', 'flashcard': serializers.FlashcardSerializer(flashcard).data})
27 27 message = RedisMessage(ws_message)
28 28 redis_publisher.publish_message(message)
  29 +
  30 +def notify_deck_new_card(flashcard):
  31 + redis_publisher = RedisPublisher(facility='deck/%d' % flashcard.section_id, broadcast=True)
  32 + ws_message = JSONRenderer().render(
  33 + {'event_type': 'new_card', 'flashcard': serializers.FlashcardSerializer(flashcard).data})
  34 + message = RedisMessage(ws_message)
  35 + redis_publisher.publish_message(message)
flashcards/serializers.py View file @ 1890b5a
1   -from json import dumps, loads
  1 +from json import loads
  2 +from collections import Iterable
2 3  
3 4 from django.utils.datetime_safe import datetime
4 5 from django.utils.timezone import now
5   -from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcard, UserFlashcardQuiz
  6 +from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcardQuiz
6 7 from flashcards.validators import FlashcardMask, OverlapIntervalException
7 8 from rest_framework import serializers
8 9 from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField, empty, \
... ... @@ -10,7 +11,6 @@
10 11 from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField
11 12 from rest_framework.validators import UniqueValidator
12 13 from flashy.settings import QUARTER_END, QUARTER_START
13   -from collections import Iterable
14 14  
15 15  
16 16 class EmailSerializer(Serializer):
17 17  
... ... @@ -46,11 +46,12 @@
46 46 except User.DoesNotExist:
47 47 raise serializers.ValidationError('Could not verify reset token')
48 48  
  49 +class EmailVerificationSerializer(Serializer):
  50 + confirmation_key = CharField()
49 51  
50 52 class UserUpdateSerializer(Serializer):
51 53 old_password = CharField(required=False)
52   - new_password = CharField(required=False, allow_blank=False)
53   - confirmation_key = CharField(required=False)
  54 + new_password = CharField(required=False, allow_blank=False)\
54 55 # reset_token = CharField(required=False)
55 56  
56 57 def validate(self, data):
... ... @@ -58,12 +59,6 @@
58 59 raise serializers.ValidationError('old_password is required to set a new_password')
59 60 return data
60 61  
61   -
62   -class Password(Serializer):
63   - email = EmailField(required=True)
64   - password = CharField(required=True)
65   -
66   -
67 62 class LecturePeriodSerializer(ModelSerializer):
68 63 class Meta:
69 64 model = LecturePeriod
70 65  
... ... @@ -74,9 +69,20 @@
74 69 lecture_times = CharField()
75 70 short_name = CharField()
76 71 long_name = CharField()
  72 + can_enroll = SerializerMethodField()
  73 + is_enrolled = SerializerMethodField()
77 74  
78 75 class Meta:
79 76 model = Section
  77 +
  78 + def get_can_enroll(self, obj):
  79 + if 'user' not in self.context: return False
  80 + if not obj.is_whitelisted: return True
  81 + return obj.is_user_on_whitelist(self.context['user'])
  82 +
  83 + def get_is_enrolled(self, obj):
  84 + if 'user' not in self.context: return False
  85 + return obj.is_user_enrolled(self.context['user'])
80 86  
81 87  
82 88 class DeepSectionSerializer(SectionSerializer):
flashcards/tests/test_api.py View file @ 1890b5a
... ... @@ -120,13 +120,13 @@
120 120  
121 121 # try activating with an invalid key
122 122  
123   - url = '/api/me/'
124   - response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'})
  123 + url = '/api/verify_email/'
  124 + response = self.client.post(url, {'confirmation_key': 'NOT A KEY'})
125 125 self.assertContains(response, 'confirmation_key is invalid', status_code=400)
126 126  
127 127 # try activating with the valid key
128   - response = self.client.patch(url, {'confirmation_key': user.confirmation_key})
129   - self.assertTrue(response.data['is_confirmed'])
  128 + response = self.client.post(url, {'confirmation_key': user.confirmation_key})
  129 + self.assertContains(response, '', status_code=204)
130 130  
131 131  
132 132 class ProfileViewTest(APITestCase):
flashcards/views.py View file @ 1890b5a
... ... @@ -4,13 +4,13 @@
4 4 from django.utils.log import getLogger
5 5 from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer, \
6 6 IsAuthenticatedAndConfirmed
7   -from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard, UserFlashcardQuiz, \
8   - FlashcardAlreadyPulledException, FlashcardNotInDeckException, Now, interval_days
  7 +from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcardQuiz, \
  8 + FlashcardAlreadyPulledException, FlashcardNotInDeckException
9 9 from flashcards.notifications import notify_new_card
10 10 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
11 11 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
12 12 FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \
13   - QuizAnswerRequestSerializer, DeepSectionSerializer, FeedRequestSerializer
  13 + QuizAnswerRequestSerializer, DeepSectionSerializer, EmailVerificationSerializer, FeedRequestSerializer
14 14 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
15 15 from rest_framework.generics import ListAPIView, GenericAPIView
16 16 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin
... ... @@ -23,6 +23,7 @@
23 23 from rest_framework.response import Response
24 24 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
25 25 from simple_email_confirmation import EmailAddress
  26 +from simple_email_confirmation.models import EmailAddressManager
26 27  
27 28  
28 29 def log_event(request, event=''):
... ... @@ -102,7 +103,7 @@
102 103 query = request.GET.get('q', None)
103 104 if not query: return Response('[]')
104 105 qs = Section.search(query.split(' '))[:20]
105   - data = SectionSerializer(qs, many=True).data
  106 + data = SectionSerializer(qs, many=True, context={'user': request.user}).data
106 107 log_event(request, query)
107 108 return Response(data)
108 109  
... ... @@ -149,7 +150,7 @@
149 150  
150 151 def patch(self, request, format=None):
151 152 """
152   - Updates the user's password, or verifies their email address
  153 + Updates the user's password
153 154 ---
154 155 request_serializer: UserUpdateSerializer
155 156 response_serializer: UserSerializer
... ... @@ -165,13 +166,6 @@
165 166 request.user.save()
166 167 log_event(request, 'change password')
167 168  
168   - if 'confirmation_key' in data:
169   - try:
170   - request.user.confirm_email(data['confirmation_key'])
171   - log_event(request, 'confirm email')
172   - except EmailAddress.DoesNotExist:
173   - raise ValidationError('confirmation_key is invalid')
174   -
175 169 return Response(UserSerializer(request.user).data)
176 170  
177 171 def get(self, request, format=None):
... ... @@ -195,6 +189,32 @@
195 189  
196 190  
197 191 @api_view(['POST'])
  192 +@permission_classes([IsAuthenticated])
  193 +def resend_confirmation_email(request):
  194 + "Resends a confirmation email to a user"
  195 + request.user.send_confirmation_email()
  196 + return Response(status=HTTP_204_NO_CONTENT)
  197 +
  198 +
  199 +@api_view(['POST'])
  200 +@permission_classes([IsAuthenticated])
  201 +def verify_email(request):
  202 + """
  203 + Accepts a user's email confirmation_key to verify their email address
  204 + ---
  205 + request_serializer: EmailVerificationSerializer
  206 + """
  207 + try:
  208 + data = EmailVerificationSerializer(data=request.data)
  209 + data.is_valid(raise_exception=True)
  210 + email = EmailAddress.objects.confirm(data.validated_data['confirmation_key'])
  211 + log_event(request, 'confirm email' + str(email))
  212 + return Response(status=HTTP_204_NO_CONTENT)
  213 + except EmailAddress.DoesNotExist:
  214 + raise ValidationError('confirmation_key is invalid')
  215 +
  216 +
  217 +@api_view(['POST'])
198 218 def register(request, format=None):
199 219 """
200 220 Register a new user
201 221  
... ... @@ -395,8 +415,8 @@
395 415 serializer = QuizRequestSerializer(data=request.data)
396 416 serializer.is_valid(raise_exception=True)
397 417 data = serializer.validated_data
398   -
399 418 user_flashcard = request.user.by_retention(**data).first()
  419 +
400 420 mask = user_flashcard.get_mask().get_random_blank()
401 421 blanked_word = ""
402 422 if mask:
flashy/settings.py View file @ 1890b5a
... ... @@ -171,4 +171,7 @@
171 171 SWAGGER_SETTINGS = {
172 172 'doc_expansion': 'list'
173 173 }
  174 +
  175 +WS4REDIS_EXPIRE = 0
  176 +SESSION_ENGINE = 'redis_sessions.session'
flashy/urls.py View file @ 1890b5a
1 1 from django.conf.urls import include, url
2 2 from django.contrib import admin
3 3 from flashcards.views import SectionViewSet, UserDetail, FlashcardViewSet, UserSectionListView, request_password_reset, \
4   - reset_password, logout, login, register, UserFlashcardQuizViewSet
  4 + reset_password, logout, login, register, UserFlashcardQuizViewSet, resend_confirmation_email, verify_email
5 5 from flashy.frontend_serve import serve_with_default
6 6 from flashy.settings import DEBUG, IN_PRODUCTION
7 7 from rest_framework.routers import DefaultRouter
... ... @@ -19,6 +19,8 @@
19 19 url(r'^api/login/$', login),
20 20 url(r'^api/logout/$', logout),
21 21 url(r'^api/me/sections/', UserSectionListView.as_view()),
  22 + url(r'^api/resend_confirmation_email/', resend_confirmation_email),
  23 + url(r'^api/verify_email/', verify_email),
22 24 url(r'^api/request_password_reset/', request_password_reset),
23 25 url(r'^api/reset_password/', reset_password),
24 26 url(r'^api/', include(router.urls)),
requirements.txt View file @ 1890b5a
... ... @@ -13,5 +13,5 @@
13 13 django-rest-swagger
14 14 pytz
15 15 django-extensions
16   -django-filter
  16 +django-redis-sessions