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