Initial commit, converting laundry app to site

This commit is contained in:
Tyler Hallada 2014-07-28 17:24:14 -04:00
commit 2e2ee19a6b
14 changed files with 533 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
*~
*.log
*.pot
*.py[cod]
*.so
*.db
*.DS_Store
*.swp
*.sw[on]
venv
apache
old
*db
laundry/secrets.py
static
*.svg

1
laundry.json Normal file

File diff suppressed because one or more lines are too long

0
laundry/__init__.py Normal file
View File

166
laundry/settings.py Normal file
View File

@ -0,0 +1,166 @@
# Django settings for laundry project.
import os
import secrets
DEBUG = True
TEMPLATE_DEBUG = DEBUG
ADMINS = (
('thallada', 'thallada@mit.edu'),
('tyler', 'tyler@hallada.net'),
)
MANAGERS = ADMINS
PROJECT_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..')
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': secrets.DATABASE_NAME,
'USER': secrets.DATABASE_USER,
'PASSWORD': secrets.DATABASE_PASSWORD,
'HOST': secrets.DATABASE_HOST,
'PORT': '', # Set to empty string for default. Not used with sqlite3.
}
}
# Hosts/domain names that are valid for this site; required if DEBUG is False
# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
ALLOWED_HOSTS = []
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# In a Windows environment this must be set to your system time zone.
TIME_ZONE = 'America/New_York'
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-us'
SITE_ID = 1
# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True
# If you set this to False, Django will not format dates, numbers and
# calendars according to the current locale.
USE_L10N = True
# If you set this to False, Django will not use timezone-aware datetimes.
USE_TZ = True
# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/var/www/example.com/media/"
MEDIA_ROOT = ''
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash.
# Examples: "http://example.com/media/", "http://media.example.com/"
MEDIA_URL = ''
# Absolute path to the directory static files should be collected to.
# Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/var/www/example.com/static/"
STATIC_ROOT = os.path.join(PROJECT_PATH, 'static')
# URL prefix for static files.
# Example: "http://example.com/static/", "http://static.example.com/"
STATIC_URL = '/static/'
# Additional locations of static files
STATICFILES_DIRS = (
# Put strings here, like "/home/html/static" or "C:/www/django/static".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
os.path.join(PROJECT_PATH, 'laundry_app', 'static'),
)
# List of finder classes that know how to find static files in
# various locations.
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
)
# Make this unique, and don't share it with anybody.
SECRET_KEY = secrets.SECRET_KEY
# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
# 'django.template.loaders.eggs.Loader',
)
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
# Uncomment the next line for simple clickjacking protection:
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
ROOT_URLCONF = 'laundry.urls'
# Python dotted path to the WSGI application used by Django's runserver.
WSGI_APPLICATION = 'laundry.wsgi.application'
TEMPLATE_DIRS = (
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
os.path.join(PROJECT_PATH, 'templates'),
)
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
# Uncomment the next line to enable the admin:
# 'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
'laundry_app',
'south',
)
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer'
# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
# the site admins on every HTTP 500 error when DEBUG=False.
# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse'
}
},
'handlers': {
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': {
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': True,
},
}
}

18
laundry/urls.py Normal file
View File

@ -0,0 +1,18 @@
from django.conf.urls import patterns, include, url
# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
# Examples:
# url(r'^$', 'laundry.views.home', name='home'),
# url(r'^laundry/', include('laundry.foo.urls')),
# Uncomment the admin/doc line below to enable admin documentation:
# url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
# Uncomment the next line to enable the admin:
# url(r'^admin/', include(admin.site.urls)),
url(r'', include('laundry_app.urls')),
)

32
laundry/wsgi.py Normal file
View File

@ -0,0 +1,32 @@
"""
WSGI config for laundry project.
This module contains the WSGI application used by Django's development server
and any production WSGI deployments. It should expose a module-level variable
named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
this application via the ``WSGI_APPLICATION`` setting.
Usually you will have the standard Django WSGI application here, but it also
might make sense to replace the whole Django WSGI application with a custom one
that later delegates to the Django one. For example, you could introduce WSGI
middleware here, or combine a Django application with an application of another
framework.
"""
import os
# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
# if running multiple sites in the same mod_wsgi process. To fix this, use
# mod_wsgi daemon mode with each site in its own daemon process, or use
# os.environ["DJANGO_SETTINGS_MODULE"] = "laundry.settings"
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "laundry.settings")
# This application object is used by any WSGI server configured to use this
# file. This includes Django's development server, if the WSGI_APPLICATION
# setting points here.
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
# Apply WSGI middleware here.
# from helloworld.wsgi import HelloWorldApplication
# application = HelloWorldApplication(application)

0
laundry_app/__init__.py Normal file
View File

120
laundry_app/laundry.py Normal file
View File

@ -0,0 +1,120 @@
from BeautifulSoup import BeautifulSoup
from urllib2 import urlopen
import pygal
from pygal.style import Style
from models import LaundryMachine, LaundryRecord, Timeslot, LaundrySummary
import datetime
WASHER = LaundryMachine.WASHER
DRYER = LaundryMachine.DRYER
AVAILABLE = LaundryRecord.AVAILABLE
IN_USE = LaundryRecord.IN_USE
CYCLE_COMPLETE = LaundryRecord.CYCLE_COMPLETE
UNAVAILABLE = LaundryRecord.UNAVAILABLE
BASE_URL_QUERY = 'http://gmu.esuds.net/RoomStatus/machineStatus.i?bottomLocationId='
def load_data(hall):
"""Extract table data from esuds for specified hall"""
htsl = urlopen(BASE_URL_QUERY+str(int(hall.location_id))).read()
soup = BeautifulSoup(htsl) # Start cook'n
rows = [row for row in soup.findAll('tr')[1:] if len(row('td')) > 1]
return rows
def get_num_machines_per_status(status, records):
"""
Count the number of machines in a hall which have the desired status and
return a list that can be inputed to the pygal add series function.
Returns a list of two ints. The first element is the number of washers
with the desired status and the second is the number of dryers.
"""
return [len(filter(lambda r: r.machine.type == WASHER and
r.availability == status, records)),
len(filter(lambda r: r.machine.type == DRYER and
r.availability == status, records))]
def generate_current_chart(filepath, records, hall):
"""
Generate stacked bar chart of current laundry usage for specified hall and
save svg at filepath.
"""
custom_style = Style(colors=('#B6E354', '#FF5995', '#FEED6C', '#E41B17'))
chart = pygal.StackedBar(style=custom_style, width=800, height=512, explicit_size=True)
chart.title = 'Current laundry machine usage in ' + hall.name
chart.x_labels = ['Washers', 'Dryers']
chart.add('Available', get_num_machines_per_status(AVAILABLE, records))
chart.add('In Use', get_num_machines_per_status(IN_USE, records))
chart.add('Cycle Complete', get_num_machines_per_status(CYCLE_COMPLETE, records))
chart.add('Unavailable', get_num_machines_per_status(UNAVAILABLE, records))
chart.range = [0, 11]
chart.render_to_file(filepath)
# NOTE: Abandoning generating the weekly chart via mysql and Django for now.
# (cron script and csv file is just easier) Sorry if there are a lot of
# skeletons lying around as a result of this abandonment.
#def generate_weekly_chart(filepath, hall):
# First, add a new LaundrySummary for the most recent record.
# TODO: determine if this step actually needs to be done or not.
#records = list()
#for machine in hall.machines:
#records.append(machine.get_latest_record())
#ts = records[0].timeslot
#ts = ts - datetime.timedelta(minutes=ts.minute % 15,
#seconds=ts.second, microseconds=ts.microsecond)
#day = ts.weekday()
#slot = Timeslot.objects.get_or_create(hall=hall, time=ts.time(), day=day)
#LaundrySummary.objects.create(timeslot=slot,
#washers = len([w for w in records if
#(w.machine.type == LaundryMachine.WASHER and
#w.availability == LaundryRecord.AVAILABLE)]),
#dryers = len([w for w in records if
#(w.machine.type == LaundryMachine.DRYER and
#w.availability == LaundryRecord.AVAILABLE)]),
#)
# Now, actually generate the chart by adding all of the LaundrySummaries
# to the chart and making all of the Timeslots the x-axis.
def update(hall, filepath=None):
"""
Scrape data from esuds for the specified hall, create a new LaundryRecord
for each machine, and optionally save a new current chart for the hall.
To generate a current chart with this update, set filepath to a valid
filepath to save and svg file.
"""
rows = load_data(hall)
records = list()
for row in rows:
cells = row('td')
# Some small laundry rooms (with only two machines) do not label their
# machines. Our database represents "unlabeled" as 0. We will just have
# to rely on the type of the machine when searching.
if str(cells[1].contents[0]) == "unlabeled":
number = 0
else:
number = int(cells[1].contents[0])
type = cells[2].contents[0]
# For some reason there is a distinction between normal and "stacked"
# washers/dryers in esuds; we only need normal, so cut "stacked" off.
if type == 'Stacked Dryer': type = type[8:]
if type == 'Stacked Washer': type = type[8:]
# Since Presidents Park - Harrison apparently has freak Washer/Dryer
# hybrids, we will just ignore the type field when getting the machine.
if type == 'Stacked Washer/Dryer': type = None
availability = cells[3].contents[1].contents[0]
time_remaining = None
if cells[4].contents[0] != ' ':
time_remaining = int(cells[4].contents[0])
if type:
machine = LaundryMachine.objects.get(number=number, type=type,
hall=hall)
else: # Search without type, for Presidents Park - Harrison
machine = LaundryMachine.objects.get(number=number, hall=hall)
record = LaundryRecord(machine=machine, availability=availability,
time_remaining=time_remaining)
if filepath:
records.append(record)
else:
record.save()
if filepath:
generate_current_chart(filepath, records, hall)

54
laundry_app/models.py Normal file
View File

@ -0,0 +1,54 @@
from django.db import models
class Hall(models.Model):
name = models.TextField(max_length=100)
location_id = models.IntegerField() # id used by esuds
class LaundryMachine(models.Model):
WASHER = 'Washer'
DRYER = 'Dryer'
MACHINE_TYPES = (
('Washer', 'Washer'),
('Dryer', 'Dryer'),
)
number = models.PositiveSmallIntegerField() # number assigned by esuds
type = models.TextField(choices=MACHINE_TYPES)
hall = models.ForeignKey('Hall', related_name='machine')
def get_latest_record(self):
records = self.records.all()
return records[len(records)-1]
class LaundryRecord(models.Model):
AVAILABLE = 'Available'
IN_USE = 'In Use'
CYCLE_COMPLETE = 'Cycle Complete'
UNAVAILABLE = 'Unavailable'
AVAILABILITIES = (
(AVAILABLE, 'Available'),
(IN_USE, 'In Use'),
(CYCLE_COMPLETE, 'Cycle Complete'),
(UNAVAILABLE, 'Unavailable'),
)
machine = models.ForeignKey('LaundryMachine', related_name='records')
availability = models.TextField(choices=AVAILABILITIES)
time_remaining = models.IntegerField(null=True)
timestamp = models.DateTimeField(auto_now_add=True)
class Timeslot(models.Model):
hall = models.ForeignKey('Hall', related_name='timeslots')
day = models.SmallIntegerField()
time = models.TimeField()
def washer_avg(self):
summaries = self.summaries.all()
return (sum(s.washers for s in summaries) / (len(summaries)) or 1)
def dryer_avg(self):
summaries = self.summaries.all()
return (sum(s.dryers for s in summaries) / (len(summaries)) or 1)
class LaundrySummary(models.Model):
timeslot = models.ForeignKey('Timeslot', related_name='summaries')
washers = models.IntegerField()
dryers = models.IntegerField()

16
laundry_app/tests.py Normal file
View File

@ -0,0 +1,16 @@
"""
This file demonstrates writing tests using the unittest module. These will pass
when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.assertEqual(1 + 1, 2)

6
laundry_app/urls.py Normal file
View File

@ -0,0 +1,6 @@
from django.conf.urls.defaults import patterns, url
urlpatterns = patterns('laundry_app.views',
url(r'^ajax/current/(?P<hall>\d+)/$', 'ajax_get_current', name='get_current_chart'),
url(r'^$', 'main_page', name='laundry_main'),
)

29
laundry_app/views.py Normal file
View File

@ -0,0 +1,29 @@
from django.template import RequestContext
from django.shortcuts import render_to_response, get_object_or_404
from models import Hall
from django.conf import settings
from django.http import HttpResponse
from django.core.exceptions import ObjectDoesNotExist
from os.path import join
import laundry
SVG_DIR = join(settings.PROJECT_PATH, 'laundry_app', 'static')
SVG_URL = settings.STATIC_URL
def main_page(request):
# pass the halls to the html, and let cookies/user decide which to pick
halls = [(hall.name, hall.id) for hall in
Hall.objects.all().order_by('name')]
return render_to_response('laundry/main.html', {
'halls': halls,
},
context_instance=RequestContext(request))
def ajax_get_current(request, hall):
hall_obj = get_object_or_404(Hall, pk=hall)
filename = str(hall_obj.id) + '_current.svg'
try:
laundry.update(hall_obj, filepath=join(SVG_DIR, filename))
except ObjectDoesNotExist:
return HttpResponse(status=500);
return HttpResponse(join(SVG_URL, filename))

10
manage.py Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "laundry.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)

View File

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html>
{% block head %}
<head>
<meta charset="utf-8">
{% block title %}<title>GMU Laundry</title>{% endblock %}
{% block description %}
<meta name="Description" content="Current laundry machine usage charts for George Mason University's dorms. An experiment by Tyler Hallada.">
{% endblock %}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{ STATIC_URL }}css/bootstrap.min.css" rel="stylesheet">
<link href="{{ STATIC_URL }}css/font-awesome.css" rel="stylesheet">
<link href="{{ STATIC_URL }}css/bootstrap-responsive.min.css" rel="stylesheet">
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script>
<script src="{{ STATIC_URL }}js/bootstrap.min.js"></script>
<script src="{{ STATIC_URL }}js/laundry.js"></script>
<script src="{{ STATIC_URL }}js/jquery.cookie.js"></script>
<link href="{{ STATIC_URL }}img/glyphicons-halflings.png" rel="icons">
<link href="{{ STATIC_URL }}css/style.css" rel="stylesheet">
<!-- Creative Commons - Attribution - The Noun Project -->
<link href="/favicon.ico" rel="shortcut icon">
{% block head-extra %}{% endblock %}
</head>
{% endblock %}
<script>
var halls = {
{% for h in halls %}
"{{ h.0 }}" : {{ h.1 }},
{% endfor %}
}
</script>
<style>
body {
background:#000000;
}
</style>
<body class="laundry">
<div class="container">
<div class="row hall-select-container">
<select id="hall-select">
{% for h in halls %}
{% if h.1 == 1 %}
<option selected="selected">{{ h.0 }}</option>
{% else %}
<option>{{ h.0 }}</option>
{% endif %}
{% endfor %}
</select>
</div>
<div class="row chart-container">
<div class="current-chart"></div>
</div>
</div>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-39880341-1', 'hallada.net');
ga('send', 'pageview');
</script>
</body>
</html>