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

Commit

Permalink
Up to date with 'master'
Browse files Browse the repository at this point in the history
  • Loading branch information
samsartor committed Jan 25, 2018
2 parents 23071bf + 27136e9 commit 3551c8c
Show file tree
Hide file tree
Showing 12 changed files with 201 additions and 45 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

# Default development database
devdata.db
devdata.db-journal

# Default data directory
data/*
Expand Down
17 changes: 17 additions & 0 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ a member of our ACM Chapter, you should be able to work out of this repository
(see below, you will still need to make a branch and submit a PR for most
changes).

.. sidebar:: About This File

This is a "how to contribute" file, not a "requirements for contributions"
file. Chances are, your code will undergo a code review and might receive
constructive criticism on nit-picky things. **Don't take a code review
personally; the point of the review process is to make sure the code for the
site stays clean and bug free.** Things that are not "small nit-picky"
things (i.e., they would cause a major rework before you start working on a
contribution) should be added to this file.

Coding Style
------------

Expand All @@ -14,6 +24,11 @@ Coding Style

.. _`PEP-8`: https://www.python.org/dev/peps/pep-0008/

This is a Python project, and hence documentation should be in reStructuredText
format.

XHTML, CSS, JavaScripts should use 2 spaces for indentation.

Other Notes
-----------

Expand All @@ -34,5 +49,7 @@ Other Notes
* Jack will not pull the ``master`` branch to the production site if he tests
it and discovers software bugs or regressions. Therefore, try to keep
``master`` fairly bug-free so that we can pull as often as we need.
* If you assign a task to someone, but then you later decide that you want to
do it yourself, contact that person first to prevent duplicated work.
* Don't assume any contributing guidelines this file does not state.

7 changes: 5 additions & 2 deletions acmwebsite/controllers/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from tgext.admin.controller import AdminController
from acmwebsite.config.app_cfg import AdminConfig

from acmwebsite.model import Meeting, Survey
from acmwebsite.model import Meeting, Survey, Banner

from acmwebsite.lib.base import BaseController
from acmwebsite.controllers.error import ErrorController
Expand All @@ -24,6 +24,8 @@
from acmwebsite.controllers.survey import SurveysController
from acmwebsite.controllers.project import ProjectsController

from sqlalchemy.sql import functions

import datetime

__all__ = ['RootController']
Expand Down Expand Up @@ -61,7 +63,8 @@ def index(self):
meetings = DBSession.query(Meeting).filter(
Meeting.date > datetime.datetime.now() - datetime.timedelta(hours=3)
).order_by(Meeting.date).limit(2)
return dict(page='index', meetings=meetings)
banner = DBSession.query(Banner).order_by(functions.random()).first()
return dict(page='index', meetings=meetings, banner=banner)

@expose('acmwebsite.templates.login')
def login(self, came_from=lurl('/'), failure=None, login=''):
Expand Down
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)
}
1 change: 1 addition & 0 deletions acmwebsite/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,6 @@ def init_model(engine):
'SurveyResponse',
'SurveyData',
'Project',
'Banner',
'team_table',
)
13 changes: 13 additions & 0 deletions acmwebsite/model/banner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from acmwebsite.model import DeclarativeBase
from sqlalchemy import Column
from sqlalchemy.types import Integer, Unicode
from depot.fields.sqlalchemy import UploadedFileField
from depot.fields.specialized.image import UploadedImageWithThumb


class Banner(DeclarativeBase):
__tablename__ = 'banner'

id = Column(Integer, autoincrement=True, primary_key=True)
photo = Column(UploadedFileField(upload_type=UploadedImageWithThumb))
description = Column(Unicode(2048), unique=True)
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
8 changes: 3 additions & 5 deletions acmwebsite/templates/index.xhtml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@
<body py:block="body" py:strip="True">
<div class="row">
<div class="col-md-7">
<div class="home-banner">
<img class="home-banner-img" src="${tg.url('/img/banner.jpg')}" />
<small>
Image above: ACM Members and other clubs at a Mozilla Campus Clubs training event
</small>
<div py:if="banner is not None" class="home-banner">
<img class="home-banner-img" src="${banner.photo.url}" />
<small py:if="banner.description" py:content="h.markdown(banner.description)"></small>
</div>
</div>
<div class="col-md-5">
Expand Down
Loading

0 comments on commit 3551c8c

Please sign in to comment.