b6d6fc8b5b8817c4cd5967dcad764699785c7cd9
[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
6 import json
7 from django.conf import settings
8 from django.core import serializers
9 from django.core.exceptions import MultipleObjectsReturned
10 from django.core.exceptions import ObjectDoesNotExist
11
12 from django.http import HttpResponse, HttpResponseBadRequest
13 from django.views.decorators.cache import cache_page
14 from django.views.generic import View
15 from django.views.generic import DetailView
16 from django.views.generic import TemplateView
17 from django.views.generic.list import ListView
18 from django.views.generic.edit import CreateView
19
20 from karmaworld.apps.courses.models import Course
21 from karmaworld.apps.courses.models import School
22 from karmaworld.apps.courses.forms import CourseForm
23 from karmaworld.apps.notes.models import Note
24 from karmaworld.apps.users.models import CourseKarmaEvent
25 from karmaworld.apps.notes.forms import FileUploadForm
26 from karmaworld.utils import ajax_increment, format_session_increment_field
27 from django.contrib import messages
28
29 FLAG_FIELD = 'flags'
30 USER_PROFILE_FLAGS_FIELD = 'flagged_courses'
31
32
33 # https://docs.djangoproject.com/en/1.5/topics/class-based-views/mixins/#an-alternative-better-solution
34 class CourseListView(View):
35     """
36     Composite view to list all courses and processes new course additions.
37     """
38
39     def get(self, request, *args, **kwargs):
40         if request.user.is_authenticated():
41             return CourseListSubView.as_view()(request, *args, **kwargs)
42         # Cache the homepage for non-authenicated users
43         else:
44             return cache_page(CourseListSubView.as_view(), 60 * 60)(request, *args, **kwargs)
45
46     def post(self, request, *args, **kwargs):
47         ret = CourseAddFormView.as_view()(request, *args, **kwargs)
48         # Check to see if the form came back with errors.
49         if hasattr(ret, 'context_data') and \
50            ret.context_data.has_key('form') and \
51            not ret.context_data['form'].is_valid():
52             # Invalid form. Render as if by get(), but replace the form.
53             badform = ret.context_data['form']
54             request.method = 'GET' # trick get() into returning something
55             ret = self.get(request, *args, **kwargs)
56             # Replace blank form with invalid form.
57             ret.context_data['course_form'] = badform
58             ret.context_data['jump_to_form'] = True
59         else:
60             messages.add_message(request, messages.SUCCESS, 'You\'ve just created this course. Nice!')
61         return ret
62
63
64 class CourseListSubView(ListView):
65     """ Lists all courses. Called by CourseListView. """
66     model = Course
67
68     def get_queryset(self):
69         return Course.objects.all().select_related('note_set', 'school', 'department', 'department__school')
70
71     def get_context_data(self, **kwargs):
72         """ Add the CourseForm to ListView context """
73         # get the original context
74         context = super(CourseListSubView, self).get_context_data(**kwargs)
75         # get the total number of notes
76         context['note_count'] = Note.objects.count()
77         # get the course form for the form at the bottom of the homepage
78         context['course_form'] = CourseForm()
79
80         schools = set()
81         for course in self.object_list:
82             if course.school:
83                 schools.add(course.school)
84             elif course.department and course.department.school:
85                 schools.add(course.department.school)
86
87         context['schools'] = sorted(list(schools), key=lambda x: x.name)
88
89         # Include settings constants for honeypot
90         for key in ('HONEYPOT_FIELD_NAME', 'HONEYPOT_VALUE'):
91             context[key] = getattr(settings, key)
92
93         return context
94
95
96 class CourseAddFormView(CreateView):
97     """ Processes new course additions. Called by CourseListView. """
98     model = Course
99     form_class = CourseForm
100
101     def get_template_names(self):
102         """ template_name must point back to CourseListView url """
103         # TODO clean this up. "_list" template might come from ListView above.
104         return ['courses/course_list.html',]
105
106
107 class CourseDetailView(DetailView):
108     """ Class-based view for the course html page """
109     model = Course
110     context_object_name = u"course" # name passed to template
111
112     def get_context_data(self, **kwargs):
113         """ filter the Course.note_set to return no Drafts """
114         kwargs = super(CourseDetailView, self).get_context_data()
115         kwargs['note_set'] = self.object.note_set.filter(is_hidden=False)
116
117         # For the Filepicker Partial template
118         kwargs['file_upload_form'] = FileUploadForm()
119         kwargs['note_categories'] = Note.NOTE_CATEGORIES
120
121         if self.request.user.is_authenticated():
122             try:
123                 self.request.user.get_profile().flagged_courses.get(pk=self.object.pk)
124                 kwargs['already_flagged'] = True
125             except ObjectDoesNotExist:
126                 pass
127
128         return kwargs
129
130
131 class AboutView(TemplateView):
132     """ Display the About page with the Schools leaderboard """
133     template_name = "about.html"
134
135     def get_context_data(self, **kwargs):
136         """ get the list of schools with the most files for leaderboard """
137         if 'schools' not in kwargs:
138             kwargs['schools'] = School.objects.filter(file_count__gt=0).order_by('-file_count')[:20]
139         return kwargs
140
141
142 def school_list(request):
143     """ Return JSON describing Schools that match q query on name """
144     if not (request.method == 'POST' and request.is_ajax()
145                         and request.POST.has_key('q')):
146         #return that the api call failed
147         return HttpResponseBadRequest(json.dumps({'status':'fail'}), mimetype="application/json")
148
149     # if an ajax get request with a 'q' name query
150     # get the schools as a id name dict,
151     _query = request.POST['q']
152     matching_school_aliases = list(School.objects.filter(alias__icontains=_query))
153     matching_school_names = sorted(list(School.objects.filter(name__icontains=_query)[:20]),key=lambda o:len(o.name))
154     _schools = matching_school_aliases[:2] + matching_school_names
155     schools = [{'id': s.id, 'name': s.name} for s in _schools]
156
157     # return as json
158     return HttpResponse(json.dumps({'status':'success', 'schools': schools}), mimetype="application/json")
159
160
161 def school_course_list(request):
162     """Return JSON describing courses we know of at the given school
163      that match the query """
164     if not (request.method == 'POST' and request.is_ajax()
165                         and request.POST.has_key('q')
166                         and request.POST.has_key('school_id')):
167         # return that the api call failed
168         return HttpResponseBadRequest(json.dumps({'status': 'fail', 'message': 'query parameters missing'}),
169                                     mimetype="application/json")
170
171     _query = request.POST['q']
172     try:
173       _school_id = int(request.POST['school_id'])
174     except:
175       return HttpResponseBadRequest(json.dumps({'status': 'fail',
176                                               'message': 'could not convert school id to integer'}),
177                                   mimetype="application/json")
178
179     # Look up the school
180     try:
181         school = School.objects.get(id__exact=_school_id)
182     except (MultipleObjectsReturned, ObjectDoesNotExist):
183         return HttpResponseBadRequest(json.dumps({'status': 'fail',
184                                                 'message': 'school id did not match exactly one school'}),
185                                     mimetype="application/json")
186
187     # Look up matching courses
188     _courses = Course.objects.filter(school__exact=school.id, name__icontains=_query)
189     courses = [{'name': c.name} for c in _courses]
190
191     # return as json
192     return HttpResponse(json.dumps({'status':'success', 'courses': courses}),
193                         mimetype="application/json")
194
195
196 def school_course_instructor_list(request):
197     """Return JSON describing instructors we know of at the given school
198        teaching the given course
199        that match the query """
200     if not(request.method == 'POST' and request.is_ajax()
201                         and request.POST.has_key('q')
202                         and request.POST.has_key('course_name')
203                         and request.POST.has_key('school_id')):
204         # return that the api call failed
205         return HttpResponseBadRequest(json.dumps({'status': 'fail', 'message': 'query parameters missing'}),
206                                     mimetype="application/json")
207
208     _query = request.POST['q']
209     _course_name = request.POST['course_name']
210     try:
211       _school_id = int(request.POST['school_id'])
212     except:
213       return HttpResponseBadRequest(json.dumps({'status': 'fail',
214                                               'message':'could not convert school id to integer'}),
215                                   mimetype="application/json")
216
217     # Look up the school
218     try:
219         school = School.objects.get(id__exact=_school_id)
220     except (MultipleObjectsReturned, ObjectDoesNotExist):
221         return HttpResponseBadRequest(json.dumps({'status': 'fail',
222                                                   'message': 'school id did not match exactly one school'}),
223                                     mimetype="application/json")
224
225     # Look up matching courses
226     _courses = Course.objects.filter(school__exact=school.id,
227                                      name__exact=_course_name,
228                                      instructor_name__icontains=_query)
229     instructors = [{'name': c.instructor_name, 'url': c.get_absolute_url()} for c in _courses]
230
231     # return as json
232     return HttpResponse(json.dumps({'status':'success', 'instructors': instructors}),
233                         mimetype="application/json")
234
235
236 def process_course_flag_events(request_user, course):
237     # Take a point away from person flagging this course
238     if request_user.is_authenticated():
239         CourseKarmaEvent.create_event(request_user, course, CourseKarmaEvent.GIVE_FLAG)
240
241
242 def flag_course(request, pk):
243     """Record that somebody has flagged a note."""
244     return ajax_increment(Course, request, pk, FLAG_FIELD, USER_PROFILE_FLAGS_FIELD, process_course_flag_events)
245
246
247 def edit_course(request, pk):
248     """
249     Saves the edited course content
250     """
251     if request.method == "POST" and request.is_ajax():
252         course = Course.objects.get(pk=pk)
253         original_name = course.name
254         course_form = CourseForm(request.POST or None, instance=course)
255
256         if course_form.is_valid():
257             course_form.save()
258
259             course_json = serializers.serialize('json', [course,])
260             resp = json.loads(course_json)[0]
261
262             if (course.name != original_name):
263                 course.set_slug()
264                 resp['fields']['new_url'] = course.get_absolute_url()
265
266             return HttpResponse(json.dumps(resp), mimetype="application/json")
267         else:
268             return HttpResponseBadRequest(json.dumps({'status': 'fail', 'message': 'Validation error',
269                                           'errors': course_form.errors}),
270                                           mimetype="application/json")
271     else:
272         return HttpResponseBadRequest(json.dumps({'status': 'fail', 'message': 'Invalid request'}),
273                                       mimetype="application/json")