ecdf9b48266fae549a834632e59bf93a915fd5bd
[oweals/karmaworld.git] / karmaworld / apps / courses / views.py
1 #!/usr/bin/env python
2 # -*- coding:utf8 -*-
3 # Copyright (C) 2012  FinalsClub Foundation
4 """ Views for the KarmaNotes Courses app """
5 import calendar
6 from time import strftime
7 from django.db.models import Q
8 from django.utils.html import escape
9
10 from querystring_parser import parser as querystring_parser
11 import json
12 from django.conf import settings
13 from django.core import serializers
14 from django.core.exceptions import MultipleObjectsReturned
15 from django.core.exceptions import ObjectDoesNotExist
16
17 from django.http import HttpResponse, HttpResponseBadRequest
18 from django.views.decorators.cache import cache_page
19 from django.views.generic import View
20 from django.views.generic import DetailView
21 from django.views.generic import TemplateView
22 from django.views.generic.list import ListView
23 from django.views.generic.edit import CreateView
24
25 from karmaworld.apps.courses.models import Course
26 from karmaworld.apps.courses.models import School
27 from karmaworld.apps.courses.forms import CourseForm
28 from karmaworld.apps.notes.models import Note
29 from karmaworld.apps.users.models import CourseKarmaEvent
30 from karmaworld.apps.notes.forms import FileUploadForm
31 from karmaworld.utils import ajax_increment, format_session_increment_field, ajax_base
32 from django.contrib import messages
33
34 FLAG_FIELD = 'flags'
35 USER_PROFILE_FLAGS_FIELD = 'flagged_courses'
36
37
38 # https://docs.djangoproject.com/en/1.5/topics/class-based-views/mixins/#an-alternative-better-solution
39 class CourseListView(View):
40     """
41     Composite view to list all courses and processes new course additions.
42     """
43
44     def get(self, request, *args, **kwargs):
45         return CourseListSubView.as_view()(request, *args, **kwargs)
46
47     def post(self, request, *args, **kwargs):
48         ret = CourseAddFormView.as_view()(request, *args, **kwargs)
49         # Check to see if the form came back with errors.
50         if hasattr(ret, 'context_data') and \
51            ret.context_data.has_key('form') and \
52            not ret.context_data['form'].is_valid():
53             # Invalid form. Render as if by get(), but replace the form.
54             badform = ret.context_data['form']
55             request.method = 'GET' # trick get() into returning something
56             ret = self.get(request, *args, **kwargs)
57             # Replace blank form with invalid form.
58             ret.context_data['course_form'] = badform
59             ret.context_data['jump_to_form'] = True
60         else:
61             messages.add_message(request, messages.SUCCESS, 'You\'ve just created this course. Nice!')
62         return ret
63
64
65 class CourseListSubView(ListView):
66     """ Lists all courses. Called by CourseListView. """
67     model = Course
68
69     def get_queryset(self):
70         return Course.objects.all().select_related('note_set', 'school', 'department', 'department__school')
71
72     def get_context_data(self, **kwargs):
73         """ Add the CourseForm to ListView context """
74         # get the original context
75         context = super(CourseListSubView, self).get_context_data(**kwargs)
76         # get the total number of notes
77         context['note_count'] = Note.objects.count()
78         # get the course form for the form at the bottom of the homepage
79         context['course_form'] = CourseForm()
80
81         schools = set()
82         for course in self.object_list:
83             if course.school:
84                 schools.add(course.school)
85             elif course.department and course.department.school:
86                 schools.add(course.department.school)
87
88         context['schools'] = sorted(list(schools), key=lambda x: x.name)
89
90         # Include settings constants for honeypot
91         for key in ('HONEYPOT_FIELD_NAME', 'HONEYPOT_VALUE'):
92             context[key] = getattr(settings, key)
93
94         return context
95
96
97 class CourseAddFormView(CreateView):
98     """ Processes new course additions. Called by CourseListView. """
99     model = Course
100     form_class = CourseForm
101
102     def get_template_names(self):
103         """ template_name must point back to CourseListView url """
104         # TODO clean this up. "_list" template might come from ListView above.
105         return ['courses/course_list.html',]
106
107
108 class CourseDetailView(DetailView):
109     """ Class-based view for the course html page """
110     model = Course
111     context_object_name = u"course" # name passed to template
112
113     def get_context_data(self, **kwargs):
114         """ filter the Course.note_set to return no Drafts """
115         kwargs = super(CourseDetailView, self).get_context_data()
116         kwargs['note_set'] = self.object.note_set.filter(is_hidden=False)
117
118         # For the Filepicker Partial template
119         kwargs['file_upload_form'] = FileUploadForm()
120         kwargs['note_categories'] = Note.NOTE_CATEGORIES
121
122         if self.request.user.is_authenticated():
123             try:
124                 self.request.user.get_profile().flagged_courses.get(pk=self.object.pk)
125                 kwargs['already_flagged'] = True
126             except ObjectDoesNotExist:
127                 pass
128
129         return kwargs
130
131
132 class AboutView(TemplateView):
133     """ Display the About page with the Schools leaderboard """
134     template_name = "about.html"
135
136     def get_context_data(self, **kwargs):
137         """ get the list of schools with the most files for leaderboard """
138         if 'schools' not in kwargs:
139             kwargs['schools'] = School.objects.filter(file_count__gt=0).order_by('-file_count')[:20]
140         return kwargs
141
142
143 def school_list(request):
144     """ Return JSON describing Schools that match q query on name """
145     if not (request.method == 'POST' and request.is_ajax()
146                         and request.POST.has_key('q')):
147         #return that the api call failed
148         return HttpResponseBadRequest(json.dumps({'status':'fail'}), mimetype="application/json")
149
150     # if an ajax get request with a 'q' name query
151     # get the schools as a id name dict,
152     _query = request.POST['q']
153     matching_school_aliases = list(School.objects.filter(alias__icontains=_query))
154     matching_school_names = sorted(list(School.objects.filter(name__icontains=_query)[:20]),key=lambda o:len(o.name))
155     _schools = matching_school_aliases[:2] + matching_school_names
156     schools = [{'id': s.id, 'name': s.name} for s in _schools]
157
158     # return as json
159     return HttpResponse(json.dumps({'status':'success', 'schools': schools}), mimetype="application/json")
160
161
162 def school_course_list(request):
163     """Return JSON describing courses we know of at the given school
164      that match the query """
165     if not (request.method == 'POST' and request.is_ajax()
166                         and request.POST.has_key('q')
167                         and request.POST.has_key('school_id')):
168         # return that the api call failed
169         return HttpResponseBadRequest(json.dumps({'status': 'fail', 'message': 'query parameters missing'}),
170                                     mimetype="application/json")
171
172     _query = request.POST['q']
173     try:
174       _school_id = int(request.POST['school_id'])
175     except:
176       return HttpResponseBadRequest(json.dumps({'status': 'fail',
177                                               'message': 'could not convert school id to integer'}),
178                                   mimetype="application/json")
179
180     # Look up the school
181     try:
182         school = School.objects.get(id__exact=_school_id)
183     except (MultipleObjectsReturned, ObjectDoesNotExist):
184         return HttpResponseBadRequest(json.dumps({'status': 'fail',
185                                                 'message': 'school id did not match exactly one school'}),
186                                     mimetype="application/json")
187
188     # Look up matching courses
189     _courses = Course.objects.filter(school__exact=school.id, name__icontains=_query)
190     courses = [{'name': c.name} for c in _courses]
191
192     # return as json
193     return HttpResponse(json.dumps({'status':'success', 'courses': courses}),
194                         mimetype="application/json")
195
196
197 def school_course_instructor_list(request):
198     """Return JSON describing instructors we know of at the given school
199        teaching the given course
200        that match the query """
201     if not(request.method == 'POST' and request.is_ajax()
202                         and request.POST.has_key('q')
203                         and request.POST.has_key('course_name')
204                         and request.POST.has_key('school_id')):
205         # return that the api call failed
206         return HttpResponseBadRequest(json.dumps({'status': 'fail', 'message': 'query parameters missing'}),
207                                     mimetype="application/json")
208
209     _query = request.POST['q']
210     _course_name = request.POST['course_name']
211     try:
212       _school_id = int(request.POST['school_id'])
213     except:
214       return HttpResponseBadRequest(json.dumps({'status': 'fail',
215                                               'message':'could not convert school id to integer'}),
216                                   mimetype="application/json")
217
218     # Look up the school
219     try:
220         school = School.objects.get(id__exact=_school_id)
221     except (MultipleObjectsReturned, ObjectDoesNotExist):
222         return HttpResponseBadRequest(json.dumps({'status': 'fail',
223                                                   'message': 'school id did not match exactly one school'}),
224                                     mimetype="application/json")
225
226     # Look up matching courses
227     _courses = Course.objects.filter(school__exact=school.id,
228                                      name__exact=_course_name,
229                                      instructor_name__icontains=_query)
230     instructors = [{'name': c.instructor_name, 'url': c.get_absolute_url()} for c in _courses]
231
232     # return as json
233     return HttpResponse(json.dumps({'status':'success', 'instructors': instructors}),
234                         mimetype="application/json")
235
236
237 def process_course_flag_events(request_user, course):
238     # Take a point away from person flagging this course
239     if request_user.is_authenticated():
240         CourseKarmaEvent.create_event(request_user, course, CourseKarmaEvent.GIVE_FLAG)
241
242
243 def flag_course(request, pk):
244     """Record that somebody has flagged a note."""
245     return ajax_increment(Course, request, pk, FLAG_FIELD, USER_PROFILE_FLAGS_FIELD, process_course_flag_events)
246
247
248 def course_json(course):
249     course_data = {
250         'school': course.school.name if course.school else course.department.school.name,
251         'department': course.department.name if course.department else None,
252         'instructor': course.instructor_name if course.instructor_name else ', '.join([p.name for p in course.professor.all()]),
253         'name': course.name,
254         'link': course.get_absolute_url(),
255         'file_count': course.file_count,
256         'popularity': course.thank_count,
257         'updated_at': strftime('%B %d, %Y', course.updated_at.utctimetuple())
258     }
259
260     # Prevent XSS attacks
261     for k in course_data:
262         course_data[k] = escape(course_data[k])
263
264     return course_data
265
266
267 def course_list_ajax_handler(request):
268     request_dict = querystring_parser.parse(request.GET.urlencode())
269     draw = int(request_dict['draw'])
270     start = request_dict['start']
271     length = request_dict['length']
272     search = request_dict.get('search', None)
273
274     objects = Course.objects.all()
275
276     if search and search['value']:
277         objects = objects.filter(Q(name__icontains=search['value']) |
278                                  Q(school__name__icontains=search['value']) |
279                                  Q(department__school__name__icontains=search['value']))
280
281     order_fields = []
282     for order_index in request_dict['order']:
283         order_field = None
284         order = request_dict['order'][order_index]
285         if order['column'] == 1:
286             order_field = 'updated_at'
287         elif order['column'] == 2:
288             order_field = 'file_count'
289         elif order['column'] == 3:
290             order_field = 'thank_count'
291
292         if order['dir'] == 'desc':
293             order_field = '-' + order_field
294
295         if order_field:
296             order_fields.append(order_field)
297
298     objects = objects.order_by(*order_fields)
299
300     displayRecords = objects.count()
301
302     if start > 0:
303         objects = objects[start:]
304
305     objects = objects[:length]
306
307     row_data = [
308         [
309             course_json(course),
310             calendar.timegm(course.updated_at.timetuple()),
311             course.file_count,
312             course.thank_count,
313             course.school.name if course.school else course.department.school.name,
314         ] for course in objects
315     ]
316
317     response_dict = {
318         'draw': draw,
319         'recordsTotal': Course.objects.count(),
320         'recordsFiltered': displayRecords,
321         'data': row_data
322     }
323
324     return HttpResponse(json.dumps(response_dict), mimetype='application/json')
325
326
327 def course_list_ajax(request):
328     return ajax_base(request, course_list_ajax_handler, ['GET'])
329
330
331 def edit_course(request, pk):
332     """
333     Saves the edited course content
334     """
335     if request.method == "POST" and request.is_ajax():
336         course = Course.objects.get(pk=pk)
337         original_name = course.name
338         course_form = CourseForm(request.POST or None, instance=course)
339
340         if course_form.is_valid():
341             course_form.save()
342
343             course_json = serializers.serialize('json', [course,])
344             resp = json.loads(course_json)[0]
345
346             if (course.name != original_name):
347                 course.set_slug()
348                 resp['fields']['new_url'] = course.get_absolute_url()
349
350             return HttpResponse(json.dumps(resp), mimetype="application/json")
351         else:
352             return HttpResponseBadRequest(json.dumps({'status': 'fail', 'message': 'Validation error',
353                                           'errors': course_form.errors}),
354                                           mimetype="application/json")
355     else:
356         return HttpResponseBadRequest(json.dumps({'status': 'fail', 'message': 'Invalid request'}),
357                                       mimetype="application/json")