Initial commit, converting laundry app to site
This commit is contained in:
0
laundry_app/__init__.py
Normal file
0
laundry_app/__init__.py
Normal file
120
laundry_app/laundry.py
Normal file
120
laundry_app/laundry.py
Normal 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
54
laundry_app/models.py
Normal 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
16
laundry_app/tests.py
Normal 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
6
laundry_app/urls.py
Normal 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
29
laundry_app/views.py
Normal 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))
|
||||
Reference in New Issue
Block a user