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

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;}'
......@@ -16,12 +16,12 @@ Please visit the [SRCT Wiki](http://wiki.srct.gmu.edu/) for more information on
## Setting everything up for development
These instructions are for Ubuntu and Debian, or related Linux distributions. (If you'd like to help write the instructions for Mac OSX, please do!)
### Prerequisities
### Prerequisities and Package Installation
First, install python, pip, and git on your system. Python is the programming language used for Django, the web framework used by Roomlist. 'Pip' is the python package manager. Git is the version control system used for SRCT projects.
**Debian/Ubuntu**
Open a terminal and run the following commands.
`sudo apt-get update`
......@@ -32,23 +32,47 @@ This retrieves links to the most up-to-date and secure versions of your packages
you install python and git.
Now, we're going to clone down a copy of the Roomlist codebase from git.gmu.edu, the SRCT code respository.
Next, install these packages from the standard repositories
Configure your ssh keys by following the directions at git.gmu.edu/help/ssh/README.
`$ sudo apt-get install libldap2-dev mysql-server mysql-client libmysqlclient-dev python-mysqldb libsasl2-dev libjpeg-dev redis-server`
Now, on your computer, navigate to the directory in which you want to download the project (perhaps one called development/ or something similar), and run
If prompted to install additional required packages, install those as well.
`git clone git@git.gmu.edu:srct/roomlist.git`
When prompted to set your mysql password, it's advisable to set it as the same as your normal superuser password.
### Package Installation
Now you're ready to set up the Roomlist repository on your machine.
Next, install these packages from the standard repositories
**macOS (Formerly OS X)**
`$ sudo apt-get install libldap2-dev mysql-server mysql-client libmysqlclient-dev python-mysqldb libsasl2-dev libjpeg-dev redis-server`
This tutorial uses the third party Homebrew package manager for macOS, which allows you to install
packages from your terminal just as easily as you could on a Linux based system. You could use another
package manager (or not use one at all), but Homebrew is highly reccomended.
If prompted to install additional required packages, install those as well.
In order to use homebrew, you must first install XCode Command Line Tools. For users of OS X Mavericks and all newer
versions, this can be done by running ``xcode-select --install`` and clicking "Install" on the popup that appears.
When prompted to set your mysql password, it's advisable to set it as the same as your normal superuser password.
On older versions of OS X, you should simply install the entirety of XCode, which can be found online.
To get homebrew, run the following command in a terminal:
``/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"`
**Note**: You do NOT need to use `sudo` when running any Homebrew commands, and it likely won't work if you do.
Now you want to Python, pip, git, and MySQL (macOS actually ships with some of these, but we want to have the latest versions). We'll also install Redis, though this package is only relevant when testing the production environment. To do so, run the following command in your terminal:
`brew install python git mysql redis`
Now you're ready to set up the Roomlist repository on your machine.
#### Git Setup
Now, we're going to clone down a copy of the Roomlist codebase from git.gmu.edu, the SRCT code respository.
Configure your ssh keys by following the directions at git.gmu.edu/help/ssh/README.
Now, on your computer, navigate to the directory in which you want to download the project (perhaps one called development/ or something similar), and run
`git clone git@git.gmu.edu:srct/roomlist.git`
### The Virtual Environment
......@@ -87,7 +111,7 @@ Load up the mysql shell by running
``mysql -u root -p``
and putting in your mysql password.
and putting in your mysql password (on MacOS, this will default to an empty string, so you can just press return).
Create the database by running
......@@ -103,7 +127,7 @@ Though you can use an existing user to access this database, here's how to creat
For local development, password strength is less important, but use a strong passphrase for deployment. You can choose a different username.
``GRANT ALL ON roomlist.* TO 'roommate'@'localhost';``
This allows your database user to create all the tables it needs on the bookshare database. (Each model in each app's models.py is a separate table, and each attribute a column, and each instance a row.)
This allows your database user to create all the tables it needs on the roomlist database. (Each model in each app's models.py is a separate table, and each attribute a column, and each instance a row.)
``GRANT ALL ON test_roomlist.* TO 'roommate'@'localhost';`` ``FLUSH PRIVILEGES;``
When running test cases, django creates a test database so your 'real' database doesn't get screwed up. This database is called 'test_' + whatever your normal database is named. Note that for permissions it doesn't matter that this database hasn't yet been created.
......@@ -112,23 +136,21 @@ The .\* is to grant access all tables in the database, and 'flush privileges' re
Exit the mysql shell by typing `exit`.
Now, to configure your newly created database with the project settings, and set up your project's cryptographic key, copy the secret.py.template in settings/ to secret.py. Follow the comment instructions provided in each file to set your secret key and database info.
Also copy config.py.template to config.py. You won't need to make any changes here off the bat. See more information about this file under the 'Deployment' section.
Now, to configure your newly created database with the project settings, and set up your project's cryptographic key, **copy the `secret.py.template` in settings/ to `secret.py`**. Follow the comment instructions provided in each file to set your secret key and database info. `secret.py` also contains a few additional passwords for email and for Slack. The Slack API key will not be necessary unless more 50 people sign into your development instance. The email password is unnecessary in development, because the email settings are configured not to send emails via a server, but to print them out to the terminal wherever you're running `manage.py`.
Run `python manage.py makemigrations` to create the tables and rows and columns for your database. This command generates sql code based on your database models. If you don't see output noting the creation of a number of models, add the app name to the end of the command, e.g. `python manage.py makemigrations housing`.
Run `python manage.py makemigrations` to create the tables and rows and columns for your database. This command generates sql code based on your database models. If you don't see output noting the creation of a number of models, add the app name to the end of the command, e.g. `python manage.py makemigrations housing` and then `python manage.py makemigrations accounts`.
Then run `python manage.py migrate` to execute that sql code and set up your database. Migrations also track how you've changed your models over the life of your database, which allows you to make changes to your tables without screwing up existing information.
Finally, run `python manage.py createsuperuser` to create an admin account, using the same username and email as you'll access through CAS. This means your 'full' email address, for instance gmason@masonlive.gmu.edu. Your password will be handled through CAS, so you can just use 'password' here.
Finally, run `python manage.py createsuperuser` to create an admin account, using the same username and email as you'll access through CAS. This means your 'full' email address, for instance *gmason@masonlive.gmu.edu*. Your password will be handled through CAS, so you can just use 'password' here.
(If you accidentally skip this step, you can run `python manage.py shell` and edit your user from there. Selectyour user, and set .is_staff and .is_superuser to True, then save.)
## Loading in demo data
## Loading in initial data
The project includes a json file with demo data to load into the database for majors. Run `python manage.py loaddata accounts/major_fixtures.json`. You'll see output saying 'Installed 79 objects from 1 fixture(s) if all goes smoothly.
The project includes a json file to load majors into the database. Run `python manage.py loaddata accounts/major_fixtures.json`. You'll see output saying 'Installed 79 objects from 1 fixture(s) if all goes smoothly.
To update all of the rooms, halls, and floors, you must execute a command to populate them in order for it to work properly. Run `python manage.py shell < housing/housing_obj_creator.py`. You should see the progress creating the housing objects printed in your terminal.
To add all supported housing, with the virtual environment enabled, run `python manage.py shell < housing/housing_obj_creator.py`. It will take a couple of minutes, but this script will create every building, floor, and room in your database.
## Starting up the test server
......@@ -210,9 +232,45 @@ If you've forgotten the name with the number, back on Google's page it's in the
Add localhost as the site, click save, and throw a party, because thank goodness, you're finally all set.
### Notes on Cacheing
### Github
Github's auth setup is mercifully comparatively easy. Sign in and go to https://github.com/settings/applications/new. (Note if you're creating a token for an organization, you'll need to instead go to
https://github.com/organizations/srct/settings/applications/new/. App names do not need to be universally unique. Set http://localhost:8000 as your Homepage URL. Your description will be shown to your users; write something like 'Verify your Github account with Roomlist!'. For the authorization callback, use http://localhost:8000/accounts/github/login/callback/. Note your Client ID and Client Secret in the refreshed page, and add a new social application. Copy everything over directly from your just configured Github OAuth Application page, and add localhost as your chosen site.
### Tumblr
Head to https://www.tumblr.com/settings/apps. The page is predominantly about Tumblr's mobile apps, but there's a faint gray line of text at the bottom. 'Wanna make an app? Cool. Register to use the Tumblr API, then have at it.' You'll need to verify your email address with Tumblr before continuing.
Click the '+ Register application' button, and then you'll have another page ahead of you to fill of OAuth information.
A couple of notes: the Application Name is not universal. Use the application description you've been using throughout, 'Verify your Tumblr account with Roomlist!'. The administrative contact email will be your Tumblr default account email. Set the callback url as http://localhost:8000/accounts/tumblr/login/callback/. Your application page icons cannot have transparent background. All right, you're ready to register!
On the Applications page that you'll be redirected to, the 'OAuth Consumer Key' is the Client ID you'll need when you add a new Social Application. Click 'Show Secret Key' to get the Client Secret. Use the name you gave your app, add localhost for your site, and you're off to the races.
Tumblr doesn't seem to have a way to only request specific permissions-- it will ask if it's okay both to access information and to post on your behalf. We're not interested in the latter, but keep in mind it will ask users if that's okay.
### Pinterest
Stop on over at https://developers.pinterest.com/apps/. If this is the first time you're playing with their API, you'll be asked to accept their terms of service. This then pops a modal to name your app and write a description. Note that while you may name your app anything (it does not have to be universally unique) it cannot be changed later. For the description, hit the standard: 'Verify your Pinterest account with Roomlist!'.
Pinterest has a fascinating approval process. When you get redirected to your app's page, you'll see 'You're almost ready! You still need at least 1 collaborator to authorize your app before you can submit.' That's right, just like an elementary school field trip to a pool, app approval is a buddy process. You and your buddy must follow each other. Thus, for deployment you must have a friend, but this step is unnecessary for development.
Pinterest has the unique requirement additionally of requiring that redirect URIs use https. We don't have certs for dev, so just go ahead and drop in an 's' to https://localhost:8000/accounts/pinterest/login/callback/. If you're going to change the picture that shows up when users connect your account, you'll need to hover over the large image at the top next to your app's name. There's no button otherwise.
Now, you're not going to be able to test this in development. You can get close by adding the `ACCOUNT_DEFAULT_HTTP_PROTOCOL` to https in `settings.py` like it is in `production.py`, and you'll be able to see most of the process. But for the final redirect, your browser will tell you something like 'This site can’t provide a secure connection: localhost sent an invalid response. ERR_SSL_PROTOCOL_ERROR', and Django will print you out 'You're accessing the development server over HTTPS, but it only supports HTTP.'. This is lame, but trust that if you get to that point it's working.
Add in your site information, and click on the button to reveal the 'App Secret'. Copy that and the 'App ID' into 'Client id' and 'Secret Key' in the Django admin interface when adding a new Pinterst app. Move over your chosen sites, and strap in, because it's still a while to go.
You have to submit your app for approval as detailed here in their documentation https://developers.pinterest.com/docs/api/overview/. As they state, be as descriptive as possible when putting in your request.
Phew. You're finished. Get a stiff drink.
### Spotify
Visit https://developer.spotify.com/my-applications. You'll land on a big splash page with a green login button. A window will pop up, and after you've put in your username and password, it will ask to "Connect Spotify Developer to your Spotify account." Hit "Okay" and accept the Terms of Service.
The next page will be for creating your application. The app name does not have to be universally unique. User 'Verify your Spotify account with Roomlist!' as the app description. The next page will ask for your site's location and the callback url. You **must** include the trailing slash, so http://localhost:8000/accounts/spotify/login/callback/. This page already provides you the Client ID and Client Secret. Copy these to the Client ID and Secret Key when adding an app in the django admin interface, and add your site to the chosen sites.
Roomlist's urls are set to be cached for periods of time set so that ordinary user experience will not be impacted, but a substantial load will be lifted from a deployment server. However, this can be annoying when you're making and want to check small changes rapidly on development. You can edit the respective apps' urls.py files and remove the cacheing configurations, but make sure that you do not include such edits in any pushes!
Thankfully, there isn't any scope handling we need to deal with-- by default, we are limited to 'Read your publicly available information'.
## Modifying search
......@@ -220,7 +278,7 @@ If you make changes to the search indexes, you will need to re-indexing your mod
## Deployment
A number of deployment-related settings have been moved from settings.py to config.py in settings/ for ease of use. Make sure to never have DEBUG mode on when running your project in deployment.
Configure and use `production.py` instead of `settings.py` in the production environment, to turn on caching, turn off debug mode, set up email, support https, and a whole lot more. To use this settings file, pass an additional argmument when running the project. `python manage.py runserver --settings=settings.production`
### Docker
......
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,