Commit bbf6af1f authored by Daniel W Bond's avatar Daniel W Bond

merged amherst into master

parents 7fdd6299 644bca2a
......@@ -11,6 +11,7 @@ develop-eggs/
dist/
downloads/
eggs/
.idea
lib/
lib64/
parts/
......
image: ubuntu:14.04
services:
- mysql:latest
types:
- test
variables:
MYSQL_DATABASE: roomlist
MYSQL_ROOT_PASSWORD: root
test_Roomlist:
type: test
before_script:
- apt-get update -qy
- apt-get install -y python-dev python-pip libldap2-dev mysql-server mysql-client libmysqlclient-dev python-mysqldb libsasl2-dev libjpeg-dev git
- pip install -r requirements.txt
- pip install coverage
- cp roomlist/settings/secret.py.template roomlist/settings/secret.py
- export SECRET_KEY=$(dd if=/dev/urandom count=100 | tr -dc "A-Za-z0-9" | fold -w 60 | head -n1 2>/dev/null)
- sed -i go/settings/secret.py -e 's/DB_PASSWORD.*/DB_PASSWORD = \"root\"/'
- cd roomlist
- python manage.py makemigrations accounts
- python manage.py makemigrations housing
- python manage.py makemigrations
- python manage.py migrate
- python manage.py loaddata accounts/major_fixtures.json
- python manage.py shell < housing/housing_obj_creator.py
- echo "from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.create_superuser('root', 'root@srct.gmu.edu', 'root') " | python ./manage.py shell
script:
- cd ..
- coverage run --source=roomlist ./roomlist/manage.py test
- coverage html
- grep pc_cov htmlcov/index.html | egrep -o "[0-9]+\%" | awk '{ print "covered " $1;}'
This diff is collapsed.
Django==1.7.3
Whoosh==2.6.0
argparse==1.2.1
beautifulsoup4==4.3.2
django-allauth==0.20.0
django-analytical==0.19.0
django-autoslug==1.7.2
django-braces==1.4.0
django-crispy-forms==1.4.0
git+https://github.com/kstateome/django-cas.git
django-gravatar2==1.1.4
django-haystack==2.3.1
django-localflavor==1.0
django-model-utils==2.2
beautifulsoup4==4.4.1
Django==1.9.12
django-allauth==0.28.0
django-analytical==1.0.0
django-autoslug==1.9.3
django-braces==1.8.1
django-cas-client==1.2.0
django-crispy-forms==1.5.2
django-filter==0.12.0
django-gravatar2==1.3.0
django-haystack==2.4.1
django-localflavor==1.2
django-model-utils==2.4
django-multiselectfield==0.1.3
django-randomslugfield==0.3.0
django-ratelimit==0.6.0
django-redis-cache==0.13.0
flake8==2.4.0
mccabe==0.3
django-redis-cache==1.6.4
djangorestframework==3.3.2
flake8==2.5.0
gunicorn==19.6.0
Markdown==2.6.5
mccabe==0.3.1
MySQL-python==1.2.5
oauthlib==0.7.2
pep8==1.5.7
pyflakes==0.8.1
oauthlib==2.0.0
pep8==1.6.2
pyflakes==1.0.0
python-openid==2.2.5
redis==2.10.3
requests==2.5.1
requests-oauthlib==0.4.2
six==1.8.0
redis==2.10.5
requests==2.11.1
requests-oauthlib==0.7.0
six==1.10.0
wsgiref==0.1.2
wheel==0.26.0
Whoosh==2.7.0
# standard library imports
from __future__ import absolute_import, print_function
from datetime import datetime, timedelta
# core django imports
from django.core.urlresolvers import reverse
from django.views.generic import FormView
from django.contrib import messages
from django.http import HttpResponseRedirect
# third party imports
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from allauth.socialaccount.views import ConnectionsView
from allauth.socialaccount.forms import DisconnectForm
from allauth.exceptions import ImmediateHttpResponse
from braces.views import LoginRequiredMixin
class AccountAdapter(DefaultSocialAccountAdapter):
"""A custom implementation of a portion of the allauth social media package.
# the request processed by the adapter is one from the successful oauth callback
We're overriding a number of aspects of the allauth account adapter to support
our special use of the package. We are using CAS, not Django's built-in
authentication. Accordingly we change the where directed when successfully
connecting an account and how errors are dealt with. Additionally, we are not using
the social media accounts to verify or overwrite any aspect of the User model.
"""
#def pre_social_login(self, request, sociallogin):
#print(request.get_full_path(), 'pre_login')
# the request processed by the adapter is one from the successful oauth callback
# uncomment this method to print what URL you are arriving from
# def pre_social_login(self, request, sociallogin):
# print(request.get_full_path(), 'pre_login')
def populate_user(self, request, sociallogin, data):
# we don't actually want to overwrite anything from the
# social media account user
# This is a hook to populate User attributes, but we expressly don't actually
# want to overwrite anything from the social media account user. It's intended
# in the package for when you are using social media for login.
user = sociallogin.user
return user
def get_connect_redirect_url(self, request, socialaccount):
# where the user is sent if the social account is indeed authenticated
assert request.user.is_authenticated()
#print(request.get_full_path())
#if 'welcome' in request.get_full_path():
# ergo, we go with more of an approximation (at least for now)
# we are approximating that if a user has not completed the welcome walkthough,
# it is likely the page on which they started-- see the pre_social_login method
if not request.user.student.completedSocial:
return reverse('welcomeSocial', kwargs={
'slug': request.user.username,
})
return reverse('welcomeSocial')
else:
return reverse('updateStudent', kwargs={
return reverse('update_student', kwargs={
'slug': request.user.username,
})
def authentication_error(self, request, provider_id, error=None, exception=None,
extra_context=None):
"""Adds a custom message to the message queue if social media auth fails."""
error_message = """Looks like something went awry with your social
authentication. Wait a moment and try your username and
......@@ -52,27 +56,32 @@ class AccountAdapter(DefaultSocialAccountAdapter):
sending an email to roomlist@lists.srct.gmu.edu."""
if not request.user.student.completedSocial:
# as a reminder, here is how django handles messages
# https://docs.djangoproject.com/en/1.8/ref/contrib/messages/
messages.add_message(request, messages.ERROR, error_message)
social_redirect = HttpResponseRedirect(reverse('welcomeSocial', kwargs={
'slug': request.user.username,
}))
social_redirect = HttpResponseRedirect(reverse('welcomeSocial'))
raise ImmediateHttpResponse(social_redirect)
else:
messages.add_message(request, messages.ERROR, error_message)
update_redirect = HttpResponseRedirect(reverse('updateStudent', kwargs={
update_redirect = HttpResponseRedirect(reverse('update_student', kwargs={
'slug': request.user.username,
}))
}))
raise ImmediateHttpResponse(update_redirect)
class RemoveSocialConfirmationView(LoginRequiredMixin, ConnectionsView):
"""To customize where users are sent when removing their social media connections.
We have written our own template to handle this feature that is much prettier than
the one provided by allauth."""
template_name = "remove_social.html"
login_url = 'login'
def get(self, request, *args, **kwargs):
if not request.user.socialaccount_set.all():
# no social media accounts? back to the settings page with you!
return HttpResponseRedirect(reverse('updateStudent',
kwargs={'slug':self.request.user.username}))
return HttpResponseRedirect(reverse('update_student',
kwargs={'slug': self.request.user.username}))
else:
return super(RemoveSocialConfirmationView, self).get(request, *args, **kwargs)
......@@ -80,5 +89,5 @@ class RemoveSocialConfirmationView(LoginRequiredMixin, ConnectionsView):
return super(RemoveSocialConfirmationView, self).form_valid(form)
def get_success_url(self):
return reverse('updateStudent',
kwargs={'slug':self.request.user.username})
return reverse('update_student',
kwargs={'slug': self.request.user.username})
......@@ -5,14 +5,24 @@ from django.contrib import admin
# imports from your apps
from .models import Student, Major, Confirmation
class StudentAdmin(admin.ModelAdmin):
list_display = ("get_name", "room", "privacy", "major", "created")
list_display = ("get_name", "room", "privacy", "get_first_major", "created")
def get_name(self, student):
return student.get_full_name_or_uname()
get_name.short_description = 'Name'
get_name.admin_order_field = 'user__username' # ordering by callables is hard
# We cannot use a manytomanyfield as a field in the list, so we're getting the first
# major. If we need to see a student's second major, we can just click the student.
# This covers nearly all students, who will have only one major.
def get_first_major(self, student):
return student.major.first()
get_first_major.short_description = 'Major'
# we're not going to give the option to sort by major for now
class MajorAdmin(admin.ModelAdmin):
list_display = ("name", "get_major_num", )
......
# standard library imports
from __future__ import absolute_import, print_function
from __future__ import absolute_import, print_function, unicode_literals
# core django imports
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings
from django.contrib import messages
# third party imports
import requests
# imports from your apps
......@@ -38,18 +37,24 @@ def pfinfo(uname):
except requests.exceptions.RequestException as e:
print("Cannot resolve to peoplefinder api:", e)
print("Returning empty user info tuple.")
return ([u'', u''], u'')
return (['', ''], '')
else:
pfjson = metadata.json()
try:
if len(pfjson['results']) == 1:
name_str = pfjson['results'][0]['name']
name = pfparse(name_str)
major = pfjson['results'][0]['major']
# could conceivably throw a key error
final_tuple = (name, major)
if len(pfjson['results']) == 1: # ordinary case
if pfjson['method'] == 'peoplefinder':
name_str = pfjson['results'][0]['name']
name = pfparse(name_str)
major = pfjson['results'][0]['major']
# could conceivably throw a key error
final_tuple = (name, major)
elif pfjson['method'] == 'ldap':
name = [pfjson['results'][0]['givenname'], # includes middle initial
pfjson['results'][0]['surname']]
major = '' # ldap does not have major information
final_tuple = (name, major)
return final_tuple
else:
else: # handles student employees
name_str = pfjson['results'][1]['name']
name = pfparse(name_str)
major = pfjson['results'][1]['major']
......@@ -59,19 +64,19 @@ def pfinfo(uname):
# if the name is not in peoplefinder, return empty first and last name
except IndexError:
print("Name not found in peoplefinder.")
name = [u'', u'']
major = u''
name = ['', '']
major = ''
final_tuple = (name, major)
return final_tuple
# if there's no major, just return that as an empty string
except KeyError:
print("Major not found in peoplefinder.")
final_tuple = (name, u'')
final_tuple = (name, '')
return final_tuple
except Exception as e:
print("Unknown peoplefinder error:", e)
print("Returning empty user info tuple.")
return ([u'', u''], u'')
return (['', ''], '')
def create_user(tree):
......
......@@ -4,20 +4,18 @@ from __future__ import absolute_import, print_function
from django import forms
from django.utils.safestring import mark_safe
from django.template.loader import render_to_string
from django.utils.encoding import force_text
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy as _
# third party imports
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Layout
from crispy_forms.bootstrap import PrependedText, AppendedText
from multiselectfield import MultiSelectFormField
from haystack.forms import SearchForm
# imports from your apps
from .models import Student, Major
from housing.models import Building, Floor, Room
class SelectRoomWidget(forms.widgets.Select):
"""A series of dropdowns in which a student can filter through housing options."""
template_name = 'room_select_widget.html'
......@@ -28,12 +26,12 @@ class SelectRoomWidget(forms.widgets.Select):
print("Sorry about that, but we're currently ignoring your fancy attrs.")
# should probably type check the other fields too
if rooms is None:
self.rooms = Room.objects.all()
self.rooms = Room.objects.all().prefetch_related('floor')
else:
if not all(isinstance(thing, Room) for thing in rooms):
raise TypeError("Rooms in a SelectRoomWidget must all be Rooms!")
if floors is None:
self.floors = Floor.objects.all()
self.floors = Floor.objects.all().prefetch_related('building')
if buildings is None:
self.buildings = Building.objects.all()
if neighborhoods is None:
......@@ -52,90 +50,83 @@ class SelectRoomWidget(forms.widgets.Select):
class SelectRoomField(forms.models.ModelChoiceField):
"""A special field for room selection, using the room selection widget."""
widget = SelectRoomWidget
# should raise error if user hasn't actually selected room, made it to end of selectors
# def clean(self, value):
class BooleanRadioField(forms.TypedChoiceField):
"""Displays booleans as a radio selector, rather than checkboxes."""
def __init__(self, *args, **kwargs):
boolean_choices = ((True, 'Yes'), (False, 'No'))
kwargs['widget'] = forms.RadioSelect
kwargs['choices'] = boolean_choices
kwargs['coerce'] = bool
kwargs['required'] = True
super(BooleanRadioField, self).__init__(*args, **kwargs)
class StudentUpdateForm(forms.Form):
first_name = forms.CharField(label='First Name', required=False)
last_name = forms.CharField(label='Last Name', required=False)
first_name = forms.CharField(required=False, max_length=30)
first_name.widget.attrs['class'] = 'form-control'
last_name = forms.CharField(required=False, max_length=30)
last_name.widget.attrs['class'] = 'form-control'
gender = MultiSelectFormField(choices=Student.GENDER_CHOICES,
label='Gender Identity (please choose all that apply)',
required=False)
show_gender = forms.BooleanField(label='Show your gender on your profile?',
required=False)
show_gender = BooleanRadioField(required=True)
room = SelectRoomField(queryset=Room.objects.all(), label='', required=False)
on_campus = BooleanRadioField(required=True)
room = SelectRoomField(queryset=Room.objects.all(), required=False)
privacy = forms.ChoiceField(choices=Student.PRIVACY_CHOICES)
major = forms.ModelChoiceField(queryset=Major.objects.all(), required=False,
label='Major (select one)',)
graduating_year = forms.IntegerField(label='Graduating Year')
privacy = forms.TypedChoiceField(choices=Student.PRIVACY_CHOICES)
privacy.widget.attrs['class'] = 'form-control'
# exclude self from request in form instantiation
blocked_kids = forms.ModelMultipleChoiceField(queryset=Student.objects.all(),
required=False)
major = forms.ModelMultipleChoiceField(queryset=Major.objects.all(), required=False)
graduating_year = forms.IntegerField(max_value=9999, min_value=-9999, required=False)
graduating_year.widget.attrs['class'] = 'form-control'
def clean(self):
cleaned_data = super(StudentUpdateForm, self).clean()
form_room = cleaned_data.get('room')
if not(form_room is None):
students_in_room = Student.objects.filter(room=form_room).count()
#print(students_in_room)
# print(students_in_room)
# like in bookshare, I have no idea why the form errors don't display.
if students_in_room > 12:
raise ValidationError(_('Too many students in room (%d).' % students_in_room), code='invalid')
def is_valid(self):
# errors are not printed in form.as_p?
#print("In is_valid.")
#print(self.is_bound, 'is bound')
#print(self.errors, type(self.errors), 'errors')
# print("In is_valid.")
# print(self.is_bound, 'is bound')
# print(self.errors, type(self.errors), 'errors')
valid = super(StudentUpdateForm, self).is_valid()
#print(valid)
# print(valid)
return valid
class WelcomeNameForm(forms.Form):
first_name = forms.CharField(label='First Name', required=False)
last_name = forms.CharField(label='Last Name', required=False)
gender = MultiSelectFormField(choices=Student.GENDER_CHOICES,
label='Gender Identity (please choose all that apply)',
required=False)
show_gender = forms.BooleanField(label='Show your gender on your profile?',
required=False)
class FarewellFeedbackForm(forms.Form):
class WelcomePrivacyForm(forms.ModelForm):
# required = True by default
leaving = BooleanRadioField(label="Are you graduating or leaving Mason?")
feedback = forms.CharField(label="Thoughts",
max_length=1000,
widget=forms.Textarea(attrs={'class': 'form-control'}),
required=False)
def __init__(self, *args, **kwargs):
super(WelcomePrivacyForm, self).__init__(*args, **kwargs)
if self.instance.recent_changes() > 2:
self.fields['room'].widget = forms.widgets.HiddenInput()
else:
self.fields['room'] = SelectRoomField(queryset=Room.objects.all(),
label='', required=False)
def clean(self):
cleaned_data = super(WelcomePrivacyForm, self).clean()
form_room = cleaned_data.get('room')
if not(form_room is None):
students_in_room = Student.objects.filter(room=form_room).count()
#print(students_in_room)
# like in bookshare, I have no idea why the form errors don't display.
if students_in_room > 12:
raise ValidationError(_('Too many students in room (%d).' % students_in_room), code='invalid')
class Meta:
model = Student
fields = ('room', 'privacy', )
class WelcomeSocialForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(WelcomeSocialForm, self).__init__(*args, **kwargs)
self.fields['completedSocial'].widget = forms.widgets.HiddenInput()
# we are overwriting the search form to include boostrap css classes
class AccountSearchForm(SearchForm):
class Meta:
model = Student
fields = ('completedSocial', )
q = forms.CharField(required=False, label=_('Search'),
widget=forms.TextInput(attrs={'type': 'search',
'class': 'form-control',
'autofocus': 'autofocus'}))
This diff is collapsed.
# python manage.py shell < accounts/major_slug_update.py
# this script updates slugs from a previous release of django from random strings
# to a slugified version of the object's name, e.g. the name of the major
# standard library imports
from __future__ import absolute_import, print_function, unicode_literals
from accounts.models import Major
majors = Major.objects.all()
for major in majors:
print(major.name)
print(major.slug)
major.save()
print(major.slug)
from django.core.management.base import BaseCommand, CommandError
from accounts.models import Student
from django.core.management.base import BaseCommand
from accounts.models import Student
class Command(BaseCommand):
args = ""
help = "Deletes all students' rooms at the end of the semester"
def handle(self, *args, **kwargs):
count = 0
for student in Student.objects.all():
student.room = None
count += 1
count += 1
self.stdout.write("Successfully overwrote %d student room(s)." % count)
This diff is collapsed.
bug_reporting = """Welcome back to SRCT Roomlist. This project is the
<a href="https://srct.gmu.edu/projects/">collaborative work
of students like you</a>. If you see anything amiss, or have ideas
for features or a better user experience, please email
<a href="mailto:roomlist@lists.srct.gmu.edu">
roomlist@lists.srct.gmu.edu</a>, tweet
<a href="https://twitter.com/MasonSRCT/">@MasonSRCT</a>, or, for the
more technically experienced, review our
<a href="https://git.gmu.edu/srct/roomlist/issues">issues page</a>."""
privacy_reminder = """Welcome back to SRCT Roomlist. A friendly reminder that you can
change your privacy settings at any time by clicking the cog in
the upper right of your screen."""
disclaimer = """Welcome back to SRCT Roomlist. This project was written for the benefit
of the Mason community by <a href="https://srct.gmu.edu/">SRCT</a>,
a <a href="https://gmu.collegiatelink.net/organization/srct">
registered student organization</a>. We are not affiliated with
<a href="http://housing.gmu.edu/">Mason Housing</a> and
all information is voluntarily provided by participating students."""
whatsopen_plug = """Welcome back to SRCT Roomlist. Wondering what's open at this hour?
Check out another one of our
<a href="https://srct.gmu.edu/projects/">student-built and hosted</a>
projects:
<a href="https://whatsopen.gmu.edu/">whatsopen.gmu.edu</a>."""
open_source = """Welcome back to SRCT Roomlist. For the curious at heart,
<a href="http://www.gnu.org/philosophy/free-sw.en.html">you can always
review</a> this project's
<a href="https://git.gmu.edu/srct/roomlist/tree/master">source code</a>.
Come <a href="https://srct.gmu.edu/calendar">to a meeting</a> and learn
how to contribute!"""
return_messages = [bug_reporting, privacy_reminder, disclaimer,
whatsopen_plug, open_source]
{% extends 'layouts/base.html' %}
{% block title %} SRCT RoomList | Student | {{ student.get_full_name_or_uname }} | Delete {% endblock %}
{% block message_queue %}
{% endblock %}
{% block content %}
<div class="page-header" id="banner">
<div class="row">
<div class="col-md-12 text-center">
<h1><strong>SRCT</strong>ROOMLIST</h1>
<p class="lead">Delete <strong>{{ student.get_full_name_or_uname }}</strong></p>
</div>
</div>
</div>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<p>
We're sorry to see you go, but we'd love if could take a couple of moments to give
us a little information about why you've decided to leave our service.
</p>
<p>
We'll use your response to guide development to make Roomlist better for everyone.
</p>
</div>
</div>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">
<h1 class="panel-title text-center"><strong>Departing Feedback</strong></h1>
</div>
<div class="panel-body">
{% if form.non_field_errors %}
<hr />
<ul class="text-center">
{% for error in form.non_field_errors %}
<li class="text-danger"><i class="fa fa-exclamation-triangle fa-fw"></i> {{ error }}</li>
{% endfor %}
</ul>
<hr />
{% endif %}
<form class="form-horizontal" autocomplete="off" action="" method="post">{% csrf_token %}
<fieldset>
{% if form.leaving.errors %}
{% for error in form.leaving.errors %}
<p class="text-danger"><i class="fa fa-exclamation-triangle fa-fw"></i> {{ error }}</p>
{% endfor %}
{% endif %}
<div class="form-group">
<label for="{{ form.leaving.id_for_label }}" class="col-md-5">
<strong>{{ form.leaving.label }}</strong> *
</label>
<div class="col-md-7">
{% for option in form.leaving %}
<label class="radio-inline">{{ option.choice_label }}{{ option.tag }}</label>
{% endfor %}
</div>
</div>
<hr/>
{% if form.feedback.errors %}
{% for error in form.feedback.errors %}
<p class="text-danger"><i class="fa fa-exclamation-triangle fa-fw"></i> {{ error }}</p>
{% endfor %}
{% endif %}
<div class="form-group">
<label for="{{ form.feedback.id_for_label }}" class="col-md-2">
<strong>{{ form.feedback.label }}</strong>
</label>
<div class="col-md-10">
{{ form.feedback }}
</div>
</div>
</fieldset>
</div>
</div>
<p class="text-muted"><small>* required</small></p>
</div>
</div>
<div class="row">
<div class="col-md-8 col-md-offset-2 text-center">
<a href="{{ student.get_absolute_url }}" style="margin:10px">
<button type="button" class="btn btn-default">Cancel</button>
</a>
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#deleteModal">
Delete Me
</button>
</div>
</div>
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">