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