From f0b284adb26e03956a15ceb1f139af762ab4c06e Mon Sep 17 00:00:00 2001 From: Rohan Rangray Date: Sat, 16 May 2015 03:21:30 -0700 Subject: [PATCH] Refactored FlashcardReport into FlashcardHide. Removed unpulled from UserFlashcard. Added flashcard edit in FlashcardViewSet.patch --- flashcards/models.py | 80 ++++++++++++++++++++++++++++------------- flashcards/serializers.py | 6 ++++ flashcards/tests/test_models.py | 8 ++--- flashcards/views.py | 37 ++++++++----------- 4 files changed, 79 insertions(+), 52 deletions(-) diff --git a/flashcards/models.py b/flashcards/models.py index 0e1de5a..704cd15 100644 --- a/flashcards/models.py +++ b/flashcards/models.py @@ -1,8 +1,10 @@ from django.contrib.auth.models import AbstractUser, UserManager +from django.core.exceptions import PermissionDenied from django.db.models import * from django.utils.timezone import now from simple_email_confirmation import SimpleEmailConfirmationUserMixin from fields import MaskField +from datetime import datetime # Hack to fix AbstractUser before subclassing it AbstractUser._meta.get_field('email')._unique = True @@ -45,6 +47,16 @@ class User(AbstractUser, SimpleEmailConfirmationUserMixin): REQUIRED_FIELDS = [] sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") + def is_in_section(self, section): + return section in self.sections.all() + + def pull(self, flashcard): + if not self.is_in_section(flashcard.section): + raise ValueError("User not in the section this flashcard belongs to") + user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) + user_card.pulled = datetime.now() + user_card.save() + class UserFlashcard(Model): """ @@ -57,7 +69,6 @@ class UserFlashcard(Model): mask = MaskField(max_length=255, null=True, blank=True, default=None, help_text="The user-specific mask on the card") pulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user pulled the card") flashcard = ForeignKey('Flashcard') - unpulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user unpulled this card") class Meta: # There can be at most one UserFlashcard for each User and Flashcard @@ -66,18 +77,23 @@ class UserFlashcard(Model): # By default, order by most recently pulled ordering = ['-pulled'] - def is_hidden(self): - """ - A card is hidden only if a user has not ever added it to their deck. - :return: Whether the flashcard is hidden from the user - """ - return not self.pulled - def is_in_deck(self): - """ - :return:Whether the flashcard is in the user's deck - """ - return self.pulled and not self.unpulled +class FlashcardHide(Model): + """ + Represents the property of a flashcard being hidden by a user. + Each instance of this class represents a single user hiding a single flashcard. + If reason is null, the flashcard was just hidden. + If reason is not null, the flashcard was reported, and reason is the reason why it was reported. + """ + user = ForeignKey('User') + flashcard = ForeignKey('Flashcard') + reason = CharField(max_length=255, blank=True, null=True) + hidden = DateTimeField(auto_now_add=True) + + class Meta: + # There can only be one FlashcardHide object for each User and Flashcard + unique_together = (('user', 'flashcard'),) + index_together = ["user", "flashcard"] class Flashcard(Model): @@ -107,6 +123,34 @@ class Flashcard(Model): if not result.exists(): return self.is_hidden return result[0].is_hidden() + def edit(self, user, new_flashcard): + """ + Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. + Sets up everything correctly so this object, when saved, will result in the appropriate changes. + :param user: The user editing this card. + :param new_flashcard: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. + """ + if not user.is_in_section(self.section): + raise PermissionDenied("You don't have the permission to edit this card") + + # content_changed is True iff either material_date or text were changed + content_changed = False + # create_new is True iff the user editing this card is the author of this card + # and there are no other users with this card in their decks + create_new = user != self.author or \ + UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() + + if 'material_date' in new_flashcard and self.material_date != new_flashcard['material_date']: + content_changed |= True + self.material_date = new_flashcard['material_date'] + if 'text' in new_flashcard and self.text != new_flashcard['text']: + content_changed |= True + self.text = new_flashcard['text'] + if create_new and content_changed: + self.pk = None + if 'mask' in new_flashcard: + self.mask = new_flashcard['mask'] + @classmethod def cards_visible_to(cls, user): """ @@ -236,15 +280,3 @@ class WhitelistedAddress(Model): """ email = EmailField() section = ForeignKey(Section, related_name='whitelist') - - -class FlashcardReport(Model): - """ - A report by a user that a flashcard is unsuitable. A reason is optional - """ - user = ForeignKey(User) - flashcard = ForeignKey(Flashcard) - reason = CharField(max_length=255, blank=True) - - class Meta: - unique_together = (('user', 'flashcard'),) diff --git a/flashcards/serializers.py b/flashcards/serializers.py index 3bbe8df..20561d4 100644 --- a/flashcards/serializers.py +++ b/flashcards/serializers.py @@ -173,3 +173,9 @@ class FlashcardUpdateSerializer(serializers.Serializer): if date > quarter_end: raise serializers.ValidationError("Invalid material_date for the flashcard") return date + + def validate(self, attrs): + # Make sure that at least one of the attributes was passed in + if not any(i in attrs for i in ['material_date', 'text', 'mask']): + raise serializers.ValidationError("No new value passed in") + return attrs diff --git a/flashcards/tests/test_models.py b/flashcards/tests/test_models.py index 5028cc9..3563617 100644 --- a/flashcards/tests/test_models.py +++ b/flashcards/tests/test_models.py @@ -62,12 +62,8 @@ class FlashcardTests(TestCase): self.assertEqual(flashcard.text[slice(*blank1)], 'This') self.assertEqual(flashcard.text[slice(*blank2)], 'Flashcard') try: - Flashcard.objects.create(text="This is the text of the Flashcard", - section=section, - author=user, - material_date=datetime.now(), - previous=None, - mask={(10,34), (0, 14)}) + flashcard.mask = {(10, 34), (0, 14)} + flashcard.save() self.fail() except OverlapIntervalException: self.assertTrue(True) diff --git a/flashcards/views.py b/flashcards/views.py index 73503eb..daedaaa 100644 --- a/flashcards/views.py +++ b/flashcards/views.py @@ -1,6 +1,6 @@ from django.contrib import auth from flashcards.api import StandardResultsSetPagination -from flashcards.models import Section, User, Flashcard, FlashcardReport, UserFlashcard +from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ FlashcardUpdateSerializer @@ -289,9 +289,10 @@ class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): parameters_strategy: form: replace """ - obj, created = FlashcardReport.objects.get_or_create(user=request.user, flashcard=self.get_object()) + obj, created = FlashcardHide.objects.get_or_create(user=request.user, flashcard=self.get_object()) obj.reason = request.data['reason'] - obj.save() + if created: + obj.save() return Response(status=HTTP_204_NO_CONTENT) @detail_route(methods=['POST'], permission_classes=[IsAuthenticated]) @@ -304,8 +305,7 @@ class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): """ user = request.user flashcard = self.get_object() - user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=flashcard) - user_card.save() + user.pull(flashcard) return Response(status=HTTP_204_NO_CONTENT) @detail_route(methods=['PATCH'], permission_classes=[IsAuthenticated]) @@ -318,25 +318,18 @@ class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): """ user = request.user flashcard = Flashcard.objects.get(pk=pk) - mask = flashcard.mask data = FlashcardUpdateSerializer(data=request.data) data.is_valid(raise_exception=True) new_flashcard = data.validated_data - if ('material_date' in new_flashcard and new_flashcard['material_date'] != flashcard.material_date) \ - or ('text' in new_flashcard and new_flashcard.text != flashcard.text): - if flashcard.author != user or UserFlashcard.objects.filter(flashcard=flashcard).count() > 1: - flashcard.pk = None - flashcard.mask = mask - if 'material_date' in new_flashcard: - flashcard.material_date = new_flashcard['material_date'] - if 'text' in new_flashcard: - flashcard.text = new_flashcard['text'] - if 'mask' in new_flashcard: - flashcard.mask = new_flashcard['mask'] + + flashcard.edit(user, new_flashcard) flashcard.save() - user_flashcard, created = UserFlashcard.objects.get_or_create(user=user, flashcard=flashcard) - if created: - user_flashcard.pulled = datetime.now() - user_flashcard.mask = flashcard.mask + user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=flashcard) + user_card.mask = flashcard.mask + if 'mask' in new_flashcard: - user_flashcard.mask = new_flashcard['mask'] + user_card.mask = new_flashcard['mask'] + if 'mask' in new_flashcard or created: + user_card.save() + + return Response(status=HTTP_204_NO_CONTENT) \ No newline at end of file -- 1.9.1