Clean up how we edit notes and courses -- disable course edit for now until we figure...
[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
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 word == '':
210                 continue
211             try:
212                 keyword_object = Keyword.objects.get(id=id)
213             except (ValueError, ObjectDoesNotExist):
214                 keyword_object = Keyword()
215
216             keyword_object.note = self.get_object()
217             keyword_object.word = word
218             keyword_object.definition = definition
219             keyword_object.save()
220
221
222 class NoteQuizView(TemplateView):
223     template_name = 'notes/note_base.html'
224
225     def get_context_data(self, **kwargs):
226         note = Note.objects.get(slug=self.kwargs['slug'])
227
228         note_page_context_helper(note, self.request, kwargs)
229
230         kwargs['note'] = note
231         kwargs['questions'] = quiz_from_keywords(note)
232         kwargs['show_quiz'] = True
233
234         return super(NoteQuizView, self).get_context_data(**kwargs)
235
236
237 class NoteSearchView(ListView):
238     template_name = 'notes/search_results.html'
239
240     def get_queryset(self):
241         if not 'query' in self.request.GET:
242             return Note.objects.none()
243
244         if 'page' in self.request.GET:
245             page = int(self.request.GET['page'])
246         else:
247             page = 0
248
249         try:
250             index = SearchIndex()
251
252             if 'course_id' in self.request.GET:
253                 raw_results = index.search(self.request.GET['query'],
254                                                   self.request.GET['course_id'],
255                                                   page=page)
256             else:
257                 raw_results = index.search(self.request.GET['query'],
258                                             page=page)
259
260         except Exception:
261             logger.error("Error with IndexDen:\n" + traceback.format_exc())
262             self.error = True
263             return Note.objects.none()
264         else:
265             self.error = False
266
267         instances = Note.objects.in_bulk(raw_results.ordered_ids)
268         results = []
269         for id in raw_results.ordered_ids:
270             if id in instances:
271                 results.append((instances[id], raw_results.snippet_dict[id]))
272         self.has_more = raw_results.has_more
273
274         return results
275
276     def get_context_data(self, **kwargs):
277         if 'query' in self.request.GET:
278             kwargs['query'] = self.request.GET['query']
279
280         if 'course_id' in self.request.GET:
281             kwargs['course'] = Course.objects.get(id=self.request.GET['course_id'])
282
283         if self.error:
284             kwargs['error'] = True
285             return super(NoteSearchView, self).get_context_data(**kwargs)
286
287         # If query returned more search results than could
288         # fit on one page, show "Next" button
289         if self.has_more:
290             kwargs['has_next'] = True
291             if 'page' in self.request.GET:
292                 kwargs['next_page'] = int(self.request.GET['page']) + 1
293             else:
294                 kwargs['next_page'] = 1
295
296         # If the user is looking at a search result page
297         # that isn't the first one, show "Prev" button
298         if 'page' in self.request.GET and \
299             int(self.request.GET['page']) > 0:
300             kwargs['has_prev'] = True
301             kwargs['prev_page'] = int(self.request.GET['page']) - 1
302
303         return super(NoteSearchView, self).get_context_data(**kwargs)
304
305
306 def process_note_thank_events(request_user, note):
307     # Give points to the person who uploaded this note
308     if note.user != request_user and note.user:
309         NoteKarmaEvent.create_event(note.user, note, NoteKarmaEvent.THANKS)
310
311     # If note thanks exceeds a threshold, create a Mechanical
312     # Turk task to get some keywords for it
313     if note.thanks == 3:
314         submit_extract_keywords_hit(note)
315
316
317 def thank_note(request, pk):
318     """Record that somebody has thanked a note."""
319     return ajax_increment(Note, request, pk, THANKS_FIELD, USER_PROFILE_THANKS_FIELD, process_note_thank_events)
320
321
322 def process_note_flag_events(request_user, note):
323     # Take a point away from person flagging this note
324     if request_user.is_authenticated():
325         NoteKarmaEvent.create_event(request_user, note, NoteKarmaEvent.GIVE_FLAG)
326     # If this is the 6th time this note has been flagged,
327     # punish the uploader
328     if note.flags == 6 and note.user:
329         NoteKarmaEvent.create_event(note.user, note, NoteKarmaEvent.GET_FLAGGED)
330
331
332 def flag_note(request, pk):
333     """Record that somebody has flagged a note."""
334     return ajax_increment(Note, request, pk, FLAG_FIELD, USER_PROFILE_FLAGS_FIELD, process_note_flag_events)
335
336
337 def process_downloaded_note(request_user, note):
338     """Record that somebody has downloaded a note"""
339     if request_user.is_authenticated() and request_user != note.user:
340         NoteKarmaEvent.create_event(request_user, note, NoteKarmaEvent.DOWNLOADED_NOTE)
341     if request_user.is_authenticated() and note.user:
342         NoteKarmaEvent.create_event(note.user, note, NoteKarmaEvent.HAD_NOTE_DOWNLOADED)
343
344
345 def downloaded_note(request, pk):
346     """Record that somebody has flagged a note."""
347     return ajax_pk_base(Note, request, pk, process_downloaded_note)
348
349
350 def edit_note_tags(request, pk):
351     """
352     Saves the posted string of tags
353     """
354     if request.method == "POST" and request.is_ajax() and request.user.is_authenticated() and request.user.get_profile().can_edit_items():
355         note = Note.objects.get(pk=pk)
356         note.tags.set(request.body)
357
358         note_json = serializers.serialize('json', [note,])
359         resp = json.loads(note_json)[0]
360         resp['fields']['tags'] = list(note.tags.names())
361
362         return HttpResponse(json.dumps(resp), mimetype="application/json")
363     else:
364         return HttpResponseBadRequest(json.dumps({'status': 'fail', 'message': 'Invalid request'}),
365                                       mimetype="application/json")
366