diff --git a/flashcards/api.py b/flashcards/api.py index d555000..849f9d2 100644 --- a/flashcards/api.py +++ b/flashcards/api.py @@ -3,6 +3,8 @@ from rest_framework.pagination import PageNumberPagination from rest_framework.permissions import BasePermission +mock_no_params = lambda x:None + class StandardResultsSetPagination(PageNumberPagination): page_size = 40 page_size_query_param = 'page_size' diff --git a/flashcards/migrations/0001_squashed_0015_auto_20150518_0017.py b/flashcards/migrations/0001_squashed_0015_auto_20150518_0017.py new file mode 100644 index 0000000..cf4d9bf --- /dev/null +++ b/flashcards/migrations/0001_squashed_0015_auto_20150518_0017.py @@ -0,0 +1,357 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django.contrib.auth.models +import django.utils.timezone +from django.conf import settings +import django.core.validators +import simple_email_confirmation.models +import flashcards.models +import flashcards.fields + + +class Migration(migrations.Migration): + + replaces = [(b'flashcards', '0001_initial'), (b'flashcards', '0002_auto_20150504_1327'), (b'flashcards', '0003_auto_20150504_1600'), (b'flashcards', '0004_auto_20150506_1443'), (b'flashcards', '0005_auto_20150510_1458'), (b'flashcards', '0006_auto_20150512_0042'), (b'flashcards', '0007_userflashcard_mask'), (b'flashcards', '0008_section_department_abbreviation'), (b'flashcards', '0009_auto_20150512_0318'), (b'flashcards', '0010_auto_20150513_1546'), (b'flashcards', '0011_auto_20150514_0207'), (b'flashcards', '0012_auto_20150516_0313'), (b'flashcards', '0013_auto_20150517_0402'), (b'flashcards', '0013_auto_20150516_2356'), (b'flashcards', '0014_merge'), (b'flashcards', '0015_auto_20150518_0017')] + + dependencies = [ + ('auth', '__latest__'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(null=True, verbose_name='last login', blank=True)), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, max_length=30, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')], help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True, verbose_name='username')), + ('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)), + ('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)), + ('email', models.EmailField(unique=True, max_length=254, verbose_name='email address', blank=True)), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', to=b'auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', verbose_name='groups')), + ], + options={ + 'abstract': False, + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + }, + bases=(models.Model, simple_email_confirmation.models.SimpleEmailConfirmationUserMixin), + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='Flashcard', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('text', models.CharField(help_text=b'The text on the card', max_length=255)), + ('pushed', models.DateTimeField(help_text=b'When the card was first pushed', auto_now_add=True)), + ('material_date', models.DateTimeField(help_text=b'The date with which the card is associated')), + ('is_hidden', models.BooleanField(default=False)), + ('hide_reason', models.CharField(help_text=b'Reason for hiding this card', max_length=255, blank=True)), + ('author', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-pushed'], + }, + ), + migrations.CreateModel( + name='FlashcardMask', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('ranges', models.CharField(max_length=255)), + ], + ), + migrations.CreateModel( + name='FlashcardReport', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('reason', models.CharField(max_length=255)), + ('flashcard', models.ForeignKey(to='flashcards.Flashcard')), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='LecturePeriod', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('week_day', models.IntegerField(help_text=b'0-indexed day of week, starting at Monday')), + ('start_time', models.TimeField()), + ('end_time', models.TimeField()), + ], + ), + migrations.CreateModel( + name='Section', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('department', models.CharField(max_length=50)), + ('course_num', models.CharField(max_length=6)), + ('course_title', models.CharField(max_length=50)), + ('instructor', models.CharField(max_length=50)), + ('quarter', models.CharField(max_length=4)), + ('whitelist', models.ManyToManyField(related_name='whitelisted_sections', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-quarter'], + }, + ), + migrations.CreateModel( + name='UserFlashcard', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('pulled', models.DateTimeField(help_text=b'When the user pulled the card', null=True, blank=True)), + ('unpulled', models.DateTimeField(help_text=b'When the user unpulled this card', null=True, blank=True)), + ('flashcard', models.ForeignKey(to='flashcards.Flashcard')), + ('mask', models.ForeignKey(help_text=b"A mask which overrides the card's mask", to='flashcards.FlashcardMask')), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-pulled'], + }, + ), + migrations.CreateModel( + name='WhitelistedAddress', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('email', models.EmailField(max_length=254)), + ('section', models.ForeignKey(related_name='whitelist', to='flashcards.Section')), + ], + ), + migrations.AddField( + model_name='lectureperiod', + name='section', + field=models.ForeignKey(to='flashcards.Section'), + ), + migrations.AddField( + model_name='flashcard', + name='previous', + field=models.ForeignKey(blank=True, to='flashcards.Flashcard', help_text=b'The previous version of this card, if one exists', null=True), + ), + migrations.AddField( + model_name='flashcard', + name='section', + field=models.ForeignKey(help_text=b'The section with which the card is associated', to='flashcards.Section'), + ), + migrations.AddField( + model_name='user', + name='sections', + field=models.ManyToManyField(to=b'flashcards.Section'), + ), + migrations.AddField( + model_name='user', + name='user_permissions', + field=models.ManyToManyField(related_query_name='user', related_name='user_set', to=b'auth.Permission', blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions'), + ), + migrations.AlterUniqueTogether( + name='userflashcard', + unique_together=set([('user', 'flashcard')]), + ), + migrations.AlterIndexTogether( + name='userflashcard', + index_together=set([('user', 'flashcard')]), + ), + migrations.AlterUniqueTogether( + name='section', + unique_together=set([('department', 'course_num', 'quarter', 'instructor')]), + ), + migrations.AlterUniqueTogether( + name='lectureperiod', + unique_together=set([('section', 'start_time', 'week_day')]), + ), + migrations.AlterUniqueTogether( + name='flashcardreport', + unique_together=set([('user', 'flashcard')]), + ), + migrations.AlterModelOptions( + name='section', + options={}, + ), + migrations.AlterModelManagers( + name='user', + managers=[ + ('objects', flashcards.models.EmailOnlyUserManager()), + ], + ), + migrations.AlterField( + model_name='flashcardreport', + name='reason', + field=models.CharField(max_length=255, blank=True), + ), + migrations.AlterField( + model_name='lectureperiod', + name='week_day', + field=models.IntegerField(help_text=b'1-indexed day of week, starting at Sunday'), + ), + migrations.AlterField( + model_name='user', + name='sections', + field=models.ManyToManyField(help_text=b'The sections which the user is enrolled in', to=b'flashcards.Section'), + ), + migrations.AlterField( + model_name='user', + name='username', + field=models.CharField(help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=30, error_messages={'unique': 'A user with that username already exists.'}, verbose_name='username', validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')]), + ), + migrations.AlterField( + model_name='section', + name='instructor', + field=models.CharField(max_length=100), + ), + migrations.AlterField( + model_name='userflashcard', + name='mask', + field=models.ForeignKey(blank=True, to='flashcards.FlashcardMask', help_text=b"A mask which overrides the card's mask", null=True), + ), + migrations.CreateModel( + name='UserFlashcardQuiz', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('when', models.DateTimeField(auto_now=True)), + ('blanked_word', models.CharField(help_text=b'The character range which was blanked', max_length=8, blank=True)), + ('response', models.CharField(help_text=b"The user's response", max_length=255, null=True, blank=True)), + ('correct', models.NullBooleanField(help_text=b"The user's self-evaluation of their response")), + ('user_flashcard', models.ForeignKey(to='flashcards.UserFlashcard')), + ], + ), + migrations.AlterModelOptions( + name='section', + options={'ordering': ['-course_title']}, + ), + migrations.AlterUniqueTogether( + name='section', + unique_together=set([]), + ), + migrations.RemoveField( + model_name='section', + name='whitelist', + ), + migrations.RemoveField( + model_name='userflashcard', + name='mask', + ), + migrations.DeleteModel( + name='FlashcardMask', + ), + migrations.AddField( + model_name='flashcard', + name='mask', + field=flashcards.fields.MaskField(blank_sep=b',', range_sep=b'-', max_length=255, blank=True, help_text=b'The mask on the card', null=True), + ), + migrations.AddField( + model_name='userflashcard', + name='mask', + field=flashcards.fields.MaskField(default=None, blank_sep=b',', range_sep=b'-', max_length=255, blank=True, help_text=b'The user-specific mask on the card', null=True), + ), + migrations.AddField( + model_name='section', + name='department_abbreviation', + field=models.CharField(max_length=10, db_index=True), + ), + migrations.AlterField( + model_name='section', + name='course_num', + field=models.CharField(max_length=6, db_index=True), + ), + migrations.AlterField( + model_name='section', + name='course_title', + field=models.CharField(max_length=50, db_index=True), + ), + migrations.AlterField( + model_name='section', + name='department', + field=models.CharField(max_length=50, db_index=True), + ), + migrations.AlterField( + model_name='section', + name='instructor', + field=models.CharField(max_length=100, db_index=True), + ), + migrations.AlterField( + model_name='section', + name='quarter', + field=models.CharField(max_length=4, db_index=True), + ), + migrations.AlterField( + model_name='flashcard', + name='material_date', + field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'The date with which the card is associated'), + ), + migrations.AlterModelOptions( + name='lectureperiod', + options={'ordering': ['section', 'week_day']}, + ), + migrations.AlterField( + model_name='userflashcard', + name='pulled', + field=models.DateTimeField(default=None, help_text=b'When the user pulled the card', null=True, blank=True), + ), + migrations.RemoveField( + model_name='userflashcard', + name='unpulled', + ), + migrations.CreateModel( + name='FlashcardHide', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('reason', models.CharField(max_length=255, null=True, blank=True)), + ('hidden', models.DateTimeField(auto_now_add=True)), + ('flashcard', models.ForeignKey(to='flashcards.Flashcard')), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AlterUniqueTogether( + name='flashcardreport', + unique_together=set([]), + ), + migrations.RemoveField( + model_name='flashcardreport', + name='flashcard', + ), + migrations.RemoveField( + model_name='flashcardreport', + name='user', + ), + migrations.DeleteModel( + name='FlashcardReport', + ), + migrations.AlterUniqueTogether( + name='flashcardhide', + unique_together=set([('user', 'flashcard')]), + ), + migrations.AlterIndexTogether( + name='flashcardhide', + index_together=set([('user', 'flashcard')]), + ), + migrations.AlterField( + model_name='flashcard', + name='hide_reason', + field=models.CharField(default=None, help_text=b'Reason for hiding this card', max_length=255, blank=True), + ), + migrations.AlterField( + model_name='flashcard', + name='previous', + field=models.ForeignKey(default=None, blank=True, to='flashcards.Flashcard', help_text=b'The previous version of this card, if one exists', null=True), + ), + migrations.AlterField( + model_name='flashcard', + name='hide_reason', + field=models.CharField(default=None, max_length=255, null=True, help_text=b'Reason for hiding this card', blank=True), + ), + migrations.AlterField( + model_name='flashcard', + name='previous', + field=models.ForeignKey(default=None, blank=True, to='flashcards.Flashcard', help_text=b'The previous version of this card, if one exists', null=True), + ), + migrations.AlterField( + model_name='flashcard', + name='hide_reason', + field=models.CharField(default=b'', max_length=255, null=True, help_text=b'Reason for hiding this card', blank=True), + ), + ] diff --git a/flashcards/migrations/0015_auto_20150518_0017.py b/flashcards/migrations/0015_auto_20150518_0017.py new file mode 100644 index 0000000..898197e --- /dev/null +++ b/flashcards/migrations/0015_auto_20150518_0017.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_merge'), + ] + + operations = [ + migrations.AlterField( + model_name='flashcard', + name='hide_reason', + field=models.CharField(default=b'', max_length=255, null=True, help_text=b'Reason for hiding this card', blank=True), + ), + ] diff --git a/flashcards/models.py b/flashcards/models.py index 41ce37d..bec4c6c 100644 --- a/flashcards/models.py +++ b/flashcards/models.py @@ -12,7 +12,6 @@ from fields import MaskField AbstractUser._meta.get_field('email')._unique = True AbstractUser._meta.get_field('username')._unique = False - class EmailOnlyUserManager(UserManager): """ A tiny extension of Django's UserManager which correctly creates users @@ -143,10 +142,9 @@ class Flashcard(Model): if self.is_hidden or self.flashcardhide_set.filter(user=user).exists(): return True return False - - def hide_from(self, user): + def hide_from(self, user, reason=None): if self.is_in_deck(user): user.unpull(self) - obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self) + obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self, reason=None) if not created: raise ValidationError("The card has already been hidden.") obj.save() @@ -309,6 +307,9 @@ class Section(Model): qs = Flashcard.objects.filter(section=self).exclude(userflashcard__user=user).order_by('pushed') return qs + def get_cards_for_user(self, user): + return Flashcard.cards_visible_to(user).filter(section=self) + def __unicode__(self): return '%s %s: %s (%s %s)' % ( self.department_abbreviation, self.course_num, self.course_title, self.instructor, self.quarter) diff --git a/flashcards/serializers.py b/flashcards/serializers.py index 0744422..2f372b5 100644 --- a/flashcards/serializers.py +++ b/flashcards/serializers.py @@ -1,3 +1,5 @@ +from json import dumps, loads + from django.utils.datetime_safe import datetime from django.utils.timezone import now import pytz @@ -8,7 +10,6 @@ from rest_framework.fields import EmailField, BooleanField, CharField, IntegerFi from rest_framework.relations import HyperlinkedRelatedField from rest_framework.serializers import ModelSerializer, Serializer from rest_framework.validators import UniqueValidator -from json import dumps, loads class EmailSerializer(Serializer): @@ -132,28 +133,11 @@ class FlashcardSerializer(ModelSerializer): else: raise serializers.ValidationError("Material date is outside allowed range for this quarter") - def validate_previous(self, value): - if value is None: - return value - if Flashcard.objects.filter(pk=value.pk).count() > 0: - return value - raise serializers.ValidationError("Invalid previous Flashcard object") - def validate_pushed(self, value): if value > datetime.now(): raise serializers.ValidationError("Invalid creation date for the Flashcard") return value - def validate_text(self, value): - if len(value) > 255: - raise serializers.ValidationError("Flashcard text limit exceeded") - return value - - def validate_hide_reason(self, value): - if len(value) > 255: - raise serializers.ValidationError("Hide reason limit exceeded") - return value - def validate_mask(self, value): if value is None: return None @@ -163,7 +147,7 @@ class FlashcardSerializer(ModelSerializer): class Meta: model = Flashcard - exclude = 'author', + exclude = 'author', 'previous' class FlashcardUpdateSerializer(serializers.Serializer): diff --git a/flashcards/tests/test_api.py b/flashcards/tests/test_api.py index ac9781b..9a7f82f 100644 --- a/flashcards/tests/test_api.py +++ b/flashcards/tests/test_api.py @@ -207,7 +207,7 @@ class FlashcardDetailTest(APITestCase): self.inaccessible_flashcard.save() self.flashcard = Flashcard(text="jason", section=self.section, author=self.user) self.flashcard.save() - self.flashcard.add_to_deck(self.user) + #self.flashcard.add_to_deck(self.user) self.client.login(email='none@none.com', password='1234') def test_edit_flashcard(self): @@ -259,9 +259,6 @@ class FlashcardDetailTest(APITestCase): self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) self.assertTrue(self.flashcard.is_hidden_from(self.user)) - response = self.client.post('/api/flashcards/%d/hide/' % self.flashcard.id, format='json') - self.assertContains(response, 'The card has already been hidden', status_code=400) - response = self.client.post('/api/flashcards/%d/hide/' % self.inaccessible_flashcard.pk, format='json') # This should fail because the user is not enrolled in section id 2 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) diff --git a/flashcards/views.py b/flashcards/views.py index a6c0ea4..ea75d7c 100644 --- a/flashcards/views.py +++ b/flashcards/views.py @@ -9,7 +9,7 @@ from flashcards.serializers import SectionSerializer, UserUpdateSerializer, Regi FlashcardUpdateSerializer from rest_framework.decorators import detail_route, permission_classes, api_view, list_route from rest_framework.generics import ListAPIView, GenericAPIView -from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin +from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin from rest_framework.permissions import IsAuthenticated from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet from django.core.mail import send_mail @@ -42,11 +42,7 @@ class SectionViewSet(ReadOnlyModelViewSet): Add the current user to a specified section If the class has a whitelist, but the user is not on the whitelist, the request will fail. --- - omit_serializer: true - parameters: - - fake: None - parameters_strategy: - form: replace + view_mocker: flashcards.api.mock_no_params """ self.get_object().enroll(request.user) @@ -58,22 +54,26 @@ class SectionViewSet(ReadOnlyModelViewSet): Remove the current user from a specified section If the user is not in the class, the request will fail. --- - omit_serializer: true - parameters: - - fake: None - parameters_strategy: - form: replace + view_mocker: flashcards.api.mock_no_params """ try: self.get_object().drop(request.user) - except django.core.exceptions.PermissionDenied as e: raise PermissionDenied(e) - except django.core.exceptions.ValidationError as e: raise ValidationError(e) + except django.core.exceptions.PermissionDenied as e: + raise PermissionDenied(e) + except django.core.exceptions.ValidationError as e: + raise ValidationError(e) return Response(status=HTTP_204_NO_CONTENT) @list_route(methods=['GET']) def search(self, request): """ Returns a list of sections which match a user's query + --- + parameters: + - name: q + description: space-separated list of terms + required: true + type: form """ query = request.GET.get('q', None) if not query: return Response('[]') @@ -277,7 +277,7 @@ def reset_password(request, format=None): return Response(status=HTTP_204_NO_CONTENT) -class FlashcardViewSet(GenericViewSet, UpdateModelMixin, CreateModelMixin, RetrieveModelMixin): +class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): queryset = Flashcard.objects.all() serializer_class = FlashcardSerializer permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection] @@ -287,6 +287,8 @@ class FlashcardViewSet(GenericViewSet, UpdateModelMixin, CreateModelMixin, Retri serializer = FlashcardSerializer(data=request.data) serializer.is_valid(raise_exception=True) data = serializer.validated_data + if not request.user.is_in_section(data['section']): + raise PermissionDenied('The user is not enrolled in that section') data['author'] = request.user flashcard = Flashcard.objects.create(**data) self.perform_create(flashcard) @@ -294,33 +296,13 @@ class FlashcardViewSet(GenericViewSet, UpdateModelMixin, CreateModelMixin, Retri response_data = FlashcardSerializer(flashcard) return Response(response_data.data, status=HTTP_201_CREATED, headers=headers) - @detail_route(methods=['post']) - def hide(self, request, pk): - """ - Hide a flashcard - --- - omit_serializer: true - parameters: - - fake: None - parameters_strategy: - form: replace - """ - try: - self.get_object().hide_from(request.user) - except django.core.exceptions.ValidationError: - raise ValidationError("The card has already been hidden.") - return Response(status=HTTP_204_NO_CONTENT) @detail_route(methods=['post']) def unhide(self, request, pk): """ - Report the given card + Unhide the given card --- - omit_serializer: true - parameters: - - fake: None - parameters_strategy: - form: replace + view_mocker: flashcards.api.mock_no_params """ hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object()) hide.delete() @@ -329,27 +311,23 @@ class FlashcardViewSet(GenericViewSet, UpdateModelMixin, CreateModelMixin, Retri @detail_route(methods=['post']) def report(self, request, pk): """ - Report the given card + Hide the given card --- - omit_serializer: true - parameters: - - fake: None - parameters_strategy: - form: replace + view_mocker: flashcards.api.mock_no_params """ obj, created = FlashcardHide.objects.get_or_create(user=request.user, flashcard=self.get_object()) - obj.reason = request.data['reason'] - if created: - obj.save() + obj.reason = request.data.get('reason', None) + obj.save() return Response(status=HTTP_204_NO_CONTENT) + hide = report + @detail_route(methods=['POST']) def pull(self, request, pk): """ Pull a card from the live feed into the user's deck. - :param request: The request object - :param pk: The primary key - :return: A 204 response upon success. + --- + view_mocker: flashcards.api.mock_no_params """ user = request.user flashcard = self.get_object() @@ -360,21 +338,19 @@ class FlashcardViewSet(GenericViewSet, UpdateModelMixin, CreateModelMixin, Retri def unpull(self, request, pk): """ Unpull a card from the user's deck - :param request: The request object - :param pk: The primary key - :return: A 204 response upon success. + --- + view_mocker: flashcards.api.mock_no_params """ user = request.user flashcard = self.get_object() user.unpull(flashcard) return Response(status=HTTP_204_NO_CONTENT) - def update(self, request, *args, **kwargs): + def partial_update(self, request, *args, **kwargs): """ Edit settings related to a card for the user. - :param request: The request object. - :param pk: The primary key of the flashcard. - :return: A 204 response upon success. + --- + request_serializer: FlashcardUpdateSerializer """ user = request.user flashcard = self.get_object()