Commit e8079030e46eb506b6f03157dcdfdb69cf3fde3e
Exists in
master
Test
Merge branch 'master' of https://git.ucsd.edu/110swag/flashy-backend
Showing 6 changed files Side-by-side Diff
flashcards/admin.py
View file @
e807903
1 | 1 | from django.contrib import admin |
2 | 2 | from flashcards.models import Flashcard, UserFlashcard, Section, FlashcardMask, \ |
3 | - UserFlashcardReview | |
3 | + UserFlashcardReview, LecturePeriod, User | |
4 | +from simple_email_confirmation import EmailAddress | |
4 | 5 | |
5 | 6 | admin.site.register([ |
7 | + User, | |
6 | 8 | Flashcard, |
7 | 9 | FlashcardMask, |
8 | 10 | UserFlashcard, |
9 | 11 | UserFlashcardReview, |
10 | - Section | |
12 | + Section, | |
13 | + LecturePeriod | |
11 | 14 | ]) |
flashcards/api.py
View file @
e807903
1 | 1 | from django.core.mail import send_mail |
2 | +from django.contrib.auth import authenticate, login | |
3 | +from django.contrib.auth.tokens import default_token_generator | |
2 | 4 | from rest_framework.views import APIView |
3 | 5 | from rest_framework.response import Response |
4 | 6 | from rest_framework import status |
5 | -from rest_framework.exceptions import ValidationError | |
7 | +from rest_framework.exceptions import ValidationError, NotFound | |
6 | 8 | from flashcards.serializers import * |
7 | 9 | |
8 | 10 | |
... | ... | @@ -49,7 +51,7 @@ |
49 | 51 | raise ValidationError('Password is required') |
50 | 52 | |
51 | 53 | email = request.data['email'] |
52 | - user = User.objects.create_user(email) | |
54 | + user = User.objects.create_user(email, email=email, password=request.data['password']) | |
53 | 55 | |
54 | 56 | body = ''' |
55 | 57 | Visit the following link to confirm your email address: |
56 | 58 | |
... | ... | @@ -63,5 +65,91 @@ |
63 | 65 | "noreply@flashy.cards", |
64 | 66 | [user.email]) |
65 | 67 | |
68 | + user = authenticate(email=email, password=request.data['password']) | |
69 | + login(request, user) | |
70 | + return Response(UserSerializer(user).data) | |
71 | + | |
72 | + def delete(self, request, format=None): | |
73 | + request.user.delete() | |
74 | + return Response(status=status.HTTP_204_NO_CONTENT) | |
75 | + | |
76 | + | |
77 | +class UserLogin(APIView): | |
78 | + """ | |
79 | + Authenticates user and returns user data if valid. Handles invalid | |
80 | + users. | |
81 | + """ | |
82 | + | |
83 | + def post(self, request, format=None): | |
84 | + """ | |
85 | + Returns user data if valid. | |
86 | + """ | |
87 | + if 'email' not in request.data: | |
88 | + raise ValidationError('Email is required') | |
89 | + if 'password' not in request.data: | |
90 | + raise ValidationError('Password is required') | |
91 | + | |
92 | + email = request.data['email'] | |
93 | + password = request.data['password'] | |
94 | + user = authenticate(username=email, password=password) | |
95 | + | |
96 | + if user is None: | |
97 | + raise ValidationError('Invalid email or password') | |
98 | + if not user.is_active: | |
99 | + raise ValidationError('Account is disabled') | |
100 | + login(request, user) | |
66 | 101 | return Response(UserSerializer(User).data) |
102 | + | |
103 | + | |
104 | +class PasswordReset(APIView): | |
105 | + """ | |
106 | + Allows user to reset their password. | |
107 | + """ | |
108 | + | |
109 | + def post(self, request, format=None): | |
110 | + """ | |
111 | + Send a password reset token/link to the provided email. | |
112 | + """ | |
113 | + if 'email' not in request.data: | |
114 | + raise ValidationError('Email is required') | |
115 | + | |
116 | + email = request.data['email'] | |
117 | + | |
118 | + # Find the user since they are not logged in. | |
119 | + try: | |
120 | + user = User.objects.get(email=email) | |
121 | + except User.DoesNotExist: | |
122 | + raise NotFound('Email does not exist') | |
123 | + | |
124 | + token = default_token_generator.make_token(user) | |
125 | + | |
126 | + body = ''' | |
127 | + Visit the following link to reset your password: | |
128 | + http://flashy.cards/app/reset_password/%d/%s | |
129 | + | |
130 | + If you did not request a password reset, no action is required. | |
131 | + ''' | |
132 | + | |
133 | + send_mail("Flashy password reset", | |
134 | + body % (user.pk, token), | |
135 | + "noreply@flashy.cards", | |
136 | + [user.email]) | |
137 | + | |
138 | + return Response(status=status.HTTP_204_NO_CONTENT) | |
139 | + | |
140 | + def patch(self, request, format=None): | |
141 | + """ | |
142 | + Updates user's password to new password. | |
143 | + """ | |
144 | + if 'new_password' not in request.data: | |
145 | + raise ValidationError('New password is required') | |
146 | + if not request.data['new_password']: | |
147 | + raise ValidationError('Password cannot be blank') | |
148 | + | |
149 | + user = request.user | |
150 | + | |
151 | + user.set_password(request.data['new_password']) | |
152 | + user.save() | |
153 | + | |
154 | + return Response(status=status.HTTP_204_NO_CONTENT) |
flashcards/models.py
View file @
e807903
1 | -from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin | |
1 | +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin, AbstractUser | |
2 | 2 | from django.contrib.auth.tests.custom_user import CustomUser |
3 | 3 | from django.db.models import * |
4 | 4 | from simple_email_confirmation import SimpleEmailConfirmationUserMixin |
5 | 5 | |
6 | +# Hack to fix AbstractUser before subclassing it | |
7 | +AbstractUser._meta.get_field('email')._unique = True | |
6 | 8 | |
7 | -class UserManager(BaseUserManager): | |
8 | - def create_user(self, email, password=None): | |
9 | - """ | |
10 | - Creates and saves a User with the given email, date of | |
11 | - birth and password. | |
12 | - """ | |
13 | - if not email: | |
14 | - raise ValueError('Users must have an email address') | |
15 | - | |
16 | - user = self.model(email=self.normalize_email(email)) | |
17 | - | |
18 | - user.set_password(password) | |
19 | - user.save(using=self._db) | |
20 | - return user | |
21 | - | |
22 | - def create_superuser(self, email, password): | |
23 | - """ | |
24 | - Creates and saves a superuser with the given email and password. | |
25 | - """ | |
26 | - user = self.create_user(email, password=password) | |
27 | - user.is_staff = True | |
28 | - user.save(using=self._db) | |
29 | - return user | |
30 | - | |
31 | - | |
32 | -class User(AbstractBaseUser, SimpleEmailConfirmationUserMixin, ): | |
9 | +class User(AbstractUser, SimpleEmailConfirmationUserMixin, ): | |
33 | 10 | USERNAME_FIELD = 'email' |
34 | 11 | REQUIRED_FIELDS = [] |
35 | - | |
36 | - objects = UserManager() | |
37 | - is_staff = BooleanField(default=False) | |
38 | - | |
39 | - email = EmailField( | |
40 | - verbose_name='email address', | |
41 | - max_length=255, | |
42 | - unique=True, | |
43 | - ) | |
44 | - date_joined = DateTimeField(auto_now_add=True) | |
45 | 12 | sections = ManyToManyField('Section') |
46 | 13 | |
47 | 14 | |
... | ... | @@ -52,7 +19,7 @@ |
52 | 19 | 2. A user used to have a flashcard in their deck |
53 | 20 | 3. A user has a flashcard hidden from them |
54 | 21 | """ |
55 | - user = ForeignKey(User) | |
22 | + user = ForeignKey('User') | |
56 | 23 | mask = ForeignKey('FlashcardMask', help_text="A mask which overrides the card's mask") |
57 | 24 | pulled = DateTimeField(blank=True, null=True, help_text="When the user pulled the card") |
58 | 25 | flashcard = ForeignKey('Flashcard') |
flashcards/serializers.py
View file @
e807903
... | ... | @@ -18,11 +18,10 @@ |
18 | 18 | |
19 | 19 | |
20 | 20 | class UserSerializer(HyperlinkedModelSerializer): |
21 | - """ | |
22 | - """ | |
23 | 21 | email = EmailField(required=False) |
22 | + sections = HyperlinkedRelatedField(queryset=Section.objects.all(), many=True, view_name='section-detail') | |
24 | 23 | |
25 | 24 | class Meta: |
26 | 25 | model = User |
27 | - fields = ("email", "is_active", "last_login", "date_joined") | |
26 | + fields = ("sections", "email", "is_active", "last_login", "date_joined") |
flashy/settings.py
View file @
e807903
flashy/urls.py
View file @
e807903
... | ... | @@ -9,7 +9,9 @@ |
9 | 9 | router.register(r'lectureperiods', LecturePeriodViewSet) |
10 | 10 | |
11 | 11 | urlpatterns = [ |
12 | - url(r'^api/user/me$', UserDetail.as_view()), | |
12 | + url(r'^api/users/me$', UserDetail.as_view()), | |
13 | + url(r'^api/login$', UserLogin.as_view()), | |
14 | + url(r'^api/reset_password$', PasswordReset.as_view()), | |
13 | 15 | url(r'^api/', include(router.urls)), |
14 | 16 | url(r'^admin/doc/', include('django.contrib.admindocs.urls')), |
15 | 17 | url(r'^admin/', include(admin.site.urls)), |