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