Skip to content
This repository has been archived by the owner on Jan 15, 2019. It is now read-only.

Commit

Permalink
Merge pull request #33 from ColoradoSchoolOfMines/survey-results
Browse files Browse the repository at this point in the history
Graphical survey results page
  • Loading branch information
sumnerevans authored Jan 22, 2018
2 parents 027e51e + e559ba5 commit 27136e9
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 38 deletions.
77 changes: 54 additions & 23 deletions acmwebsite/controllers/survey.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,59 @@
from tg import expose, redirect, validate, flash, url, lurl, abort, require, request
from tg.predicates import has_permission, not_anonymous
from tg import abort, expose, flash, redirect, request, require
from tg.predicates import has_permission

from acmwebsite.lib.base import BaseController
from acmwebsite.lib.helpers import log
from acmwebsite.model import DBSession, Survey, SurveyResponse, SurveyData, User
from acmwebsite.model import DBSession, Survey, SurveyData, SurveyResponse

def survey_fields(survey):
return [{'name': f.name, 'type': f.type} for f in survey.fields]

def response_to_dict(response):
out = {'name': response.name, 'email': response.email}
for item in response.data:
out[item.field.name] = item.field.type_object().from_contents(item.contents)
return out

class SurveyController(BaseController):
def __init__(self, survey):
self.survey = survey

@expose('json')
@expose('acmwebsite.templates.survey_results')
@expose("json")
@require(has_permission('admin'))
def results(self, number=None):
responses = self.survey.responses or []
responses = [response_to_dict(r) for r in responses]
def results(self, number=None, order_by=None, reverse=False):
if type(reverse) is str:
reverse = reverse == 'True'

if order_by:
# TODO (#46): this doesn't work... If the column is nullable, then
# the sort can't deal with comparing None types
# order = '{} {}'.format(order_by, 'asc' if reverse else 'desc')
# responses = self.survey.responses.order_by(order)
responses = self.survey.responses
else:
responses = self.survey.responses

# TODO (#46): This sucks. It would be good to have this done for us with
# SQLAlchemy magic
responses = [self._response_dict(r) for r in responses or []]

survey_title = (self.survey.title or
(self.survey.meeting and self.survey.meeting.title) or
'Survey')
return {
'survey_id': self.survey.id,
'title': survey_title,
'count': len(responses),
'responses': responses,
'fields': survey_fields(self.survey),
'responses': responses,
'fields': self.survey.field_metadata(),
'order_by': order_by,
'reverse': reverse,
}

def _response_dict(self, response):
out = {'name': response.name, 'email': response.email}

# Add the actual response data for the fields that exist.
out.update({
item.field.name:
item.field.type_object().from_contents(item.contents)
for item in response.data
})

return out

@expose('acmwebsite.templates.survey')
def respond(self):
if not self.survey:
Expand All @@ -38,12 +63,16 @@ def respond(self):
if not has_permission('admin'):
abort(403, 'Survey not avalible')
return
flash('This page is currently disabled. You can see it because you are an admin.', 'warn')
flash('This page is currently disabled. You can see it because you are an admin.',
'warn')

form = request.POST
if form:
user = request.identity and request.identity.get('user')
response = SurveyResponse(user=user, provided_name=form.get('_provided_name'), survey=self.survey)
response = SurveyResponse(
user=user,
provided_name=form.get('_provided_name'),
survey=self.survey)
DBSession.add(response)

requires_ft = bool(form.get('first_time'))
Expand All @@ -54,16 +83,18 @@ def respond(self):
fo = f.type_object()
v = fo.from_post(form)
if v:
DBSession.add(SurveyData(response=response, field=f, contents=v))
DBSession.add(
SurveyData(response=response, field=f, contents=v))
flash('Response submitted successfully')
redirect(base_url='/')
else:
return {'survey': self.survey }
return {'survey': self.survey}


class SurveysController(BaseController):
@expose()
def _lookup(self, sid, *args):
survey = DBSession.query(Survey).filter(Survey.id==sid).first()
survey = DBSession.query(Survey).filter(Survey.id == sid).first()
if not survey:
abort(404, "No such survey")
return SurveyController(survey), args
19 changes: 17 additions & 2 deletions acmwebsite/lib/surveytypes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from ast import literal_eval


class SurveyType:
def __init__(self, name, label=None, required=False, first_time=False, **kwargs):
self.name = name
Expand All @@ -14,6 +15,7 @@ def from_post(self, form):
def from_contents(self, contents):
return literal_eval(contents)


class Bool(SurveyType):
template = 'checkbox'

Expand All @@ -24,6 +26,7 @@ def __init__(self, value=None, **kwargs):
def from_post(self, form):
return str(bool(form.get(self.name)))


class Text(SurveyType):
def __init__(self, value='', placeholder=None, **kwargs):
super().__init__(**kwargs)
Expand All @@ -37,12 +40,15 @@ def from_post(self, form):
def from_contents(self, contents):
return contents


class ShortText(Text):
template = 'text'


class LongText(Text):
template = 'textarea'


class ManyOf(SurveyType):
template = 'checkbox_group'

Expand All @@ -54,6 +60,7 @@ def from_post(self, form):
vals = [n for i, n in enumerate(self.options) if form.get('{}_{}'.format(self.name, i))]
return str(vals)


class OneOf(SurveyType):
template = 'radio_group'

Expand All @@ -68,6 +75,7 @@ def from_post(self, form):
def from_contents(self, contents):
return contents


class Select(SurveyType):
template = 'select'

Expand All @@ -83,11 +91,13 @@ def from_post(self, form):
def from_contents(self, contents):
return contents


class SelectMany(Select):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.multiple = True



class Number(SurveyType):
template = 'number'

Expand All @@ -104,4 +114,9 @@ def from_post(self, form):
return None
return repr(float(v))

types = {k: v for k, v in globals().items() if isinstance(v, type) and issubclass(v, SurveyType)}

types = {
k: v
for k, v in globals().items()
if isinstance(v, type) and issubclass(v, SurveyType)
}
32 changes: 19 additions & 13 deletions acmwebsite/model/survey.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
from sqlalchemy import *
from sqlalchemy.orm import mapper, relation, relationship, backref
from sqlalchemy import Table, ForeignKey, Column
from sqlalchemy.types import Integer, String, Unicode

from datetime import datetime

from acmwebsite.model import DeclarativeBase, metadata, DBSession
from sqlalchemy import Boolean, Column, DateTime, Float, ForeignKey, Table
from sqlalchemy.orm import relation
from sqlalchemy.types import Integer, String, Unicode

from acmwebsite.lib.surveytypes import types
from acmwebsite.model import DeclarativeBase, metadata

from ast import literal_eval
survey_field_table = Table(
'survey_field', metadata,
Column('survey_id', Integer, ForeignKey('survey.id'), primary_key=True),
Column('field_id', Integer, ForeignKey('field.id'), primary_key=True))

survey_field_table = Table('survey_field', metadata,
Column('survey_id', Integer, ForeignKey('survey.id'), primary_key=True),
Column('field_id', Integer, ForeignKey('field.id'), primary_key=True)
)

class Survey(DeclarativeBase):
__tablename__ = 'survey'

id = Column(Integer, autoincrement=True, primary_key=True)
meeting = relation('Meeting', back_populates='survey', uselist=False)
fields = relation('SurveyField', secondary=survey_field_table, backref='surveys', order_by='SurveyField.priority')
fields = relation(
'SurveyField',
secondary=survey_field_table,
backref='surveys',
order_by='SurveyField.priority')
title = Column(Unicode)
opens = Column(DateTime)
closes = Column(DateTime)
Expand All @@ -30,6 +32,10 @@ def active(self):
now = datetime.now()
return self.opens and self.opens < now and (not self.closes or self.closes > now)

def field_metadata(self):
return [{'name': f.name, 'type': f.type} for f in self.fields]


class SurveyField(DeclarativeBase):
__tablename__ = 'field'

Expand All @@ -47,7 +53,6 @@ class SurveyField(DeclarativeBase):
max = Column(Float)
step = Column(Float)


def type_object(self):
return types[self.type](
name=self.name,
Expand All @@ -62,6 +67,7 @@ def type_object(self):
step=self.step,
)


class SurveyResponse(DeclarativeBase):
__tablename__ = 'response'

Expand Down
40 changes: 40 additions & 0 deletions acmwebsite/templates/survey_results.xhtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<html py:strip=""
xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude"
py:extends="master.xhtml">

<head py:block="head" py:strip="True">
<title>Mines ACM - Survey ${survey_id} Responses</title>
</head>

<body py:block="body" py:strip="True">
<h1 class="page-header" py:content="title" />
<h2>Responses</h2>
<div class="row">
<div class="col-md-12">
<table class="table table-striped table-condensed table-bordered">
<!--!
I'm doing some fancy shenanigans here to allow the user to sort by
each column ascending and descending.
-->
<thead>
<tr>
<th py:for="field in ('name', *(f['name'] for f in fields))">
<a href="${tg.url('/s/{}/results'.format(survey_id), {'order_by': field, 'reverse': (order_by == field and not reverse)})}"
py:content="field" />
<span py:if="order_by == field"
py:content="h.icon('triangle-%s' % ('top' if reverse else 'bottom'))" />
</th>
</tr>
</thead>
<tbody>
<tr py:for="r in responses">
<td py:for="field in ('name', *(f['name'] for f in fields))"
py:content="r.get(field)" />
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>

0 comments on commit 27136e9

Please sign in to comment.