Commit f6900af2ccb7324eea6c1c2c211517fa4dfa86ed
1 parent
2144619015
Exists in
master
Added things to collect registration tokens and notify users
Showing 9 changed files with 98 additions and 6 deletions Side-by-side Diff
flashcards/management/commands/notifyusers.py
View file @
f6900af
1 | +from django.core.management import BaseCommand | |
2 | +from flashcards.models import UserFlashcard, Now | |
3 | +from django.utils.timezone import now | |
4 | +from datetime import timedelta | |
5 | + | |
6 | + | |
7 | +class Command(BaseCommand): | |
8 | + help = 'Notify the users if they have cards to be reviewed' | |
9 | + | |
10 | + def handle(self, *args, **options): | |
11 | + notify_list = UserFlashcard.objects.filter( | |
12 | + next_review__lte=Now() | |
13 | + ).exclude( | |
14 | + user__registration_token=None, | |
15 | + user__last_notified__range=(now()-timedelta(days=1), now()) | |
16 | + ).values_list('user').distinct().all() | |
17 | + | |
18 | + for user in notify_list: | |
19 | + user.notify() |
flashcards/migrations/0015_user_registration_token.py
View file @
f6900af
1 | +# -*- coding: utf-8 -*- | |
2 | +from __future__ import unicode_literals | |
3 | + | |
4 | +from django.db import models, migrations | |
5 | + | |
6 | + | |
7 | +class Migration(migrations.Migration): | |
8 | + | |
9 | + dependencies = [ | |
10 | + ('flashcards', '0014_auto_20150601_1315'), | |
11 | + ] | |
12 | + | |
13 | + operations = [ | |
14 | + migrations.AddField( | |
15 | + model_name='user', | |
16 | + name='registration_token', | |
17 | + field=models.CharField(default=None, max_length=4096, null=True), | |
18 | + ), | |
19 | + ] |
flashcards/migrations/0016_user_last_notified.py
View file @
f6900af
1 | +# -*- coding: utf-8 -*- | |
2 | +from __future__ import unicode_literals | |
3 | + | |
4 | +from django.db import models, migrations | |
5 | + | |
6 | + | |
7 | +class Migration(migrations.Migration): | |
8 | + | |
9 | + dependencies = [ | |
10 | + ('flashcards', '0015_user_registration_token'), | |
11 | + ] | |
12 | + | |
13 | + operations = [ | |
14 | + migrations.AddField( | |
15 | + model_name='user', | |
16 | + name='last_notified', | |
17 | + field=models.DateTimeField(default=None, null=True), | |
18 | + ), | |
19 | + ] |
flashcards/models.py
View file @
f6900af
1 | 1 | from math import log1p |
2 | 2 | from math import exp |
3 | 3 | from datetime import datetime, timedelta |
4 | +from gcm import GCM | |
4 | 5 | |
5 | 6 | from django.contrib.auth.models import AbstractUser, UserManager |
6 | 7 | from django.contrib.auth.tokens import default_token_generator |
7 | 8 | |
... | ... | @@ -12,11 +13,10 @@ |
12 | 13 | from django.db import IntegrityError |
13 | 14 | from django.db.models import * |
14 | 15 | from django.utils.timezone import now, make_aware |
15 | -from flashy.settings import QUARTER_START, ABSOLUTE_URL_ROOT | |
16 | +from flashy.settings import QUARTER_START, ABSOLUTE_URL_ROOT, IN_PRODUCTION, GCM_API_KEY | |
16 | 17 | from simple_email_confirmation import SimpleEmailConfirmationUserMixin, EmailAddress |
17 | 18 | from fields import MaskField |
18 | 19 | from cached_property import cached_property |
19 | -from flashy.settings import IN_PRODUCTION | |
20 | 20 | |
21 | 21 | |
22 | 22 | |
... | ... | @@ -78,6 +78,8 @@ |
78 | 78 | REQUIRED_FIELDS = [] |
79 | 79 | sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") |
80 | 80 | confirmed_email = BooleanField(default=False) |
81 | + registration_token = CharField(null=True, default=None, max_length=4096) | |
82 | + last_notified = DateTimeField(null=True, default=None) | |
81 | 83 | |
82 | 84 | @property |
83 | 85 | def locked(self): |
... | ... | @@ -93,6 +95,19 @@ |
93 | 95 | ''' |
94 | 96 | send_mail("Flashy email verification", body % (ABSOLUTE_URL_ROOT, self.confirmation_key), |
95 | 97 | "noreply@flashy.cards", [self.email]) |
98 | + | |
99 | + def notify(self): | |
100 | + gcm = GCM(GCM_API_KEY) | |
101 | + gcm.plaintext_request( | |
102 | + registration_id=self.registration_token, | |
103 | + data="You have flashcards to study!" | |
104 | + ) | |
105 | + self.last_notified = now() | |
106 | + self.save() | |
107 | + | |
108 | + def set_registration_token(self, token): | |
109 | + self.registration_token = token | |
110 | + self.save() | |
96 | 111 | |
97 | 112 | def is_in_section(self, section): |
98 | 113 | return self.sections.filter(pk=section.pk).exists() |
flashcards/serializers.py
View file @
f6900af
flashcards/views.py
View file @
f6900af
... | ... | @@ -6,10 +6,10 @@ |
6 | 6 | IsAuthenticatedAndConfirmed |
7 | 7 | from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcardQuiz, \ |
8 | 8 | FlashcardAlreadyPulledException, FlashcardNotInDeckException |
9 | -from flashcards.notifications import notify_new_card, notify_pull | |
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 | - FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \ | |
12 | + FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, SubscribeViewSerializer, \ | |
13 | 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 |
... | ... | @@ -230,6 +230,19 @@ |
230 | 230 | auth.login(request, user) |
231 | 231 | log_event(request) |
232 | 232 | return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) |
233 | + | |
234 | + | |
235 | +@api_view(['POST']) | |
236 | +def subscribe(request, format=None): | |
237 | + """ | |
238 | + Associate the user with the passed in registration token | |
239 | + --- | |
240 | + request_serializer: SubscribeViewSerializer | |
241 | + """ | |
242 | + serializer = SubscribeViewSerializer(data=request.data) | |
243 | + serializer.is_valid(raise_exception=True) | |
244 | + request.user.set_registration_token(serializer.validated_data['registration_token']) | |
245 | + return Response(status=HTTP_204_NO_CONTENT) | |
233 | 246 | |
234 | 247 | |
235 | 248 | @api_view(['POST']) |
flashy/settings.py
View file @
f6900af
... | ... | @@ -169,6 +169,7 @@ |
169 | 169 | # If we're in production, SECRET_KEY is pulled from the environ |
170 | 170 | # Otherwise it doesn't matter |
171 | 171 | SECRET_KEY = os.environ.get('SECRET_KEY', 'LOL DEFAULT SECRET KEY') |
172 | +GCM_API_KEY = os.environ.get('GCM_API_KEY', 'GCM PLS') | |
172 | 173 | |
173 | 174 | CACHES = { |
174 | 175 | 'default': { |
flashy/urls.py
View file @
f6900af
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, resend_confirmation_email, verify_email | |
4 | + reset_password, logout, login, register, UserFlashcardQuizViewSet, resend_confirmation_email, verify_email, subscribe | |
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 |
... | ... | @@ -18,6 +18,7 @@ |
18 | 18 | url(r'^api/register/', register), |
19 | 19 | url(r'^api/login/$', login), |
20 | 20 | url(r'^api/logout/$', logout), |
21 | + url(r'^api/subscribe/$', subscribe), | |
21 | 22 | url(r'^api/me/sections/', UserSectionListView.as_view()), |
22 | 23 | url(r'^api/resend_confirmation_email/', resend_confirmation_email), |
23 | 24 | url(r'^api/verify_email/', verify_email), |