Commit f0b284adb26e03956a15ceb1f139af762ab4c06e
1 parent
21835759b7
Exists in
master
Refactored FlashcardReport into FlashcardHide. Removed unpulled from UserFlashca…
…rd. Added flashcard edit in FlashcardViewSet.patch
Showing 4 changed files with 79 additions and 52 deletions Side-by-side Diff
flashcards/models.py
View file @
f0b284a
1 | 1 | from django.contrib.auth.models import AbstractUser, UserManager |
2 | +from django.core.exceptions import PermissionDenied | |
2 | 3 | from django.db.models import * |
3 | 4 | from django.utils.timezone import now |
4 | 5 | from simple_email_confirmation import SimpleEmailConfirmationUserMixin |
5 | 6 | from fields import MaskField |
7 | +from datetime import datetime | |
6 | 8 | |
7 | 9 | # Hack to fix AbstractUser before subclassing it |
8 | 10 | AbstractUser._meta.get_field('email')._unique = True |
9 | 11 | |
... | ... | @@ -45,7 +47,17 @@ |
45 | 47 | REQUIRED_FIELDS = [] |
46 | 48 | sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") |
47 | 49 | |
50 | + def is_in_section(self, section): | |
51 | + return section in self.sections.all() | |
48 | 52 | |
53 | + def pull(self, flashcard): | |
54 | + if not self.is_in_section(flashcard.section): | |
55 | + raise ValueError("User not in the section this flashcard belongs to") | |
56 | + user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) | |
57 | + user_card.pulled = datetime.now() | |
58 | + user_card.save() | |
59 | + | |
60 | + | |
49 | 61 | class UserFlashcard(Model): |
50 | 62 | """ |
51 | 63 | Represents the relationship between a user and a flashcard by: |
... | ... | @@ -57,7 +69,6 @@ |
57 | 69 | mask = MaskField(max_length=255, null=True, blank=True, default=None, help_text="The user-specific mask on the card") |
58 | 70 | pulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user pulled the card") |
59 | 71 | flashcard = ForeignKey('Flashcard') |
60 | - unpulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user unpulled this card") | |
61 | 72 | |
62 | 73 | class Meta: |
63 | 74 | # There can be at most one UserFlashcard for each User and Flashcard |
64 | 75 | |
65 | 76 | |
66 | 77 | |
... | ... | @@ -66,20 +77,25 @@ |
66 | 77 | # By default, order by most recently pulled |
67 | 78 | ordering = ['-pulled'] |
68 | 79 | |
69 | - def is_hidden(self): | |
70 | - """ | |
71 | - A card is hidden only if a user has not ever added it to their deck. | |
72 | - :return: Whether the flashcard is hidden from the user | |
73 | - """ | |
74 | - return not self.pulled | |
75 | 80 | |
76 | - def is_in_deck(self): | |
77 | - """ | |
78 | - :return:Whether the flashcard is in the user's deck | |
79 | - """ | |
80 | - return self.pulled and not self.unpulled | |
81 | +class FlashcardHide(Model): | |
82 | + """ | |
83 | + Represents the property of a flashcard being hidden by a user. | |
84 | + Each instance of this class represents a single user hiding a single flashcard. | |
85 | + If reason is null, the flashcard was just hidden. | |
86 | + If reason is not null, the flashcard was reported, and reason is the reason why it was reported. | |
87 | + """ | |
88 | + user = ForeignKey('User') | |
89 | + flashcard = ForeignKey('Flashcard') | |
90 | + reason = CharField(max_length=255, blank=True, null=True) | |
91 | + hidden = DateTimeField(auto_now_add=True) | |
81 | 92 | |
93 | + class Meta: | |
94 | + # There can only be one FlashcardHide object for each User and Flashcard | |
95 | + unique_together = (('user', 'flashcard'),) | |
96 | + index_together = ["user", "flashcard"] | |
82 | 97 | |
98 | + | |
83 | 99 | class Flashcard(Model): |
84 | 100 | text = CharField(max_length=255, help_text='The text on the card') |
85 | 101 | section = ForeignKey('Section', help_text='The section with which the card is associated') |
... | ... | @@ -107,6 +123,34 @@ |
107 | 123 | if not result.exists(): return self.is_hidden |
108 | 124 | return result[0].is_hidden() |
109 | 125 | |
126 | + def edit(self, user, new_flashcard): | |
127 | + """ | |
128 | + Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. | |
129 | + Sets up everything correctly so this object, when saved, will result in the appropriate changes. | |
130 | + :param user: The user editing this card. | |
131 | + :param new_flashcard: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. | |
132 | + """ | |
133 | + if not user.is_in_section(self.section): | |
134 | + raise PermissionDenied("You don't have the permission to edit this card") | |
135 | + | |
136 | + # content_changed is True iff either material_date or text were changed | |
137 | + content_changed = False | |
138 | + # create_new is True iff the user editing this card is the author of this card | |
139 | + # and there are no other users with this card in their decks | |
140 | + create_new = user != self.author or \ | |
141 | + UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() | |
142 | + | |
143 | + if 'material_date' in new_flashcard and self.material_date != new_flashcard['material_date']: | |
144 | + content_changed |= True | |
145 | + self.material_date = new_flashcard['material_date'] | |
146 | + if 'text' in new_flashcard and self.text != new_flashcard['text']: | |
147 | + content_changed |= True | |
148 | + self.text = new_flashcard['text'] | |
149 | + if create_new and content_changed: | |
150 | + self.pk = None | |
151 | + if 'mask' in new_flashcard: | |
152 | + self.mask = new_flashcard['mask'] | |
153 | + | |
110 | 154 | @classmethod |
111 | 155 | def cards_visible_to(cls, user): |
112 | 156 | """ |
... | ... | @@ -236,16 +280,4 @@ |
236 | 280 | """ |
237 | 281 | email = EmailField() |
238 | 282 | section = ForeignKey(Section, related_name='whitelist') |
239 | - | |
240 | - | |
241 | -class FlashcardReport(Model): | |
242 | - """ | |
243 | - A report by a user that a flashcard is unsuitable. A reason is optional | |
244 | - """ | |
245 | - user = ForeignKey(User) | |
246 | - flashcard = ForeignKey(Flashcard) | |
247 | - reason = CharField(max_length=255, blank=True) | |
248 | - | |
249 | - class Meta: | |
250 | - unique_together = (('user', 'flashcard'),) |
flashcards/serializers.py
View file @
f0b284a
... | ... | @@ -173,4 +173,10 @@ |
173 | 173 | if date > quarter_end: |
174 | 174 | raise serializers.ValidationError("Invalid material_date for the flashcard") |
175 | 175 | return date |
176 | + | |
177 | + def validate(self, attrs): | |
178 | + # Make sure that at least one of the attributes was passed in | |
179 | + if not any(i in attrs for i in ['material_date', 'text', 'mask']): | |
180 | + raise serializers.ValidationError("No new value passed in") | |
181 | + return attrs |
flashcards/tests/test_models.py
View file @
f0b284a
... | ... | @@ -62,12 +62,8 @@ |
62 | 62 | self.assertEqual(flashcard.text[slice(*blank1)], 'This') |
63 | 63 | self.assertEqual(flashcard.text[slice(*blank2)], 'Flashcard') |
64 | 64 | try: |
65 | - Flashcard.objects.create(text="This is the text of the Flashcard", | |
66 | - section=section, | |
67 | - author=user, | |
68 | - material_date=datetime.now(), | |
69 | - previous=None, | |
70 | - mask={(10,34), (0, 14)}) | |
65 | + flashcard.mask = {(10, 34), (0, 14)} | |
66 | + flashcard.save() | |
71 | 67 | self.fail() |
72 | 68 | except OverlapIntervalException: |
73 | 69 | self.assertTrue(True) |
flashcards/views.py
View file @
f0b284a
1 | 1 | from django.contrib import auth |
2 | 2 | from flashcards.api import StandardResultsSetPagination |
3 | -from flashcards.models import Section, User, Flashcard, FlashcardReport, UserFlashcard | |
3 | +from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard | |
4 | 4 | from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ |
5 | 5 | PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ |
6 | 6 | FlashcardUpdateSerializer |
7 | 7 | |
... | ... | @@ -289,9 +289,10 @@ |
289 | 289 | parameters_strategy: |
290 | 290 | form: replace |
291 | 291 | """ |
292 | - obj, created = FlashcardReport.objects.get_or_create(user=request.user, flashcard=self.get_object()) | |
292 | + obj, created = FlashcardHide.objects.get_or_create(user=request.user, flashcard=self.get_object()) | |
293 | 293 | obj.reason = request.data['reason'] |
294 | - obj.save() | |
294 | + if created: | |
295 | + obj.save() | |
295 | 296 | return Response(status=HTTP_204_NO_CONTENT) |
296 | 297 | |
297 | 298 | @detail_route(methods=['POST'], permission_classes=[IsAuthenticated]) |
... | ... | @@ -304,8 +305,7 @@ |
304 | 305 | """ |
305 | 306 | user = request.user |
306 | 307 | flashcard = self.get_object() |
307 | - user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=flashcard) | |
308 | - user_card.save() | |
308 | + user.pull(flashcard) | |
309 | 309 | return Response(status=HTTP_204_NO_CONTENT) |
310 | 310 | |
311 | 311 | @detail_route(methods=['PATCH'], permission_classes=[IsAuthenticated]) |
312 | 312 | |
313 | 313 | |
314 | 314 | |
... | ... | @@ -318,26 +318,19 @@ |
318 | 318 | """ |
319 | 319 | user = request.user |
320 | 320 | flashcard = Flashcard.objects.get(pk=pk) |
321 | - mask = flashcard.mask | |
322 | 321 | data = FlashcardUpdateSerializer(data=request.data) |
323 | 322 | data.is_valid(raise_exception=True) |
324 | 323 | new_flashcard = data.validated_data |
325 | - if ('material_date' in new_flashcard and new_flashcard['material_date'] != flashcard.material_date) \ | |
326 | - or ('text' in new_flashcard and new_flashcard.text != flashcard.text): | |
327 | - if flashcard.author != user or UserFlashcard.objects.filter(flashcard=flashcard).count() > 1: | |
328 | - flashcard.pk = None | |
329 | - flashcard.mask = mask | |
330 | - if 'material_date' in new_flashcard: | |
331 | - flashcard.material_date = new_flashcard['material_date'] | |
332 | - if 'text' in new_flashcard: | |
333 | - flashcard.text = new_flashcard['text'] | |
334 | - if 'mask' in new_flashcard: | |
335 | - flashcard.mask = new_flashcard['mask'] | |
324 | + | |
325 | + flashcard.edit(user, new_flashcard) | |
336 | 326 | flashcard.save() |
337 | - user_flashcard, created = UserFlashcard.objects.get_or_create(user=user, flashcard=flashcard) | |
338 | - if created: | |
339 | - user_flashcard.pulled = datetime.now() | |
340 | - user_flashcard.mask = flashcard.mask | |
327 | + user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=flashcard) | |
328 | + user_card.mask = flashcard.mask | |
329 | + | |
341 | 330 | if 'mask' in new_flashcard: |
342 | - user_flashcard.mask = new_flashcard['mask'] | |
331 | + user_card.mask = new_flashcard['mask'] | |
332 | + if 'mask' in new_flashcard or created: | |
333 | + user_card.save() | |
334 | + | |
335 | + return Response(status=HTTP_204_NO_CONTENT) |