Commit 9b4606f8 authored by Daniel W Bond's avatar Daniel W Bond
Browse files

Merge branch 'blocking' into amherst

parents ac9810e0 e7ad0471
......@@ -16,8 +16,6 @@ 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 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.
......@@ -135,8 +133,6 @@ 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.
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`.
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.
......@@ -151,10 +147,6 @@ The project includes a json file to load majors into the database. Run `python m
To add all freshman 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 search
To start off indexing your models for searching, run `python manage.py update_index`.
## Starting up the test server
With your virtual environment active, run
......@@ -258,7 +250,7 @@ Roomlist's urls are set to be cached for periods of time set so that ordinary us
## 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
......
......@@ -80,6 +80,10 @@ class StudentUpdateForm(forms.Form):
room = SelectRoomField(queryset=Room.objects.all(), required=False)
privacy = forms.TypedChoiceField(choices=Student.PRIVACY_CHOICES)
# 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()
......
......@@ -164,6 +164,7 @@ class Student(TimeStampedModel):
show_gender = models.BooleanField(default=False)
privacy = models.CharField(max_length=100, choices=PRIVACY_CHOICES, default=FLOOR)
blocked_kids = models.ManyToManyField("self", blank=True)
on_campus = models.BooleanField(default=True)
room = models.ForeignKey(Room, null=True, blank=True)
......@@ -292,10 +293,10 @@ class Student(TimeStampedModel):
ordering = ['user']
def __str__(self): # __unicode__ on Python 2
return self.user.username
return self.get_full_name_or_uname()
def __unicode__(self):
return unicode(self.user.username)
return unicode(self.get_full_name_or_uname())
# uncomment if there's something going awry while saving
# def save(self, *args, **kwargs):
......
......@@ -26,6 +26,7 @@
<legend><a name="{{ building.grouper }}" href="{{ building.grouper.get_absolute_url }}">{{ building.grouper }}</a></legend>
<div class="row">
{% for student in building.list %}
{% if not student in request.user.student.blocked_kids.all %}
<div class="col-sm-3 text-center">
<img class="img-circle img-responsive center center-block" src="{{ student.profile_image_url }}" alt="{{ student.get_first_name_or_uname }} profile picture">
<h4><a href="{{ student.get_absolute_url }}"><strong>{{ student.get_full_name_or_uname }}</strong></a></h4>
......@@ -34,6 +35,7 @@
<p><em>* a number of other floormates say this info is incorrect</em></p>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
{% empty %}
......@@ -53,6 +55,7 @@
<legend><a name="{{ building.grouper }}" href="{{ building.grouper.get_absolute_url }}">{{ building.grouper }}</a></legend>
<div class="row">
{% for student in building.list %}
{% if not student in request.user.student.blocked_kids.all %}
<div class="col-sm-3 text-center">
<img class="img-circle img-responsive center center-block" src="{{ student.profile_image_url }}" alt="{{ student.get_first_name_or_uname }} profile picture">
<h4><a href="{{ student.get_absolute_url }}"><strong>{{ student.get_full_name_or_uname }}</strong></a></h4>
......@@ -61,6 +64,7 @@
<p><em>* a number of other floormates say this info is incorrect</em></p>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
{% empty %}
......@@ -80,6 +84,7 @@
<legend><a name="{{ building.grouper }}" href="{{ building.grouper.get_absolute_url }}">{{ building.grouper }}</a></legend>
<div class="row">
{% for student in building.list %}
{% if not student in request.user.student.blocked_kids.all %}
<div class="col-sm-3 text-center">
<img class="img-circle img-responsive center center-block" src="{{ student.profile_image_url }}" alt="{{ student.get_first_name_or_uname }} profile picture">
<h4><a href="{{ student.get_absolute_url }}"><strong>{{ student.get_full_name_or_uname }}</strong></a></h4>
......@@ -88,6 +93,7 @@
<p><em>* a number of other floormates say this info is incorrect</em></p>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
{% empty %}
......@@ -103,11 +109,13 @@
<div class="row">
<div class="col-md-12">
{% for student in location_hidden %}
{% if not student in request.user.student.blocked_kids.all %}
<div class="col-sm-3 text-center">
<img class="img-circle img-responsive center center-block" src="{{ student.profile_image_url }}" alt="{{ student.get_first_name_or_uname }} profile picture">
<h4><a href="{{ student.get_absolute_url }}"><strong>{{ student.get_full_name_or_uname }}</strong></a></h4>
<h5>{{ student.graduating_year }}</h5>
</div>
{% endif %}
{% endfor %}
</div>
</div>
......
......@@ -17,7 +17,7 @@
{% include 'no_auth.html' %}
{% load cache %}
{% cache 900 list_majors %}
{% cache 3600 list_majors %}
<div class="page-header" id="banner">
<div class="row">
......@@ -40,13 +40,20 @@
</div>
<div class="row">
<div class="col-md-10 col-md-offset-1">
<div class="col-sm-10 col-sm-offset-1">
{% for letter in majors_by_letter %}
<legend><a name="{{ letter.grouper }}" href="#{{ letter.grouper }}">{{ letter.grouper }}</a></legend>
<div class="row">
{% for major in letter.list %}
<div class="col-md-6 text-center">
<a href="{{ major.get_absolute_url }}" class="btn btn-primary btn-lg btn-block">{{ major.name }}</a>
{% if major.name|length > 39 %}
<a href="{{ major.get_absolute_url }}" class="btn btn-primary btn-lg btn-block">
<small>{{ major.name }}</small></a>
{% else %}
<a href="{{ major.get_absolute_url }}" class="btn btn-primary btn-lg btn-block">
{{ major.name }}
</a>
{% endif %}
</div>
{% if forloop.counter|divisibleby:2 %}
</div>
......
......@@ -41,6 +41,7 @@ SRCT Roomlist &bull; Search Students
{% endif %}
{% for result in page.object_list %}
{% if not result.object.user.student in request.user.student.blocked_kids.all %}
<tr>
<td>
<h4>{{ result.object.user.username }}</h4>
......@@ -61,6 +62,7 @@ SRCT Roomlist &bull; Search Students
{% endif %}
</td>
</tr>
{% endif %}
{% empty %}
<h3 class="text-center">No results found.</h3>
{% endfor %}
......
......@@ -22,19 +22,11 @@
<div class="page-header" id="banner">
<div class="row">
<div class="col-md-12 text-center">
<h1><strong>SRCT</strong>ROOMLIST</a></strong> Student Update</h1>
<h1><strong>SRCT</strong>ROOMLIST</a></strong> {{ request.user.student.get_full_name_or_uname }}'s Settings</h1>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 text-center">
<h2><strong>Welcome, {{ request.user.student.get_first_name_or_uname }}!</strong></h2>
</div>
</div>
<br />
{% load socialaccount %}
{% get_social_accounts request.user as accounts %}
......@@ -143,7 +135,7 @@
<div class="panel panel-default">
<div class="panel-heading">
<h1 class="panel-title text-center"><strong>Student Settings</strong></h1>
<h1 class="panel-title text-center"><strong>Profile Settings</strong></h1>
</div>
<div class="panel-body">
......@@ -254,6 +246,18 @@
{{ my_form.privacy }}
</div>
</div>
<hr />
<p class="help-block">
You will not be able to see these students, and they will not be able to see you.
</p>
<div class="form-group">
<label for="{{ my_form.blocked_kids.id_for_label }}" class="col-md-2">
Blocked Students
</label>
<div class="col-md-10">
{{ my_form.blocked_kids }}
</div>
</div>
{% endif %}
<hr />
<input type="submit" value="Save" class="btn btn-primary"/>
......@@ -275,6 +279,8 @@
<script>
$(".chosen-select").chosen({max_selected_options: 2,
placeholder_text_multiple: '(select up to two)'});
$(".blocked-select").chosen({width: "100%",
placeholder_text_multiple: "(search by name)"});
</script>
{% include 'room_selection_script.html' %}
{% endblock javascript %}
......@@ -5,7 +5,7 @@ from distutils.util import strtobool
from operator import attrgetter
from itertools import chain
# core django imports
from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.http import HttpResponseForbidden, HttpResponseRedirect, Http404
from django.views.generic import CreateView, ListView, DetailView, FormView, DeleteView
from django.core.urlresolvers import reverse
from django.contrib import messages
......@@ -79,6 +79,17 @@ class DetailStudent(LoginRequiredMixin, DetailView):
login_url = 'login'
def get(self, request, *args, **kwargs):
current_url = self.request.get_full_path()
url_uname = current_url.split('/')[3]
detailed_student = Student.objects.get(user__username=url_uname)
if (detailed_student in self.request.user.student.blocked_kids.all()):
raise Http404
else:
return super(DetailStudent, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(DetailStudent, self).get_context_data(**kwargs)
......@@ -162,10 +173,13 @@ class UpdateStudent(LoginRequiredMixin, FormValidMessageMixin, FormView):
'show_gender': me.show_gender,
'room': pk_or_none(me, me.room),
'privacy': me.privacy,
'blocked_kids': me.blocked_kids.all(),
'major': majors,
'graduating_year': me.graduating_year,
'on_campus': me.on_campus, })
form.fields['blocked_kids'].queryset = Student.objects.exclude(user=self.request.user)
if me.recent_changes() > 2:
form.fields['room'].widget = HiddenInput()
form.fields['privacy'].widget = HiddenInput()
......@@ -180,6 +194,7 @@ class UpdateStudent(LoginRequiredMixin, FormValidMessageMixin, FormView):
# chosen
form.fields['major'].widget.attrs['class'] = 'chosen-select'
form.fields['blocked_kids'].widget.attrs['class'] = 'blocked-select'
context['my_form'] = form
......@@ -249,13 +264,28 @@ class UpdateStudent(LoginRequiredMixin, FormValidMessageMixin, FormView):
# don't change majors
pass
# replicate the same thing for the other m2m field
try:
form_blocked_pks = set(form.data.getlist('blocked_kids'))
current_blocked = me.blocked_kids.all()
# most people will not being blocking other students
if form_blocked_pks:
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)
except:
pass
me.user.first_name = form.data['first_name']
me.user.last_name = form.data['last_name']
me.gender = form.data.getlist('gender')
me.show_gender = strtobool(form.data.get('show_gender', 'False'))
me.privacy = form.data['privacy']
me.graduating_year = form.data['graduating_year']
me.user.save()
me.save()
......@@ -361,6 +391,10 @@ class CreateConfirmation(LoginRequiredMixin, CreateView):
if flags >= 1:
return HttpResponseForbidden()
# you can't see the page if the person has banned you
if confirmer in student.blocked_kids.all():
raise Http404
return super(CreateConfirmation, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
......
......@@ -16,7 +16,7 @@
{% include 'no_auth.html' %}
{% load cache %}
{% cache 900 detail_building building %}
{% cache 3600 detail_building building %}
<div class="page-header" id="banner">
<div class="row">
......
......@@ -17,7 +17,7 @@
{% include 'no_auth.html' %}
{% load cache %}
{% cache 900 list_buildings %}
{% cache 3600 list_buildings %}
<div class="page-header" id="banner">
<div class="row">
......
......@@ -9,6 +9,21 @@ from .models import Building, Floor, Room
from accounts.models import Student
# this should be written in cache, to be entirely honest
def shadowbanning(me, other_people):
# start with only students who are actually blocking anyone
blockers = [student for student in Student.objects.exclude(blocked_kids=None)]
# of those students, collect the ones that block *you*
blocks_me = [student
for student in blockers
if me in student.blocked_kids.all()]
if blocks_me: # python implicit truth evaluation
student_safety = list(set(other_people) - set(blocks_me))
return student_safety
else:
return other_people
# a list of neighborhoods and their buildings
class ListBuildings(ListView):
model = Building
......@@ -68,7 +83,10 @@ class DetailFloor(LoginRequiredMixin, DetailView):
requesting_student = Student.objects.get(user=self.request.user)
context['students'] = Student.objects.visible(requesting_student, self.get_object())
students = Student.objects.visible(requesting_student, self.get_object())
context['students'] = shadowbanning(requesting_student, students)
# boolean values; helps cut down on if/else block complexity on the template
context['notOnFloor'] = not(requesting_student in self.get_object())
context['notInBuilding'] = not(requesting_student in self.get_object().building)
return context
......@@ -99,8 +117,9 @@ class DetailRoom(LoginRequiredMixin, DetailView):
requesting_student = Student.objects.get(user=self.request.user)
context['students'] = Student.objects.visible(requesting_student, self.get_object())
context['notOnFloor'] = not(requesting_student in self.get_object().floor)
context['notInBuilding'] = not(requesting_student in self.get_object().floor.building)
students = Student.objects.visible(requesting_student, self.get_object())
context['students'] = shadowbanning(requesting_student, students)
context['notOnFloor'] = not(requesting_student in self.get_object())
context['notInBuilding'] = not(requesting_student in self.get_object().building)
return context
# installation configurations
# copy to config.py
# These configurations are set by default for a local development environment. Turning
# off debug mode will display 404 and 500 error pages instead of detailed logs.
# Don't run with debug turned on in production!!!
DEBUG = True
TEMPLATE_DEBUG = True
# the domains this application will be deployed on, e.g. which
# domains this app should listen to requests from.
ALLOWED_HOSTS = ['127.0.0.1']
# configure piwik analytics
# point to the piwik url
PIWIK_DOMAIN_PATH = 'piwik.example.com'
# set piwik server site id (piwik can track multiple websites)
PIWIK_SITE_ID = '4'
# If you want to change the database engine you are free to do so here.
DB_ENGINE = 'django.db.backends.mysql'
# Admin Email Handling settings
ADMINS = (('Roomlist Devs', 'roomlist@lists.srct.gmu.edu'),
('SRCT Execs', 'srct@gmu.edu'), )
SERVER_EMAIL = 'root@localhost'
EMAIL_HOST = 'localhost'
EMAIL_PORT = 25
EMAIL_HOST_USER = ''
EMAIL_HOST_PASSWORD = ''
EMAIL_USE_SSL = False
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
# override all the things for the production environment
from __future__ import absolute_import
import os
from .settings import *
from . import secret
DEBUG = False
TiEMPLATE_DEBUG = False
ALLOWED_HOSTS = ['127.0.0.1']
PIWIK_DOMAIN_PATH = 'https://piwik.srct.gmu.edu/'
PIWIK_SITE_ID = 4
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql'
'NAME': ''
'USER': ''
'PASSWORD': secret.DB_PASSWORD
'HOST': ''
'PORT': ''
}
}
# example.com is 1
# roomlist.srct.gmu.edu is 2
# roomlist.gmu.edu is 3
SITE_ID = 3
# where the static files are being hosted from
STATIC_ROOT = '/srv/roomlist/static'
# can haz ssl certs
ACCOUNT_DEFAULT_HTTP_PROTOCOL = 'https'
CACHES = {
'default': {
'BACKEND': 'redis_cache.RedisCache',
# configure to redis port
'LOCATION': 'localhost:6379',
},
}
# need to configure ADMINS email so that ERROR level logs (500 level exceptions)
# can be sent out
# https://docs.djangoproject.com/en/1.7/topics/logging/
# https://docs.djangoproject.com/en/1.7/howto/error-reporting/
# https://docs.python.org/2/library/logging.config.html#configuration-dictionary-schema
ADMINS = (('Roomlist Devs', 'roomlist@lists.srct.gmu.edu'),
('SRCT Execs', 'srct@gmu.edu'),)
SERVER_EMAIL = ''
EMAIL_HOST = 'localhost'
EMAIL_POST = 25
EMAIL_HOST_USER = ''
EMAIL_HOST_PASSWORD = secret.EMAIL_HOST_PASSWORD
EMAIL_USE_SSL = False
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'DEBUG', # will log all errors
'class': 'logging.FileHandler',
# make sure to change this to the proper path, and one that
# can be written to
'filename': '/path/to/django/debug.log',
},
# 'mail_admins' by default does not include a traceback attachment
# setting 'include_html' to True will attach an html traceback file to the email
# you can also set an additional 'email_backend' arg to a custom email handler (e.g. SES)
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
'include_html': True,
},
},
# logs request errors
'loggers': {
'django.request': {
'handlers': ['file'],
'level': 'DEBUG',
'propagate': True,
},
# django's default loggers send request and security messages at the ERROR
# or CRITICAL level to the AdminEmailHandler via mail_admins
},
}
......@@ -3,17 +3,14 @@
# used for django's cryptographic signing, 50 characters long
# you can generate one here http://www.miniwebtool.com/django-secret-key-generator/
SECRET_KEY = ''
# These configurations are partially set by default for a local development environment.
# Remember to use a strong passphrase for deployment. Use the username and password you
# configured creating the database while following the README. Change the database name,
# host, or port as necessary for deployment.
DB_NAME = 'roomlist'
DB_USER = ''
# the password that you set when setting up the database
DB_PASSWORD = ''
DB_HOST = 'localhost'
# often left blank
DB_PORT = ''
# the password to send emails-- required only for production
EMAIL_HOST_PASSWORD = ''
# the key to post information to #roomlist via the Slack API
SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/xxxxxxxxx/xxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxx'
# don't worry-- it gracefully times out if you don't have the key
......@@ -8,32 +8,46 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.7/ref/settings/
"""
# standard library imports
from __future__ import absolute_import, print_function
import os
# core django imports
from django.contrib.messages import constants as messages
# imports from your apps
from . import secret
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
ORGANIZATION_EMAIL_DOMAIN = 'masonlive.gmu.edu'
# cryptography
SECRET_KEY = secret.SECRET_KEY
TEMPLATE_DIRS = (
os.path.join(BASE_DIR, 'templates'),
os.path.join(BASE_DIR, 'housing/templates'),
os.path.join(BASE_DIR, 'accounts/templates'),
)
# These configurations are set by default for a local development environment. Turning
# off debug mode will display 404 and 500 error pages instead of detailed logs.
DEBUG = True
TEMPLATE_DEBUG = True
# the domains this application will be deployed on, e.g. which
# domains this app should listen to requests from.
ALLOWED_HOSTS = ['127.0.0.1']
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
)
# where template files are located
TEMPLATE_DIRS = (
os.path.join(BASE_DIR, 'templates'),
# may specify to avoid requiring paths
os.path.join(BASE_DIR, 'housing/templates'),
os.path.join(BASE_DIR, 'accounts/templates'),
)
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)
TEMPLATE_CONTEXT_PROCESSORS = (
'django.contrib.auth.context_processors.auth',
'django.core.context_processors.debug',
......@@ -45,19 +59,8 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'allauth.account.context_processors.account',
'allauth.socialaccount.context_processors.socialaccount',
)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
from . import config
DEBUG = config.DEBUG
TEMPLATE_DEBUG = config.TEMPLATE_DEBUG
ALLOWED_HOSTS = config.ALLOWED_HOSTS
PIWIK_DOMAIN_PATH = config.PIWIK_DOMAIN_PATH
PIWIK_SITE_ID = config.PIWIK_SITE_ID
# Application definition
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
......@@ -108,54 +111,42 @@ MIDDLEWARE_CLASSES = (
'django.middleware.clickjacking.XFrameOptionsMiddleware',
)