diff --git a/acmwebsite/controllers/survey.py b/acmwebsite/controllers/survey.py index 145f9ce..1ae058a 100644 --- a/acmwebsite/controllers/survey.py +++ b/acmwebsite/controllers/survey.py @@ -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: @@ -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')) @@ -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 diff --git a/acmwebsite/lib/surveytypes.py b/acmwebsite/lib/surveytypes.py index 231adda..305e5f2 100644 --- a/acmwebsite/lib/surveytypes.py +++ b/acmwebsite/lib/surveytypes.py @@ -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 @@ -14,6 +15,7 @@ def from_post(self, form): def from_contents(self, contents): return literal_eval(contents) + class Bool(SurveyType): template = 'checkbox' @@ -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) @@ -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' @@ -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' @@ -68,6 +75,7 @@ def from_post(self, form): def from_contents(self, contents): return contents + class Select(SurveyType): template = 'select' @@ -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' @@ -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) +} diff --git a/acmwebsite/model/survey.py b/acmwebsite/model/survey.py index 35d945e..3501fcd 100644 --- a/acmwebsite/model/survey.py +++ b/acmwebsite/model/survey.py @@ -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) @@ -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' @@ -47,7 +53,6 @@ class SurveyField(DeclarativeBase): max = Column(Float) step = Column(Float) - def type_object(self): return types[self.type]( name=self.name, @@ -62,6 +67,7 @@ def type_object(self): step=self.step, ) + class SurveyResponse(DeclarativeBase): __tablename__ = 'response' diff --git a/acmwebsite/templates/survey_results.xhtml b/acmwebsite/templates/survey_results.xhtml new file mode 100644 index 0000000..3e67a6d --- /dev/null +++ b/acmwebsite/templates/survey_results.xhtml @@ -0,0 +1,40 @@ + + + + Mines ACM - Survey ${survey_id} Responses + + + +

+

Responses

+
+
+ + + + + + + + + + + +
+ + +
+
+
+
+ +