diff --git a/flashcards/management/commands/notifyusers.py b/flashcards/management/commands/notifyusers.py new file mode 100644 index 0000000..db83efc --- /dev/null +++ b/flashcards/management/commands/notifyusers.py @@ -0,0 +1,19 @@ +from django.core.management import BaseCommand +from flashcards.models import UserFlashcard, Now +from django.utils.timezone import now +from datetime import timedelta + + +class Command(BaseCommand): + help = 'Notify the users if they have cards to be reviewed' + + def handle(self, *args, **options): + notify_list = UserFlashcard.objects.filter( + next_review__lte=Now() + ).exclude( + user__registration_token=None, + user__last_notified__range=(now()-timedelta(days=1), now()) + ).values_list('user').distinct().all() + + for user in notify_list: + user.notify() diff --git a/flashcards/migrations/0015_user_registration_token.py b/flashcards/migrations/0015_user_registration_token.py new file mode 100644 index 0000000..1d54ce7 --- /dev/null +++ b/flashcards/migrations/0015_user_registration_token.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('flashcards', '0014_auto_20150601_1315'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='registration_token', + field=models.CharField(default=None, max_length=4096, null=True), + ), + ] diff --git a/flashcards/migrations/0016_user_last_notified.py b/flashcards/migrations/0016_user_last_notified.py new file mode 100644 index 0000000..897581c --- /dev/null +++ b/flashcards/migrations/0016_user_last_notified.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('flashcards', '0015_user_registration_token'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='last_notified', + field=models.DateTimeField(default=None, null=True), + ), + ] diff --git a/flashcards/models.py b/flashcards/models.py index 486116d..f459a4c 100644 --- a/flashcards/models.py +++ b/flashcards/models.py @@ -1,6 +1,7 @@ from math import log1p from math import exp from datetime import datetime, timedelta +from gcm import GCM from django.contrib.auth.models import AbstractUser, UserManager from django.contrib.auth.tokens import default_token_generator @@ -12,11 +13,10 @@ from django.core.validators import MinLengthValidator from django.db import IntegrityError from django.db.models import * from django.utils.timezone import now, make_aware -from flashy.settings import QUARTER_START, ABSOLUTE_URL_ROOT +from flashy.settings import QUARTER_START, ABSOLUTE_URL_ROOT, IN_PRODUCTION, GCM_API_KEY from simple_email_confirmation import SimpleEmailConfirmationUserMixin, EmailAddress from fields import MaskField from cached_property import cached_property -from flashy.settings import IN_PRODUCTION @@ -78,6 +78,8 @@ class User(AbstractUser, SimpleEmailConfirmationUserMixin): REQUIRED_FIELDS = [] sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") confirmed_email = BooleanField(default=False) + registration_token = CharField(null=True, default=None, max_length=4096) + last_notified = DateTimeField(null=True, default=None) @property def locked(self): @@ -94,6 +96,19 @@ class User(AbstractUser, SimpleEmailConfirmationUserMixin): send_mail("Flashy email verification", body % (ABSOLUTE_URL_ROOT, self.confirmation_key), "noreply@flashy.cards", [self.email]) + def notify(self): + gcm = GCM(GCM_API_KEY) + gcm.plaintext_request( + registration_id=self.registration_token, + data="You have flashcards to study!" + ) + self.last_notified = now() + self.save() + + def set_registration_token(self, token): + self.registration_token = token + self.save() + def is_in_section(self, section): return self.sections.filter(pk=section.pk).exists() diff --git a/flashcards/serializers.py b/flashcards/serializers.py index 56f3f81..2de42e2 100644 --- a/flashcards/serializers.py +++ b/flashcards/serializers.py @@ -293,3 +293,7 @@ class QuizAnswerRequestSerializer(ModelSerializer): class Meta: model = UserFlashcardQuiz exclude = 'blanked_word', 'user_flashcard', 'when' + + +class SubscribeViewSerializer(Serializer): + registration_token = CharField(allow_blank=False, allow_null=False) diff --git a/flashcards/views.py b/flashcards/views.py index d3b1044..35b4d67 100644 --- a/flashcards/views.py +++ b/flashcards/views.py @@ -6,10 +6,10 @@ from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedS IsAuthenticatedAndConfirmed from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcardQuiz, \ FlashcardAlreadyPulledException, FlashcardNotInDeckException -from flashcards.notifications import notify_new_card, notify_pull +from flashcards.notifications import notify_new_card from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ - FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \ + FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, SubscribeViewSerializer, \ QuizAnswerRequestSerializer, DeepSectionSerializer, EmailVerificationSerializer, FeedRequestSerializer from rest_framework.decorators import detail_route, permission_classes, api_view, list_route from rest_framework.generics import ListAPIView, GenericAPIView @@ -233,6 +233,19 @@ def register(request, format=None): @api_view(['POST']) +def subscribe(request, format=None): + """ + Associate the user with the passed in registration token + --- + request_serializer: SubscribeViewSerializer + """ + serializer = SubscribeViewSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + request.user.set_registration_token(serializer.validated_data['registration_token']) + return Response(status=HTTP_204_NO_CONTENT) + + +@api_view(['POST']) def login(request): """ Authenticates user and returns user data if valid. diff --git a/flashy/settings.py b/flashy/settings.py index 928614b..9509dec 100644 --- a/flashy/settings.py +++ b/flashy/settings.py @@ -169,6 +169,7 @@ if IN_PRODUCTION: # If we're in production, SECRET_KEY is pulled from the environ # Otherwise it doesn't matter SECRET_KEY = os.environ.get('SECRET_KEY', 'LOL DEFAULT SECRET KEY') +GCM_API_KEY = os.environ.get('GCM_API_KEY', 'GCM PLS') CACHES = { 'default': { diff --git a/flashy/urls.py b/flashy/urls.py index cb87c09..8dc4fb8 100644 --- a/flashy/urls.py +++ b/flashy/urls.py @@ -1,7 +1,7 @@ from django.conf.urls import include, url from django.contrib import admin from flashcards.views import SectionViewSet, UserDetail, FlashcardViewSet, UserSectionListView, request_password_reset, \ - reset_password, logout, login, register, UserFlashcardQuizViewSet, resend_confirmation_email, verify_email + reset_password, logout, login, register, UserFlashcardQuizViewSet, resend_confirmation_email, verify_email, subscribe from flashy.frontend_serve import serve_with_default from flashy.settings import DEBUG, IN_PRODUCTION from rest_framework.routers import DefaultRouter @@ -18,6 +18,7 @@ urlpatterns = [ url(r'^api/register/', register), url(r'^api/login/$', login), url(r'^api/logout/$', logout), + url(r'^api/subscribe/$', subscribe), url(r'^api/me/sections/', UserSectionListView.as_view()), url(r'^api/resend_confirmation_email/', resend_confirmation_email), url(r'^api/verify_email/', verify_email), diff --git a/requirements.txt b/requirements.txt index d48568d..b4eee1b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,4 +13,5 @@ coverage django-rest-swagger pytz django-extensions -django-redis-sessions \ No newline at end of file +django-redis-sessions +python-gcm