views.py 21.8 KB
Newer Older
1
2
# standard library imports
from __future__ import absolute_import, print_function
3
import random
4
from distutils.util import strtobool
5
6
from collections import OrderedDict
from itertools import groupby
7
import re
8
# core django imports
9
from django.http import HttpResponseForbidden, HttpResponseRedirect, Http404
Daniel W Bond's avatar
pep8 me    
Daniel W Bond committed
10
from django.views.generic import CreateView, ListView, DetailView, FormView, DeleteView
11
from django.core.urlresolvers import reverse
12
13
from django.contrib import messages
from django.utils.safestring import mark_safe
14
from django.forms.widgets import HiddenInput
15
16
17
18
from django.conf import settings
from django.template.loader import get_template
from django.core.mail import EmailMultiAlternatives, get_connection
from django.template import Context
19
from django.core.exceptions import ObjectDoesNotExist
20
# third party imports
21
from braces.views import LoginRequiredMixin, FormValidMessageMixin
22
from cas.views import login as cas_login
23
from ratelimit.decorators import ratelimit
24
# imports from your apps
Daniel W Bond's avatar
Daniel W Bond committed
25
from .models import Student, Major, Confirmation
26
from .forms import StudentUpdateForm, FarewellFeedbackForm
27
from .student_messages import return_messages
28
from housing.models import Room
29
from housing.views import shadowbanning
30

31
32

def custom_cas_login(request, *args, **kwargs):
33
    """If a student has not completed the welcome walkthrough, go there on login."""
34
35
    response = cas_login(request, *args, **kwargs)
    # returns HttpResponseRedirect
36

37
38
    if request.user.is_authenticated():

39
40
41
        if not request.user.student.totally_done():

            if not request.user.student.completedName:
42
                return HttpResponseRedirect(reverse('welcomeName'))
43
            elif not request.user.student.completedPrivacy:
44
                return HttpResponseRedirect(reverse('welcomePrivacy'))
45
            elif not request.user.student.completedMajor:
46
                return HttpResponseRedirect(reverse('welcomeMajor'))
47
            elif not request.user.student.completedSocial:
48
                return HttpResponseRedirect(reverse('welcomeSocial'))
49
50
51
        else:
            welcome_back = random.choice(return_messages)
            messages.add_message(request, messages.INFO, mark_safe(welcome_back))
52
53
54
55

    return response


56
# only two students on the same floor can confirm one another (crowdsourced verification)
57
58
def on_the_same_floor(student, confirmer):
    if student == confirmer:
59
        # Student is confirmer
60
61
62
63
64
        return False
    student_floor = student.get_floor()
    confirmer_floor = confirmer.get_floor()
    # room hasn't been set yet
    if (student_floor is None) or (confirmer_floor is None):
65
        # one Student is None
66
67
        return False
    elif not(student_floor == confirmer_floor):
68
        # not the same floor
69
70
71
72
73
        return False
    else:
        return True


74
75
76
77
78
79
def pk_or_none(me, obj):
    if obj is None:
        return None
    else:
        return obj.pk

80

81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
def create_email(text_path, html_path, subject, to, context):
    text_email = get_template(text_path)
    html_email = get_template(html_path)

    email_context = Context(context)

    from_email, cc = ('noreply@srct.gmu.edu',
                      '')

    text_content = text_email.render(email_context)
    html_content = html_email.render(email_context)

    msg = EmailMultiAlternatives(subject, text_content, from_email, [to], [cc])
    # mime multipart requires attaching text and html in this order
    msg.attach_alternative(html_content, 'text/html')
    return msg

98

99
100
101
102
103
def no_nums(name):
    no_numbers = re.sub('[0-9]', '', name)
    return no_numbers


104
105
106
# details about the student
class DetailStudent(LoginRequiredMixin, DetailView):
    model = Student
107
    context_object_name = 'student'
Daniel W Bond's avatar
Daniel W Bond committed
108
    template_name = 'detail_student.html'
109
110

    login_url = 'login'
Daniel W Bond's avatar
Daniel W Bond committed
111

112
113
114
115
    def get(self, request, *args, **kwargs):

        current_url = self.request.get_full_path()
        url_uname = current_url.split('/')[3]
116
117
118
119
        try:
            detailed_student = Student.objects.get(user__username=url_uname)
        except ObjectDoesNotExist:
            raise Http404
120
121
122
123
124
125

        if (detailed_student in self.request.user.student.blocked_kids.all()):
            raise Http404
        else:
            return super(DetailStudent, self).get(request, *args, **kwargs)

Daniel W Bond's avatar
Daniel W Bond committed
126
127
128
    def get_context_data(self, **kwargs):
        context = super(DetailStudent, self).get_context_data(**kwargs)

129
        requesting_student = Student.objects.get(user=self.request.user)
Daniel W Bond's avatar
Daniel W Bond committed
130

131
132
133
134
135
        same_floor = on_the_same_floor(self.get_object(), requesting_student)

        flags = Confirmation.objects.filter(confirmer=requesting_student,
                                            student=self.get_object()).count()

Daniel W Bond's avatar
Daniel W Bond committed
136
137
138
139
140
        if flags:
            try:
                my_flag = Confirmation.objects.get(confirmer=requesting_student,
                                                   student=self.get_object())
            except Exception as e:
141
142
                print("Students are not supposed to be able to make more than one flag per student.")
                print(e)
143

Daniel W Bond's avatar
pep8 me    
Daniel W Bond committed
144
        # recognizably too complex
Daniel W Bond's avatar
Daniel W Bond committed
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
        def onFloor():
            floor_status = False
            if requesting_student.get_floor() == self.get_object().get_floor():
                floor_status = True
            return floor_status

        def inBuilding():
            floor_status = False
            if requesting_student.get_building() == self.get_object().get_building():
                floor_status = True
            return floor_status

        def shares():
            student_shares = False
            # if the student's privacy is floor and the requesting user is on their floor
            if(self.get_object().privacy == 'floor') and onFloor():
                student_shares = True
Daniel W Bond's avatar
Daniel W Bond committed
162
163
            # if the student's privacy is building and the requesting users is
            # on their floor or in their building
Daniel W Bond's avatar
Daniel W Bond committed
164
165
166
167
168
169
            elif(self.get_object().privacy == 'building') and inBuilding():
                student_shares = True
            # if the student's privacy is set to 'student'
            elif(self.get_object().privacy == 'students'):
                student_shares = True
            return student_shares
Daniel W Bond's avatar
Daniel W Bond committed
170

Daniel W Bond's avatar
Daniel W Bond committed
171
        context['shares'] = shares()
172
173
        context['same_floor'] = same_floor
        context['has_flagged'] = bool(flags)
Daniel W Bond's avatar
Daniel W Bond committed
174
175
        if flags:
            context['my_flag'] = my_flag
Daniel W Bond's avatar
Daniel W Bond committed
176
177
        return context

Daniel W Bond's avatar
Daniel W Bond committed
178

179
# update a student, but FormView to allow name update on same page
180
class UpdateStudent(LoginRequiredMixin, FormValidMessageMixin, FormView):
181
    template_name = 'update_student.html'
182
    form_class = StudentUpdateForm
183
184
    login_url = 'login'

185
186
    form_valid_message = "Your profile was successfully updated!"

187
188
189
190
    def get(self, request, *args, **kwargs):

        current_url = self.request.get_full_path()
        url_uname = current_url.split('/')[3]
191
192
193
194
        try:
            student = Student.objects.get(user__username=url_uname)
        except ObjectDoesNotExist:
            raise Http404
195
196

        if not(url_uname == self.request.user.username):
197
198
            return HttpResponseRedirect(reverse('update_student',
                                                kwargs={'slug': self.request.user.username}))
199
200
201
        else:
            return super(UpdateStudent, self).get(request, *args, **kwargs)

202
203
204
205
    def get_context_data(self, **kwargs):
        context = super(UpdateStudent, self).get_context_data(**kwargs)

        me = Student.objects.get(user=self.request.user)
206
        majors = [pk_or_none(me, major) for major in me.major.all()]
207
208

        form = StudentUpdateForm(initial={'first_name': me.user.first_name,
209
210
                                          'last_name': me.user.last_name,
                                          'gender': me.gender,
211
                                          'show_gender': me.show_gender,
212
213
                                          'room': pk_or_none(me, me.room),
                                          'privacy': me.privacy,
214
                                          'blocked_kids': me.blocked_kids.all(),
215
                                          'major': majors,
216
217
                                          'graduating_year': me.graduating_year,
                                          'on_campus': me.on_campus, })
218

219
        form.fields['blocked_kids'].queryset = Student.objects.exclude(user=self.request.user)
220

221
        if me.recent_changes() > 2:
222
            form.fields['room'].widget = HiddenInput()
223
224
            form.fields['privacy'].widget = HiddenInput()
            form.fields['on_campus'].widget = HiddenInput()
225
226
        else:
            form.fields['room'].widget.user = self.request.user
227

228
        # chosen
229
230
        form.fields['major'].widget.attrs['class'] = 'form-control chosen-select'
        form.fields['blocked_kids'].widget.attrs['class'] = 'form-control blocked-select'
231

232
        context['my_form'] = form
Daniel W Bond's avatar
Daniel W Bond committed
233

234
235
        return context

Daniel W Bond's avatar
Daniel W Bond committed
236
237
238
    @ratelimit(key='user', rate='5/m', method='POST', block=True)
    @ratelimit(key='user', rate='10/d', method='POST', block=True)
    def post(self, request, *args, **kwargs):
Daniel W Bond's avatar
pep8 me    
Daniel W Bond committed
239
        # for key, value in request.POST.iteritems():
240
        #     print(key, value)
Daniel W Bond's avatar
Daniel W Bond committed
241
242
        return super(UpdateStudent, self).post(request, *args, **kwargs)

243
244
245
    def form_valid(self, form):
        me = Student.objects.get(user=self.request.user)

Daniel W Bond's avatar
pep8 me    
Daniel W Bond committed
246
        # print("In form valid method!")
Daniel W Bond's avatar
Daniel W Bond committed
247

Daniel W Bond's avatar
pep8 me    
Daniel W Bond committed
248
        # for key, value in form.data.iteritems():
249
        #     print(key, value)
Daniel W Bond's avatar
Daniel W Bond committed
250

251
        current_room = me.room
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266

        # if you somehow got around the hidden widget, you're still outta luck
        if me.recent_changes() > 2:
            form_room = current_room
        else:
            try:
                form_room = Room.objects.get(pk=form.data['room'])
            except:
                form_room = None

        # casts to an integer, 0 or 1
        on_campus = strtobool(form.data.get('on_campus', 'True'))

        # no room if you move off campus
        if not on_campus:
267
268
            form_room = None

269
        # note this is after the 'on campus' check
270
271
        if current_room != form_room:
            me.times_changed_room += 1
272
            Confirmation.objects.filter(student=me).delete()
273

274
        me.on_campus = on_campus
275
276
        me.room = form_room

277
278
279
280
281
282
283
        # if you don't live on campus you can't limit yourself to a nonexistent
        # floor or room
        if not(me.on_campus):
            me.privacy = 'students'
        else:
            me.privacy = form.data['privacy']

284
        try:
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
            # in case someone disabled the js, limit processing to only the first
            # two majors passed by the user
            # we also eliminate the potential a student manipulates the form to
            # pass in two majors of the same type by casting to a set
            form_major_pks = set(form.data.getlist('major')[:2])
            # retrieve the major objects from the list of pk strings
            form_majors = [Major.objects.get(pk=pk) for pk in form_major_pks]
            # print(form_majors)
            # iterate over a student's current majors
            for current_major in me.major.all():
                # remove m2m relationship if not in majors from form
                if current_major not in form_majors:
                    me.major.remove(current_major)
            # iterate over the majors in the form
            for form_major in form_majors:
                # add new m2m relationship to student
                if form_major not in me.major.all():
                    me.major.add(form_major)
303
        except:
304
305
            # don't change majors
            pass
306

307
        # replicate the same thing for the other m2m field
308
        try:
309
310
311
            form_blocked_pks = set(form.data.getlist('blocked_kids'))
            current_blocked = me.blocked_kids.all()
            # most people will not being blocking other students
312
            if form_blocked_pks or current_blocked:
313
314
315
316
317
318
319
                form_blocked = [Student.objects.get(pk=pk) for pk in form_blocked_pks]
                for current_block in current_blocked:
                    if current_block not in form_blocked:
                        me.blocked_kids.remove(current_block)
                for form_block in form_blocked:
                    if form_block not in current_blocked:
                        me.blocked_kids.add(form_block)
320
        except:
321
            pass
322

323
324
        me.user.first_name = no_nums(form.data['first_name'])
        me.user.last_name = no_nums(form.data['last_name'])
325
        me.gender = form.data.getlist('gender')
326
        me.show_gender = strtobool(form.data.get('show_gender', 'False'))
327
        me.graduating_year = form.data['graduating_year']
328
329
330
331
332
333
        me.user.save()
        me.save()

        return super(UpdateStudent, self).form_valid(form)

    def get_success_url(self):
334
335
336
337
338

        if self.request.user.student.recent_changes() == 2:

            messages.add_message(self.request, messages.WARNING, 'To safeguard everyone\'s privacy, you have just one remaining room change for the semester before you\'ll need to send us an email at roomlist@lists.srct.gmu.edu.')

339
        return reverse('detail_student',
Daniel W Bond's avatar
pep8 me    
Daniel W Bond committed
340
                       kwargs={'slug': self.request.user.username})
341

342

343
344
345
class DeleteStudent(FormView):
    form_class = FarewellFeedbackForm
    template_name = 'delete_student.html'
346

347
348
349
350
    def get(self, request, *args, **kwargs):

        current_url = self.request.get_full_path()
        url_uname = current_url.split('/')[3]
351
352
353
354
        try:
            student = Student.objects.get(user__username=url_uname)
        except ObjectDoesNotExist:
            raise Http404
355
356

        if not(url_uname == self.request.user.username):
357
358
            return HttpResponseRedirect(reverse('delete_student',
                                                kwargs={'slug': self.request.user.username}))
359
        else:
360
            return super(DeleteStudent, self).get(request, *args, **kwargs)
361

362
    def get_context_data(self, **kwargs):
363
        context = super(DeleteStudent, self).get_context_data(**kwargs)
364
365
366

        me = Student.objects.get(user=self.request.user)

367
        context['student'] = me
368
369
370

        return context

Daniel W Bond's avatar
Daniel W Bond committed
371
    def form_valid(self, form):
372
373
374
        user = self.request.user
        student = self.request.user.student

375
376
377
378
379
380
381
382
383
384
385
386
        # we're using this api because opening smtp connections is taxing in
        # that it takes time-- we want to send both emails at once without
        # having to log in and out and back in and out again
        connection = get_connection()

        # send email to the student
        text_path = 'email/farewell.txt'
        html_path = 'email/farewell.html'

        if form.cleaned_data['leaving']:
            context = {
                'student_name': student.get_first_name_or_uname,
387
                'special_message': "We're glad you gave our service a try."
388
            }
389
        else:
390
391
392
393
            context = {
                'student_name': student.get_first_name_or_uname,
                'special_message': "We wish you luck in your time after Mason!"
            }
394

395
396
        subject = "You successfully deleted your account on Roomlist"
        to = user.email
397

398
        student_email = create_email(text_path, html_path, subject, to, context)
399

400
401
402
403
        # send feedback to the admins if there is feedback to send
        if form.cleaned_data['feedback']:
            text_path = 'email/feedback.txt'
            html_path = 'email/feedback.html'
404

405
            date_text = student.created.strftime('%A, %B %d, %Y')
406

407
408
409
410
            if form.cleaned_data['leaving']:
                leaving = ""
            else:
                leaving = "not"
411

412
413
414
415
416
417
            context = {
                'student_name':  student.get_first_name_or_uname,
                'signup_date': date_text,
                'leaving': leaving,
                'feedback': form.cleaned_data['feedback']
            }
418

419
420
            subject = "Feedback from Roomlist account deletion"
            to = 'roomlist@lists.srct.gmu.edu'
421

422
            feedback_email = create_email(text_path, html_path, subject, to, context)
423

424
            connection.send_messages([student_email, feedback_email])
425
        else:
426
            connection.send_messages([student_email])
427

428
429
        # yes, we do have to manually close the connection
        connection.close()
430

431
432
433
434
435
        # delete both the student object and the student object
        confirmations = Confirmation.objects.filter(confirmer=student)
        if confirmations:
            for confirmation in confirmations:
                confirmation.delete()
436
437
        student.delete()
        user.delete()
438

439
        return super(DeleteStudent, self).form_valid(form)
440

441
    def get_success_url(self):
442
        return reverse('homepage')
443
444

# majors pages
445
class ListMajors(ListView):
Daniel W Bond's avatar
Daniel W Bond committed
446
447
448
449
450
    model = Major
    queryset = Major.objects.all().order_by('name')
    context_object_name = 'majors'
    template_name = 'list_majors.html'

451
452
453
454
455
456
457
458
459
460
461
462

class DetailMajor(LoginRequiredMixin, DetailView):
    model = Major
    context_object_name = 'major'
    template_name = 'detail_major.html'

    login_url = 'login'

    def get_context_data(self, **kwargs):
        context = super(DetailMajor, self).get_context_data(**kwargs)
        me = Student.objects.get(user=self.request.user)

463
464
        # all students in the major-- needs to be ordered for groupby
        major_students = Student.objects.filter(major__in=[self.get_object()]).order_by('graduating_year')
465

466
        students_by_year = OrderedDict()  # we're ordering from senior on down
467

468
469
470
471
        for year, students in groupby(major_students, lambda student: student.graduating_year):
            student_list = list(students)  # students without the casting is an iterator
            visible_students = shadowbanning(me, student_list)  # remove blocked students
            students_by_year[year] = visible_students  # remembers insertion order
472

473
        context['students_by_year'] = students_by_year
474
475

        return context
476
477
478


class CreateConfirmation(LoginRequiredMixin, CreateView):
479
480
481
    """Students on the same floor may flag one another.

    This is our attempt at crowdsourced verification."""
482
483
484
485
486
487
488
489
490
    model = Confirmation
    fields = []
    template_name = 'create_confirmation.html'

    login_url = 'login'

    def get(self, request, *args, **kwargs):

        current_url = self.request.get_full_path()
491
492
493
        # [u'', u'accounts', u'student', u'gmason', u'flag', u'confirmer']
        confirmer_uname = current_url.split('/')[3]
        student_uname = current_url.split('/')[5]
494
495
496
497
498
499
500
        try:
            confirmer = Student.objects.get(user__username=confirmer_uname)
            student = Student.objects.get(user__username=student_uname)
            flags = Confirmation.objects.filter(confirmer=confirmer,
                                                student=student).count()
        except ObjectDoesNotExist:
            raise Http404
501
502
503

        # you can't flag yourself
        if confirmer == student:
504
            raise Http404
505
506
507
508
509
510
511
512
513

        # check that the confirmer is on the floor of the student
        if not on_the_same_floor(student, confirmer):
            return HttpResponseForbidden()

        # check if the confirmer has already flagged the student
        if flags >= 1:
            return HttpResponseForbidden()

514
515
516
        # you can't see the page if the person has banned you
        if confirmer in student.blocked_kids.all():
            raise Http404
517
518
519
520
521
522
523
524

        return super(CreateConfirmation, self).get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super(CreateConfirmation, self).get_context_data(**kwargs)

        # duplicated code
        current_url = self.request.get_full_path()
525
        url_uname = current_url.split('/')[5]
526
527
528
529
530
531
532

        student = Student.objects.get(slug=url_uname)

        context['student'] = student

        return context

533
534
535
536
537
    @ratelimit(key='user', rate='10/m', method='POST', block=True)
    @ratelimit(key='user', rate='50/d', method='POST', block=True)
    def post(self, request, *args, **kwargs):
        return super(CreateConfirmation, self).post(request, *args, **kwargs)

538
539
540
541
    def form_valid(self, form):

        # duplicated code
        current_url = self.request.get_full_path()
542
        url_uname = current_url.split('/')[5]
543
544
545
546
547
548
549
550
551
552
553
554

        confirmer = Student.objects.get(user=self.request.user)
        student = Student.objects.get(slug=url_uname)

        form.instance.confirmer = confirmer
        form.instance.student = student

        return super(CreateConfirmation, self).form_valid(form)

    def get_success_url(self):
        # redirect to the flagged student page when saving
        return reverse('detail_student',
Daniel W Bond's avatar
pep8 me    
Daniel W Bond committed
555
                       kwargs={'slug': self.object.student.slug})
556
557
558
559


class DeleteConfirmation(LoginRequiredMixin, DeleteView):
    model = Confirmation
Daniel W Bond's avatar
Daniel W Bond committed
560
    template_name = 'delete_confirmation.html'
561
562
563

    login_url = 'login'

Daniel W Bond's avatar
Daniel W Bond committed
564
    def get(self, request, *args, **kwargs):
565
566
        requester = self.request.user.student

567
568
        current_url = self.request.get_full_path()
        confirmer_uname = current_url.split('/')[3]
569
570

        # not catching exceptions here; that's handled in get_object
571

572
        # only the person who created the confirmation may delete it
Daniel W Bond's avatar
Daniel W Bond committed
573
574
        if not(requester == confirmer):
            return HttpResponseForbidden()
575
        # however, if the confirmation just flat out doesn't exist...
Daniel W Bond's avatar
Daniel W Bond committed
576
        else:
577
578
579
580
581
582
            try:
                confirmer = self.get_object().confirmer
            except ObjectDoesNotExist:
                raise Http404
            else:
                return super(DeleteConfirmation, self).get(request, *args, **kwargs)
583

584
585
586
587
588
589
    def get_object(self):
        current_url = self.request.get_full_path()
        # [u'', u'accounts', u'student', u'gmason', u'flag', u'confirmer', delete]
        confirmer_uname = current_url.split('/')[3]
        student_uname = current_url.split('/')[5]

590
591
592
593
594
595
        try:
            confirmer = Student.objects.get(user__username=confirmer_uname)
            student = Student.objects.get(user__username=student_uname)
            confirmation = Confirmation.objects.get(confirmer=confirmer, student=student)
        except ObjectDoesNotExist:
            raise Http404
596
        return confirmation
597
598
599

    def get_success_url(self):
        return reverse('detail_student',
Daniel W Bond's avatar
pep8 me    
Daniel W Bond committed
600
                       kwargs={'slug': self.object.student.slug})