import ObjectDoesNotExist closes #427
[oweals/karmaworld.git] / karmaworld / apps / notes / views.py
index 4cd3e3ec18374570164ef2fe2940afa6d34346c1..dc469efe6e136ab5805f47418dd78ffede6efa5e 100644 (file)
 # Copyright (C) 2012  FinalsClub Foundation
 
 import json
-from django.core.exceptions import ObjectDoesNotExist
-from karmaworld.apps.courses.models import Course
-from karmaworld.apps.notes.search import SearchIndex
+import logging
+import traceback
 
-import os
+from django.contrib import messages
 
-from django.conf import settings
-from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound
-from django.views.generic import DetailView, ListView
-from django.views.generic import FormView
+from django.core import serializers
+from django.core.exceptions import ValidationError
+from django.core.exceptions import ObjectDoesNotExist
+from django.forms.formsets import formset_factory
+
+from django.http import HttpResponse
+from django.http import HttpResponseRedirect
+from django.http import HttpResponseForbidden
+from django.http import HttpResponseBadRequest
+from django.views.generic import DetailView, ListView, TemplateView
+from django.views.generic import UpdateView, FormView
 from django.views.generic import View
 from django.views.generic.detail import SingleObjectMixin
 
-from karmaworld.apps.notes.models import Note
 from karmaworld.apps.notes.forms import NoteForm
+from karmaworld.apps.notes.forms import NoteDeleteForm
+from karmaworld.apps.notes.models import Note
+from karmaworld.apps.notes.models import NoteMarkdown
+from karmaworld.apps.notes.models import KEYWORD_MTURK_THRESHOLD
+from karmaworld.apps.notes.search import SearchIndex
+from karmaworld.apps.users.models import NoteKarmaEvent
+from karmaworld.apps.courses.forms import CourseForm
+from karmaworld.apps.quizzes.forms import KeywordForm
+from karmaworld.apps.quizzes.tasks import submit_extract_keywords_hit
+from karmaworld.apps.quizzes.tasks import get_extract_keywords_results
+from karmaworld.apps.courses.models import Course
+from karmaworld.apps.quizzes.models import Keyword
+from karmaworld.apps.quizzes.create_quiz import quiz_from_keywords
 
+from karmaworld.utils.ajax_utils import ajax_pk_base
+from karmaworld.utils.ajax_utils import ajax_increment
 
-PDF_MIMETYPES = (
-    'application/pdf',
-    'application/vnd.ms-powerpoint',
-    'application/vnd.openxmlformats-officedocument.presentationml.presentation'
-)
-
-def is_pdf(self):
-    if self.object.file_type == 'pdf':
-        return True
-    return False
-
-def is_ppt(self):
-    if self.object.file_type == 'ppt':
-        return True
-    return False
 
-def format_session_increment_field(id, field):
-    return field + '-' + str(id)
+logger = logging.getLogger(__name__)
 
 THANKS_FIELD = 'thanks'
+USER_PROFILE_THANKS_FIELD = 'thanked_notes'
 FLAG_FIELD = 'flags'
-
-class NoteDetailView(DetailView):
-    """ Class-based view for the note html page """
+USER_PROFILE_FLAGS_FIELD = 'flagged_notes'
+
+def note_page_context_helper(note, request, context):
+
+    if request.method == 'POST':
+        if not note.allows_edit_by(request.user):
+            # This user is Balrog. It. Shall. Not. Pass.
+            return HttpResponseForbidden()
+        # Only save tags if not forbidden above.
+        context['note_edit_form'] = NoteForm(request.POST)
+    else:
+        tags_string = ','.join([str(tag) for tag in note.tags.all()])
+        initial = {"name": note.name, "tags": tags_string}
+        try:
+            initial["html"] = note.notemarkdown.html
+        except NoteMarkdown.DoesNotExist:
+            pass
+        context['note_edit_form'] = NoteForm(initial=initial)
+
+    context['note_delete_form'] = NoteDeleteForm(initial={'note': note.id})
+
+    if note.is_pdf():
+        context['pdf_controls'] = True
+
+    if request.user.is_authenticated():
+        try:
+            request.user.get_profile().thanked_notes.get(pk=note.pk)
+            context['already_thanked'] = True
+        except ObjectDoesNotExist:
+            pass
+
+        try:
+            request.user.get_profile().flagged_notes.get(pk=note.pk)
+            context['already_flagged'] = True
+        except ObjectDoesNotExist:
+            pass
+
+class NoteView(UpdateView):
+    form_class = NoteForm
     model = Note
-    context_object_name = u"note" # name passed to template
+    context_object_name = "note"
+    template_name = "notes/note_base.html"
 
-    def get_context_data(self, **kwargs):
-        """ Generate custom context for the page rendering a Note
-            + if pdf, set the `pdf` flag
-        """
-        # not current using these
-        #kwargs['hostname'] = Site.objects.get_current()
+    def get_success_url(self):
+        return self.object.get_absolute_url()
 
-        kwargs['pdf'] = is_pdf(self)
-        kwargs['ppt'] = is_ppt(self)
+    def form_valid(self, form):
+        self.note = self.object
+        # Ensure that the requesting user has permission to edit.
+        if self.note.allows_edit_by(self.request.user):
+            return super(NoteView, self).form_valid(form)
+        else:
+            messages.error(self.request, 'Permission denied.')
+            return HttpResponseRedirect(self.get_success_url())
 
-        if self.object.mimetype in PDF_MIMETYPES:
-            kwargs['pdf_controls'] = True
+    def get_context_data(self, **kwargs):
+        context = super(NoteView, self).get_context_data(**kwargs)
+        context['show_note_container'] = True
+        context['pdf_controls'] = self.object.is_pdf()
+        u = self.request.user
+        context['already_thanked'] = (
+            u.is_authenticated() and 
+            u.get_profile().thanked_notes.filter(pk=self.object.pk).exists()
+        )
+        context['already_flagged'] = (
+            u.is_authenticated() and
+            u.get_profile().flagged_notes.filter(pk=self.object.pk).exists()
+        )
+        context['note_delete_form'] = NoteDeleteForm(initial={'note': self.object.id})
+        context['note_edit_form'] = context.get('form')
+        return context
+
+    def get_initial(self, **kwargs):
+        initial = super(NoteView, self).get_initial()
+        try:
+            initial["html"] = self.object.notemarkdown.html
+        except NoteMarkdown.DoesNotExist:
+            pass
+        initial["tags"] = ",".join([unicode(tag) for tag in self.object.tags.all()])
+        return initial
+
+
+class NoteDeleteView(FormView):
+    form_class = NoteDeleteForm
 
-        if self.request.session.get(format_session_increment_field(self.object.id, THANKS_FIELD), False):
-            kwargs['already_thanked'] = True
+    def form_valid(self, form):
+        self.note = Note.objects.get(id=form.cleaned_data['note'])
+        # Ensure that the requesting user has permission to delete.
+        if self.note.allows_delete_by(self.request.user):
+            self.note.is_hidden = True
+            self.note.save()
+            messages.success(self.request, 'The note "{0}" was deleted successfully.'.format(self.note.name))
+        else:
+            messages.error(self.request, 'Permission denied.')
 
-        if self.request.session.get(format_session_increment_field(self.object.id, FLAG_FIELD), False):
-            kwargs['already_flagged'] = True
+        return super(FormView, self).form_valid(form)
 
-        return super(NoteDetailView, self).get_context_data(**kwargs)
+    def get_success_url(self):
+        return self.note.course.get_absolute_url()
 
 
-class NoteSaveView(FormView, SingleObjectMixin):
-    """ Save a Note and then view the page, 
-        behaves the same as NoteDetailView, except for saving the
-        NoteForm ModelForm
-    """
-    form_class = NoteForm
+class NoteKeywordsView(FormView, SingleObjectMixin):
+    """ Class-based view for the note html page """
     model = Note
-    template_name = 'notes/note_detail.html'
-
-    def get_context_data(self, **kwargs):
-        context = {
-            'object': self.get_object(),
-        }
-        print "get context for NoteSaveView"
-        return super(NoteSaveView, self).get_context_data(**context)
+    context_object_name = u"note" # name passed to template
+    form_class = formset_factory(KeywordForm)
+    template_name = 'notes/note_base.html'
 
-    def get_success_url(self):
-        """ On form submission success, redirect to what url """
-        #TODO: redirect to note slug if possible (auto-slugify)
-        return u'/{school_slug}/{course_slug}?url=/{school_slug}/{course_slug}/{pk}&name={name}&thankyou'.format(
-                school_slug=self.object.course.school.slug,
-                course_slug=self.object.course.slug,
-                pk=self.object.pk,
-                name=self.object.name
-            )
+    def get_object(self, queryset=None):
+        return Note.objects.get(slug=self.kwargs['slug'])
 
-    def form_valid(self, form):
-        """ Actions to take if the submitted form is valid
-            namely, saving the new data to the existing note object
-        """
+    def post(self, request, *args, **kwargs):
         self.object = self.get_object()
-        if len(form.cleaned_data['name'].strip()) > 0:
-            self.object.name = form.cleaned_data['name']
-        self.object.year = form.cleaned_data['year']
-        # use *arg expansion to pass tags a list of tags
-        self.object.tags.add(*form.cleaned_data['tags'])
-        # User has submitted this form, so set the SHOW flag
-        self.object.is_hidden = False
-        self.object.save()
-        return super(NoteSaveView, self).form_valid(form)
-
-    def form_invalid(self, form):
-        """ Do stuff when the form is invalid !!! TODO """
-        # TODO: implement def form_invalid for returning a form with input and error
-        print "running form_invalid"
-        print form
-        print form.errors
-
-
-class NoteView(View):
-    """ Notes superclass that wraps http methods """
+        if not self.request.user.is_authenticated():
+            raise ValidationError("Only authenticated users may set keywords.")
+
+        formset = self.form_class(request.POST)
+        if formset.is_valid():
+            self.keyword_form_valid(formset)
+            self.keyword_formset = self.form_class(initial=self.get_initial_keywords())
+            return super(NoteKeywordsView, self).get(request, *args, **kwargs)
+        else:
+            self.keyword_formset = formset
+            return super(NoteKeywordsView, self).get(request, *args, **kwargs)
 
     def get(self, request, *args, **kwargs):
-        view = NoteDetailView.as_view()
-        return view(request, *args, **kwargs)
-
-    def post(self, request, *args, **kwargs):
-        view = NoteSaveView.as_view()
-        return view(request, *args, **kwargs)
-
-
-class RawNoteDetailView(DetailView):
-    """ Class-based view for the raw note html for iframes """
-    template_name = u'notes/note_raw.html'
-    context_object_name = u"note"
-    model = Note
-
+        self.object = self.get_object()
+        self.keyword_formset = self.form_class(initial=self.get_initial_keywords())
+        return super(NoteKeywordsView, self).get(request, *args, **kwargs)
 
-class PDFView(DetailView):
-    """ Render PDF files in an iframe based on ID"""
-    template_name = u'partial/pdfembed.html'
-    model = Note
+    def get_context_data(self, **kwargs):
+        kwargs['keyword_prototype_form'] = KeywordForm
+        kwargs['keyword_formset'] = self.keyword_formset
+        kwargs['keywords'] = Keyword.objects.filter(note=self.get_object())
+        kwargs['show_keywords'] = True
+
+        ret = note_page_context_helper(self.get_object(), self.request, kwargs)
+        # check for errors returned by the helper.
+        if ret:
+            return ret
+
+        return super(NoteKeywordsView, self).get_context_data(**kwargs)
+
+    def get_initial_keywords(self):
+        existing_keywords = self.get_object().keyword_set.order_by('id')
+        initial_data = [{'keyword': keyword.word, 'definition': keyword.definition, 'id': keyword.pk}
+                        for keyword in existing_keywords]
+        return initial_data
+
+    def keyword_form_valid(self, formset):
+        for form in formset:
+            word = form['keyword'].data
+            definition = form['definition'].data
+            id = form['id'].data
+            # If the user has deleted an existing keyword
+            if not word and not definition and id:
+                try:
+                    keyword_object = Keyword.objects.get(id=id)
+                    keyword_object.delete()
+                except (ValueError, ObjectDoesNotExist):
+                    pass
+
+            # otherwise get or create a keyword
+            elif word or definition:
+                try:
+                    keyword_object = Keyword.objects.get(id=id)
+                except (ValueError, ObjectDoesNotExist):
+                    keyword_object = Keyword()
+                    NoteKarmaEvent.create_event(self.request.user, self.get_object(), NoteKarmaEvent.CREATED_KEYWORD)
+
+                keyword_object.note = self.get_object()
+                keyword_object.word = word
+                keyword_object.definition = definition
+                keyword_object.unreviewed = False
+                keyword_object.save()
+
+
+class NoteQuizView(TemplateView):
+    template_name = 'notes/note_base.html'
 
     def get_context_data(self, **kwargs):
-        """ Generate a path to the pdf file associated with this note
-            by generating a path to the MEDIA_URL by hand """
+        note = Note.objects.get(slug=self.kwargs['slug'])
+
+        ret = note_page_context_helper(note, self.request, kwargs)
+        # check for errors returned by the helper.
+        if ret:
+            return ret
 
-        if is_ppt(self):
-            kwargs['pdf_path'] = "{0}{1}".format(settings.MEDIA_URL,
-                os.path.basename(self.object.pdf_file.name))
-        elif is_pdf(self):
-            kwargs['pdf_path'] = self.object.fp_file
-            #kwargs['pdf_path'] = "{0}{1}".format(settings.MEDIA_URL,
-            #    os.path.basename(self.object.note_file.name))
+        kwargs['note'] = note
+        kwargs['questions'] = quiz_from_keywords(note)
+        kwargs['show_quiz'] = True
 
-        return super(PDFView, self).get_context_data(**kwargs)
+        return super(NoteQuizView, self).get_context_data(**kwargs)
 
 
 class NoteSearchView(ListView):
@@ -169,15 +253,23 @@ class NoteSearchView(ListView):
         else:
             page = 0
 
-        index = SearchIndex()
+        try:
+            index = SearchIndex()
 
-        if 'course_id' in self.request.GET:
-            raw_results = index.search(self.request.GET['query'],
-                                              self.request.GET['course_id'],
-                                              page=page)
+            if 'course_id' in self.request.GET:
+                raw_results = index.search(self.request.GET['query'],
+                                                  self.request.GET['course_id'],
+                                                  page=page)
+            else:
+                raw_results = index.search(self.request.GET['query'],
+                                            page=page)
+
+        except Exception:
+            logger.error("Error with IndexDen:\n" + traceback.format_exc())
+            self.error = True
+            return Note.objects.none()
         else:
-            raw_results = index.search(self.request.GET['query'],
-                                        page=page)
+            self.error = False
 
         instances = Note.objects.in_bulk(raw_results.ordered_ids)
         results = []
@@ -195,6 +287,10 @@ class NoteSearchView(ListView):
         if 'course_id' in self.request.GET:
             kwargs['course'] = Course.objects.get(id=self.request.GET['course_id'])
 
+        if self.error:
+            kwargs['error'] = True
+            return super(NoteSearchView, self).get_context_data(**kwargs)
+
         # If query returned more search results than could
         # fit on one page, show "Next" button
         if self.has_more:
@@ -214,34 +310,67 @@ class NoteSearchView(ListView):
         return super(NoteSearchView, self).get_context_data(**kwargs)
 
 
-def ajaxIncrementBase(request, pk, field):
-    """Increment a note's field by one."""
-    if not (request.method == 'POST' and request.is_ajax()):
-        # return that the api call failed
-        return HttpResponseBadRequest(json.dumps({'status': 'fail', 'message': 'must be a POST ajax request'}),
-                                    mimetype="application/json")
+def process_note_thank_events(request_user, note):
+    # Give points to the person who uploaded this note
+    if note.user != request_user and note.user:
+        NoteKarmaEvent.create_event(note.user, note, NoteKarmaEvent.THANKS)
 
-    try:
-        # Increment counter
-        note = Note.objects.get(pk=pk)
-        note.__dict__[field] += 1
-        note.save()
+    # If note thanks exceeds a threshold, create a Mechanical
+    # Turk task to get some keywords for it
+    if note.thanks == KEYWORD_MTURK_THRESHOLD:
+        submit_extract_keywords_hit.delay(note)
 
-        # Record that user has performed this, to prevent
-        # them from doing it again
-        request.session[format_session_increment_field(pk, field)] = True
-    except ObjectDoesNotExist:
-        return HttpResponseNotFound(json.dumps({'status': 'fail', 'message': 'note id does not match a note'}),
-                                    mimetype="application/json")
-
-    return HttpResponse(status=204)
 
 def thank_note(request, pk):
     """Record that somebody has thanked a note."""
-    return ajaxIncrementBase(request, pk, THANKS_FIELD)
+    return ajax_increment(Note, request, pk, THANKS_FIELD, USER_PROFILE_THANKS_FIELD, process_note_thank_events)
+
+
+def process_note_flag_events(request_user, note):
+    # Take a point away from person flagging this note
+    if request_user.is_authenticated():
+        NoteKarmaEvent.create_event(request_user, note, NoteKarmaEvent.GIVE_FLAG)
+    # If this is the 6th time this note has been flagged,
+    # punish the uploader
+    if note.flags == 6 and note.user:
+        NoteKarmaEvent.create_event(note.user, note, NoteKarmaEvent.GET_FLAGGED)
+
 
 def flag_note(request, pk):
     """Record that somebody has flagged a note."""
-    return ajaxIncrementBase(request, pk, FLAG_FIELD)
+    return ajax_increment(Note, request, pk, FLAG_FIELD, USER_PROFILE_FLAGS_FIELD, process_note_flag_events)
+
+
+def process_downloaded_note(request_user, note):
+    """Record that somebody has downloaded a note"""
+    if request_user.is_authenticated() and request_user != note.user:
+        NoteKarmaEvent.create_event(request_user, note, NoteKarmaEvent.DOWNLOADED_NOTE)
+    if request_user.is_authenticated() and note.user and note.user != request_user:
+        NoteKarmaEvent.create_event(note.user, note, NoteKarmaEvent.HAD_NOTE_DOWNLOADED)
+
+
+def downloaded_note(request, pk):
+    """Record that somebody has flagged a note."""
+    return ajax_pk_base(Note, request, pk, process_downloaded_note)
 
 
+def edit_note_tags(request, pk):
+    """
+    Saves the posted string of tags
+    """
+    note = Note.objects.get(pk=pk)
+    if request.method == "POST" and request.is_ajax() and note.allows_tags_by(request.user):
+        note.tags.set(request.body)
+
+        note_json = serializers.serialize('json', [note,])
+        resp = json.loads(note_json)[0]
+        resp['fields']['tags'] = list(note.tags.names())
+
+        return HttpResponse(json.dumps(resp), mimetype="application/json")
+    if request.method != "POST" or not request.is_ajax():
+        return HttpResponseBadRequest(json.dumps({'status': 'fail', 'message': 'Invalid request'}),
+                                      mimetype="application/json")
+    else:
+        return HttpResponseForbidden(json.dumps({'status': 'fail', 'message': 'Not permitted'}),
+                                      mimetype="application/json")
+