3 # Copyright (C) 2012 FinalsClub Foundation
4 """ Views for the KarmaNotes Courses app """
6 from time import strftime
7 from django.db.models import Q
8 from django.utils.html import escape
10 from querystring_parser import parser as querystring_parser
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
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
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
35 USER_PROFILE_FLAGS_FIELD = 'flagged_courses'
38 # https://docs.djangoproject.com/en/1.5/topics/class-based-views/mixins/#an-alternative-better-solution
39 class CourseListView(View):
41 Composite view to list all courses and processes new course additions.
44 def get(self, request, *args, **kwargs):
45 return CourseListSubView.as_view()(request, *args, **kwargs)
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
61 messages.add_message(request, messages.SUCCESS, 'You\'ve just created this course. Nice!')
65 class CourseListSubView(ListView):
66 """ Lists all courses. Called by CourseListView. """
69 def get_queryset(self):
70 return Course.objects.all().select_related('note_set', 'school', 'department', 'department__school')
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()
82 for course in self.object_list:
84 schools.add(course.school)
85 elif course.department and course.department.school:
86 schools.add(course.department.school)
88 context['schools'] = sorted(list(schools), key=lambda x: x.name)
90 # Include settings constants for honeypot
91 for key in ('HONEYPOT_FIELD_NAME', 'HONEYPOT_VALUE'):
92 context[key] = getattr(settings, key)
97 class CourseAddFormView(CreateView):
98 """ Processes new course additions. Called by CourseListView. """
100 form_class = CourseForm
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',]
108 class CourseDetailView(DetailView):
109 """ Class-based view for the course html page """
111 context_object_name = u"course" # name passed to template
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)
118 # For the Filepicker Partial template
119 kwargs['file_upload_form'] = FileUploadForm()
120 kwargs['note_categories'] = Note.NOTE_CATEGORIES
122 if self.request.user.is_authenticated():
124 self.request.user.get_profile().flagged_courses.get(pk=self.object.pk)
125 kwargs['already_flagged'] = True
126 except ObjectDoesNotExist:
132 class AboutView(TemplateView):
133 """ Display the About page with the Schools leaderboard """
134 template_name = "about.html"
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]
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")
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]
159 return HttpResponse(json.dumps({'status':'success', 'schools': schools}), mimetype="application/json")
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")
172 _query = request.POST['q']
174 _school_id = int(request.POST['school_id'])
176 return HttpResponseBadRequest(json.dumps({'status': 'fail',
177 'message': 'could not convert school id to integer'}),
178 mimetype="application/json")
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")
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]
193 return HttpResponse(json.dumps({'status':'success', 'courses': courses}),
194 mimetype="application/json")
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")
209 _query = request.POST['q']
210 _course_name = request.POST['course_name']
212 _school_id = int(request.POST['school_id'])
214 return HttpResponseBadRequest(json.dumps({'status': 'fail',
215 'message':'could not convert school id to integer'}),
216 mimetype="application/json")
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")
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]
233 return HttpResponse(json.dumps({'status':'success', 'instructors': instructors}),
234 mimetype="application/json")
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)
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)
248 def course_json(course):
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()]),
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())
260 # Prevent XSS attacks
261 for k in course_data:
262 course_data[k] = escape(course_data[k])
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)
274 objects = Course.objects.all()
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']))
282 for order_index in request_dict['order']:
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'
292 if order['dir'] == 'desc':
293 order_field = '-' + order_field
296 order_fields.append(order_field)
298 objects = objects.order_by(*order_fields)
300 displayRecords = objects.count()
303 objects = objects[start:]
305 objects = objects[:length]
310 calendar.timegm(course.updated_at.timetuple()),
313 course.school.name if course.school else course.department.school.name,
314 ] for course in objects
319 'recordsTotal': Course.objects.count(),
320 'recordsFiltered': displayRecords,
324 return HttpResponse(json.dumps(response_dict), mimetype='application/json')
327 def course_list_ajax(request):
328 return ajax_base(request, course_list_ajax_handler, ['GET'])
331 def edit_course(request, pk):
333 Saves the edited course content
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)
340 if course_form.is_valid():
343 course_json = serializers.serialize('json', [course,])
344 resp = json.loads(course_json)[0]
346 if (course.name != original_name):
348 resp['fields']['new_url'] = course.get_absolute_url()
350 return HttpResponse(json.dumps(resp), mimetype="application/json")
352 return HttpResponseBadRequest(json.dumps({'status': 'fail', 'message': 'Validation error',
353 'errors': course_form.errors}),
354 mimetype="application/json")
356 return HttpResponseBadRequest(json.dumps({'status': 'fail', 'message': 'Invalid request'}),
357 mimetype="application/json")