Commit f0b284adb26e03956a15ceb1f139af762ab4c06e

Authored by Rohan Rangray
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)