# Eventually this will supercede most of flashcards.views.api # We're transitioning POX etc. RPC crap to mostly legit REST. import random from catnap.django_urls import absolute_reverse as reverse from catnap.restviews import (JsonEmitterMixin, AutoContentTypeMixin, RestView, ListView, DetailView, DeletionMixin) from catnap.exceptions import (HttpForbiddenException, HttpTemporaryRedirectException) from catnap.auth import BasicAuthentication, DjangoContribAuthentication from django.contrib.auth.decorators import login_required from django.db import transaction from django.forms import forms from django.forms.models import modelformset_factory, formset_factory from django.http import Http404, HttpResponseRedirect, HttpResponse from django.shortcuts import get_object_or_404, render_to_response from django.utils.decorators import method_decorator from django.template import RequestContext, loader from django.views.generic.create_update import ( update_object, delete_object, create_object) from dojango.decorators import json_response from dojango.util import to_dojo_data, json_decode, json_encode from apps.utils import japanese, querycleaner from apps.utils.querycleaner import clean_query from flashcards import models from flashcards.contextprocessors import review_start_context from flashcards.forms import DeckForm, FactForm, FieldContentForm, CardForm from flashcards.models import Card from flashcards.models.constants import MAX_NEW_CARD_ORDINAL from flashcards.models.undo import UndoCardReview from flashcards.restresources import ( UserResource, DeckResource, CardResource) from flashcards.views.decorators import has_card_query_filters from flashcards.views.shortcuts import get_deck_or_404 class ManabiRestView(JsonEmitterMixin, AutoContentTypeMixin, RestView): ''' Our JSON-formatted response base class. ''' content_type_template_string = 'application/vnd.org.manabi.{0}+json' authenticators = (DjangoContribAuthentication(), BasicAuthentication(realm='manabi')) class CardQueryFiltersMixin(object): def get_deck(self): if self.request.GET.get('deck'): return get_object_or_404( models.Deck, pk=self.request.GET['deck']) def get_tags(self): try: #TODO support for multiple tags tag_id = int(self.request.GET.get('tags', self.request.GET.get('tag', -1))) except ValueError: tag_id = -1 if tag_id != -1: tag_ids = [tag_id] #TODO support multiple tags return usertagging.models.Tag.objects.filter( id__in=tag_ids) # Resource views class EntryPoint(ManabiRestView): ''' Entry-point to our REST API. This view's URL is the only one that clients should need to know, and the only one that should be documented in the API! Also includes some fields for reviewing all decks at once. ''' def get(self, request): ''' List the available top-level resource URLs. ''' context = { 'deck_list_url': reverse('rest-deck_list'), 'shared_deck_list_url': reverse('rest-shared_deck_list'), 'next_cards_for_review_url': reverse( 'rest-next_cards_for_review'), 'review_undo_stack_url': reverse('rest-review_undo_stack'), 'all_decks_url': reverse('rest-all_decks'), } return self.render_to_response(context) class AllDecks(ManabiRestView): def get(self, request): context = review_start_context(self.request) context['owned_by_current_user'] = True return self.render_to_response(context) def get_url(self): return reverse('rest-all_decks') class DeckList(ListView, ManabiRestView): ''' List of the logged-in user's decks. ''' content_subtype = 'DeckList' resource_class = DeckResource def get_queryset(self): return models.Deck.objects.filter( owner=self.request.user, active=True).order_by('name') def get_url(self): return reverse('rest-deck_list') class Deck(DeletionMixin, DetailView, ManabiRestView): ''' Detail view of a single deck. Could be the user's deck, or a shared one that he could add to his library. ''' content_subtype = 'Deck' resource_class = DeckResource def get_object(self): deck = get_deck_or_404(self.request.user, self.kwargs.get('pk')) # If the user is already subscribed to this deck, # redirect to their subscribed copy. subscriber = deck.get_subscriber_deck_for_user(self.request.user) if subscriber: raise HttpTemporaryRedirectException( reverse('rest-deck', args=[subscriber.id])) return deck def get_context_data(self, *args, **kwargs): ''' Add `request.user`-dependent fields, for sharing or subscribing. ''' context = super(Deck, self).get_context_data(*args, **kwargs) deck = self.get_object() context['owned_by_current_user'] = deck.owner == self.request.user # add the review-related data. if context['owned_by_current_user']: context.update(review_start_context(self.request, deck)) else: context['subscription_url'] = reverse( 'rest-deck_subscription', args=[deck.id]) return context def allowed_methods(self, request, *args, **kwargs): '''Don't return "DELETE" if the user has no permission.''' methods = super(Deck, self).allowed_methods( request, *args, **kwargs) deck = self.get_object() if deck.owner != request.user: methods.remove('delete') return methods class DeckStatus(ManabiRestView): ''' Shows and can update the status of a deck. For now, this only handles suspending decks. ''' def get(self, request, **kwargs): deck = get_deck_or_404(request.user, kwargs.get('pk')) return self.render_to_response({'suspended': deck.suspended}) def post(self, request, **kwargs): deck = get_deck_or_404(request.user, kwargs.get('pk'), must_own=True) params = clean_query(request.POST, {'suspended': bool}) deck.suspended = params.get('suspended', deck.suspended) deck.save() return self.render_to_response({'suspended': deck.suspended}) class SharedDeckList(ListView, ManabiRestView): ''' List of shared decks from any user. ''' content_subtype = 'DeckList' resource_class = DeckResource queryset = models.Deck.objects.shared_decks() def get_url(self): return reverse('rest-shared_deck_list') class DeckSubscription(ManabiRestView): ''' Currently a write-only resource, which is used to subscribe to another user's shared deck. Each shared deck has a DeckSubscription URL which can be POSTed to, with a response indicating the URL of the newly created deck in the user's library. If the user is already subscribed to the deck, it will return the URL of the already-subscribed deck copy. ''' content_subtype = 'DeckSubscription' def post(self, request, **kwargs): user = self.request.user deck = get_deck_or_404(user, self.kwargs.get('pk')) # Is it already this user's deck? if deck.owner == user: return self.responses.see_other( reverse('rest-deck', args=[deck.id])) # Check if the user is aready subscribed to this deck. subscriber_deck = deck.get_subscriber_deck_for_user(user) if subscriber_deck: return self.responses.see_other( reverse('rest-deck', args=[subscriber_deck.id])) # Create the new subscriber deck and give its URL. new_deck = deck.subscribe(user) return self.responses.created( reverse('rest-deck', args=[new_deck.id])) class NextCardsForReview(CardQueryFiltersMixin, ManabiRestView): ''' Returns a list of Card resources -- the next cards up for review. Accepts some query parameters for filtering and influencing what cards will be selected. ''' content_subtype = 'CardList' query_structure = { 'count': int, 'early_review': bool, 'learn_more': bool, 'session_start': bool, # Beginning of review session? 'excluded_cards': querycleaner.int_list, } def get(self, request, **kwargs): params = clean_query(request.GET, self.query_structure) count = params.get('count', 5) next_cards = models.Card.objects.next_cards( request.user, count, excluded_ids=params.get('excluded_cards', []), session_start=params.get('session_start'), deck=self.get_deck(), tags=self.get_tags(), early_review=params.get('early_review'), learn_more=params.get('learn_more'), ) #FIXME need to account for 0 cards returned # Assemble a list of the cards to be serialized. return self.render_to_response({ 'card_list': [CardResource(card).get_data() for card in next_cards], }) class Card(DetailView, ManabiRestView): ''' Detail view of a single card. Currently only used for retrieving a specific card during a review session. ''' resource_class = CardResource def get_object(self): card = get_object_or_404(models.Card, pk=self.kwargs.get('pk')) if card.owner != self.request.user: raise PermissionDenied('You do not own this flashcard.') return card class CardReviews(ManabiRestView): ''' Currently a write-only resource for review operations. ''' content_subtype = 'CardReview' def post(self, request, **kwargs): # `duration` is in seconds (the time taken from viewing the card # to clicking Show Answer). params = clean_query(request.POST, { 'grade': int, 'duration': float, 'questionDuration': float }) card = get_object_or_404(models.Card, pk=self.kwargs.get('pk')) if card.owner != request.user: raise PermissionDenied('You do not own this flashcard.') card.review( params['grade'], duration=params.get('duration'), question_duration=params.get('questionDuration')) return self.responses.no_content() class ReviewUndo(ManabiRestView): ''' Undo stack for card reviews. A write (POST) or delete-only resource, at the moment. ''' def post(self, request): UndoCardReview.objects.undo(request.user) return self.responses.no_content() def delete(self, request): UndoCardReview.objects.reset(request.user) return self.responses.no_content()