import ObjectDoesNotExist closes #427
[oweals/karmaworld.git] / karmaworld / apps / notes / views.py
1 #!/usr/bin/env python
2 # -*- coding:utf8 -*-
3 # Copyright (C) 2012  FinalsClub Foundation
4
5 import json
6 import logging
7 import traceback
8
9 from django.contrib import messages
10
11 from django.core import serializers
12 from django.core.exceptions import ValidationError
13 from django.core.exceptions import ObjectDoesNotExist
14 from django.forms.formsets import formset_factory
15
16 from django.http import HttpResponse
17 from django.http import HttpResponseRedirect
18 from django.http import HttpResponseForbidden
19 from django.http import HttpResponseBadRequest
20 from django.views.generic import DetailView, ListView, TemplateView
21 from django.views.generic import UpdateView, FormView
22 from django.views.generic import View
23 from django.views.generic.detail import SingleObjectMixin
24
25 from karmaworld.apps.notes.forms import NoteForm
26 from karmaworld.apps.notes.forms import NoteDeleteForm
27 from karmaworld.apps.notes.models import Note
28 from karmaworld.apps.notes.models import NoteMarkdown
29 from karmaworld.apps.notes.models import KEYWORD_MTURK_THRESHOLD
30 from karmaworld.apps.notes.search import SearchIndex
31 from karmaworld.apps.users.models import NoteKarmaEvent
32 from karmaworld.apps.courses.forms import CourseForm
33 from karmaworld.apps.quizzes.forms import KeywordForm
34 from karmaworld.apps.quizzes.tasks import submit_extract_keywords_hit
35 from karmaworld.apps.quizzes.tasks import get_extract_keywords_results
36 from karmaworld.apps.courses.models import Course
37 from karmaworld.apps.quizzes.models import Keyword
38 from karmaworld.apps.quizzes.create_quiz import quiz_from_keywords
39
40 from karmaworld.utils.ajax_utils import ajax_pk_base
41 from karmaworld.utils.ajax_utils import ajax_increment
42
43
44 logger = logging.getLogger(__name__)
45
46 THANKS_FIELD = 'thanks'
47 USER_PROFILE_THANKS_FIELD = 'thanked_notes'
48 FLAG_FIELD = 'flags'
49 USER_PROFILE_FLAGS_FIELD = 'flagged_notes'
50
51 def note_page_context_helper(note, request, context):
52
53     if request.method == 'POST':
54         if not note.allows_edit_by(request.user):
55             # This user is Balrog. It. Shall. Not. Pass.
56             return HttpResponseForbidden()
57         # Only save tags if not forbidden above.
58         context['note_edit_form'] = NoteForm(request.POST)
59     else:
60         tags_string = ','.join([str(tag) for tag in note.tags.all()])
61         initial = {"name": note.name, "tags": tags_string}
62         try:
63             initial["html"] = note.notemarkdown.html
64         except NoteMarkdown.DoesNotExist:
65             pass
66         context['note_edit_form'] = NoteForm(initial=initial)
67
68     context['note_delete_form'] = NoteDeleteForm(initial={'note': note.id})
69
70     if note.is_pdf():
71         context['pdf_controls'] = True
72
73     if request.user.is_authenticated():
74         try:
75             request.user.get_profile().thanked_notes.get(pk=note.pk)
76             context['already_thanked'] = True
77         except ObjectDoesNotExist:
78             pass
79
80         try:
81             request.user.get_profile().flagged_notes.get(pk=note.pk)
82             context['already_flagged'] = True
83         except ObjectDoesNotExist:
84             pass
85
86 class NoteView(UpdateView):
87     form_class = NoteForm
88     model = Note
89     context_object_name = "note"
90     template_name = "notes/note_base.html"
91
92     def get_success_url(self):
93         return self.object.get_absolute_url()
94
95     def form_valid(self, form):
96         self.note = self.object
97         # Ensure that the requesting user has permission to edit.
98         if self.note.allows_edit_by(self.request.user):
99             return super(NoteView, self).form_valid(form)
100         else:
101             messages.error(self.request, 'Permission denied.')
102             return HttpResponseRedirect(self.get_success_url())
103
104     def get_context_data(self, **kwargs):
105         context = super(NoteView, self).get_context_data(**kwargs)
106         context['show_note_container'] = True
107         context['pdf_controls'] = self.object.is_pdf()
108         u = self.request.user
109         context['already_thanked'] = (
110             u.is_authenticated() and 
111             u.get_profile().thanked_notes.filter(pk=self.object.pk).exists()
112         )
113         context['already_flagged'] = (
114             u.is_authenticated() and
115             u.get_profile().flagged_notes.filter(pk=self.object.pk).exists()
116         )
117         context['note_delete_form'] = NoteDeleteForm(initial={'note': self.object.id})
118         context['note_edit_form'] = context.get('form')
119         return context
120
121     def get_initial(self, **kwargs):
122         initial = super(NoteView, self).get_initial()
123         try:
124             initial["html"] = self.object.notemarkdown.html
125         except NoteMarkdown.DoesNotExist:
126             pass
127         initial["tags"] = ",".join([unicode(tag) for tag in self.object.tags.all()])
128         return initial
129
130
131 class NoteDeleteView(FormView):
132     form_class = NoteDeleteForm
133
134     def form_valid(self, form):
135         self.note = Note.objects.get(id=form.cleaned_data['note'])
136         # Ensure that the requesting user has permission to delete.
137         if self.note.allows_delete_by(self.request.user):
138             self.note.is_hidden = True
139             self.note.save()
140             messages.success(self.request, 'The note "{0}" was deleted successfully.'.format(self.note.name))
141         else:
142             messages.error(self.request, 'Permission denied.')
143
144         return super(FormView, self).form_valid(form)
145
146     def get_success_url(self):
147         return self.note.course.get_absolute_url()
148
149
150 class NoteKeywordsView(FormView, SingleObjectMixin):
151     """ Class-based view for the note html page """
152     model = Note
153     context_object_name = u"note" # name passed to template
154     form_class = formset_factory(KeywordForm)
155     template_name = 'notes/note_base.html'
156
157     def get_object(self, queryset=None):
158         return Note.objects.get(slug=self.kwargs['slug'])
159
160     def post(self, request, *args, **kwargs):
161         self.object = self.get_object()
162         if not self.request.user.is_authenticated():
163             raise ValidationError("Only authenticated users may set keywords.")
164
165         formset = self.form_class(request.POST)
166         if formset.is_valid():
167             self.keyword_form_valid(formset)
168             self.keyword_formset = self.form_class(initial=self.get_initial_keywords())
169             return super(NoteKeywordsView, self).get(request, *args, **kwargs)
170         else:
171             self.keyword_formset = formset
172             return super(NoteKeywordsView, self).get(request, *args, **kwargs)
173
174     def get(self, request, *args, **kwargs):
175         self.object = self.get_object()
176         self.keyword_formset = self.form_class(initial=self.get_initial_keywords())
177         return super(NoteKeywordsView, self).get(request, *args, **kwargs)
178
179     def get_context_data(self, **kwargs):
180         kwargs['keyword_prototype_form'] = KeywordForm
181         kwargs['keyword_formset'] = self.keyword_formset
182         kwargs['keywords'] = Keyword.objects.filter(note=self.get_object())
183         kwargs['show_keywords'] = True
184
185         ret = note_page_context_helper(self.get_object(), self.request, kwargs)
186         # check for errors returned by the helper.
187         if ret:
188             return ret
189
190         return super(NoteKeywordsView, self).get_context_data(**kwargs)
191
192     def get_initial_keywords(self):
193         existing_keywords = self.get_object().keyword_set.order_by('id')
194         initial_data = [{'keyword': keyword.word, 'definition': keyword.definition, 'id': keyword.pk}
195                         for keyword in existing_keywords]
196         return initial_data
197
198     def keyword_form_valid(self, formset):
199         for form in formset:
200             word = form['keyword'].data
201             definition = form['definition'].data
202             id = form['id'].data
203             # If the user has deleted an existing keyword
204             if not word and not definition and id:
205                 try:
206                     keyword_object = Keyword.objects.get(id=id)
207                     keyword_object.delete()
208                 except (ValueError, ObjectDoesNotExist):
209                     pass
210
211             # otherwise get or create a keyword
212             elif word or definition:
213                 try:
214                     keyword_object = Keyword.objects.get(id=id)
215                 except (ValueError, ObjectDoesNotExist):
216                     keyword_object = Keyword()
217                     NoteKarmaEvent.create_event(self.request.user, self.get_object(), NoteKarmaEvent.CREATED_KEYWORD)
218
219                 keyword_object.note = self.get_object()
220                 keyword_object.word = word
221                 keyword_object.definition = definition
222                 keyword_object.unreviewed = False
223                 keyword_object.save()
224
225
226 class NoteQuizView(TemplateView):
227     template_name = 'notes/note_base.html'
228
229     def get_context_data(self, **kwargs):
230         note = Note.objects.get(slug=self.kwargs['slug'])
231
232         ret = note_page_context_helper(note, self.request, kwargs)
233         # check for errors returned by the helper.
234         if ret:
235             return ret
236
237         kwargs['note'] = note
238         kwargs['questions'] = quiz_from_keywords(note)
239         kwargs['show_quiz'] = True
240
241         return super(NoteQuizView, self).get_context_data(**kwargs)
242
243
244 class NoteSearchView(ListView):
245     template_name = 'notes/search_results.html'
246
247     def get_queryset(self):
248         if not 'query' in self.request.GET:
249             return Note.objects.none()
250
251         if 'page' in self.request.GET:
252             page = int(self.request.GET['page'])
253         else:
254             page = 0
255
256         try:
257             index = SearchIndex()
258
259             if 'course_id' in self.request.GET:
260                 raw_results = index.search(self.request.GET['query'],
261                                                   self.request.GET['course_id'],
262                                                   page=page)
263             else:
264                 raw_results = index.search(self.request.GET['query'],
265                                             page=page)
266
267         except Exception:
268             logger.error("Error with IndexDen:\n" + traceback.format_exc())
269             self.error = True
270             return Note.objects.none()
271         else:
272             self.error = False
273
274         instances = Note.objects.in_bulk(raw_results.ordered_ids)
275         results = []
276         for id in raw_results.ordered_ids:
277             if id in instances:
278                 results.append((instances[id], raw_results.snippet_dict[id]))
279         self.has_more = raw_results.has_more
280
281         return results
282
283     def get_context_data(self, **kwargs):
284         if 'query' in self.request.GET:
285             kwargs['query'] = self.request.GET['query']
286
287         if 'course_id' in self.request.GET:
288             kwargs['course'] = Course.objects.get(id=self.request.GET['course_id'])
289
290         if self.error:
291             kwargs['error'] = True
292             return super(NoteSearchView, self).get_context_data(**kwargs)
293
294         # If query returned more search results than could
295         # fit on one page, show "Next" button
296         if self.has_more:
297             kwargs['has_next'] = True
298             if 'page' in self.request.GET:
299                 kwargs['next_page'] = int(self.request.GET['page']) + 1
300             else:
301                 kwargs['next_page'] = 1
302
303         # If the user is looking at a search result page
304         # that isn't the first one, show "Prev" button
305         if 'page' in self.request.GET and \
306             int(self.request.GET['page']) > 0:
307             kwargs['has_prev'] = True
308             kwargs['prev_page'] = int(self.request.GET['page']) - 1
309
310         return super(NoteSearchView, self).get_context_data(**kwargs)
311
312
313 def process_note_thank_events(request_user, note):
314     # Give points to the person who uploaded this note
315     if note.user != request_user and note.user:
316         NoteKarmaEvent.create_event(note.user, note, NoteKarmaEvent.THANKS)
317
318     # If note thanks exceeds a threshold, create a Mechanical
319     # Turk task to get some keywords for it
320     if note.thanks == KEYWORD_MTURK_THRESHOLD:
321         submit_extract_keywords_hit.delay(note)
322
323
324 def thank_note(request, pk):
325     """Record that somebody has thanked a note."""
326     return ajax_increment(Note, request, pk, THANKS_FIELD, USER_PROFILE_THANKS_FIELD, process_note_thank_events)
327
328
329 def process_note_flag_events(request_user, note):
330     # Take a point away from person flagging this note
331     if request_user.is_authenticated():
332         NoteKarmaEvent.create_event(request_user, note, NoteKarmaEvent.GIVE_FLAG)
333     # If this is the 6th time this note has been flagged,
334     # punish the uploader
335     if note.flags == 6 and note.user:
336         NoteKarmaEvent.create_event(note.user, note, NoteKarmaEvent.GET_FLAGGED)
337
338
339 def flag_note(request, pk):
340     """Record that somebody has flagged a note."""
341     return ajax_increment(Note, request, pk, FLAG_FIELD, USER_PROFILE_FLAGS_FIELD, process_note_flag_events)
342
343
344 def process_downloaded_note(request_user, note):
345     """Record that somebody has downloaded a note"""
346     if request_user.is_authenticated() and request_user != note.user:
347         NoteKarmaEvent.create_event(request_user, note, NoteKarmaEvent.DOWNLOADED_NOTE)
348     if request_user.is_authenticated() and note.user and note.user != request_user:
349         NoteKarmaEvent.create_event(note.user, note, NoteKarmaEvent.HAD_NOTE_DOWNLOADED)
350
351
352 def downloaded_note(request, pk):
353     """Record that somebody has flagged a note."""
354     return ajax_pk_base(Note, request, pk, process_downloaded_note)
355
356
357 def edit_note_tags(request, pk):
358     """
359     Saves the posted string of tags
360     """
361     note = Note.objects.get(pk=pk)
362     if request.method == "POST" and request.is_ajax() and note.allows_tags_by(request.user):
363         note.tags.set(request.body)
364
365         note_json = serializers.serialize('json', [note,])
366         resp = json.loads(note_json)[0]
367         resp['fields']['tags'] = list(note.tags.names())
368
369         return HttpResponse(json.dumps(resp), mimetype="application/json")
370     if request.method != "POST" or not request.is_ajax():
371         return HttpResponseBadRequest(json.dumps({'status': 'fail', 'message': 'Invalid request'}),
372                                       mimetype="application/json")
373     else:
374         return HttpResponseForbidden(json.dumps({'status': 'fail', 'message': 'Not permitted'}),
375                                       mimetype="application/json")
376