mirror of https://github.com/fspc/workstand.git
Drew Larson
8 years ago
committed by
GitHub
124 changed files with 2801 additions and 2619 deletions
@ -0,0 +1,8 @@ |
|||||
|
FROM python:3.6 |
||||
|
RUN mkdir /code |
||||
|
WORKDIR /code |
||||
|
RUN mkdir requirements |
||||
|
ADD ./bikeshop_project /code |
||||
|
COPY ./requirements /code/requirements |
||||
|
RUN pip install -r ./requirements/development.txt |
||||
|
EXPOSE 8000:8000 |
@ -0,0 +1,14 @@ |
|||||
|
FROM python:3.6 |
||||
|
RUN apt-get install curl |
||||
|
RUN curl -sL https://deb.nodesource.com/setup_6.x | bash - |
||||
|
RUN apt-get install -y nodejs |
||||
|
RUN mkdir /code |
||||
|
WORKDIR /code |
||||
|
RUN mkdir requirements |
||||
|
ADD bikeshop_project /code |
||||
|
COPY requirements /code/requirements |
||||
|
RUN pip install -r requirements/production.txt |
||||
|
RUN npm cache clean |
||||
|
RUN npm install --unsafe-perm |
||||
|
RUN npm run build-production |
||||
|
RUN DJANGO_SETTINGS_MODULE=bikeshop.settings.production python manage.py collectstatic --no-input |
@ -0,0 +1,9 @@ |
|||||
|
FROM node:7.3.0 |
||||
|
RUN mkdir /code |
||||
|
WORKDIR /code |
||||
|
ADD ./bikeshop_project /code |
||||
|
RUN npm install |
||||
|
RUN npm install -g bower |
||||
|
COPY ./bower.json . |
||||
|
RUN bower install --allow-root |
||||
|
EXPOSE 3000:3000 |
@ -1,53 +0,0 @@ |
|||||
# -*- mode: ruby -*- |
|
||||
# vi: set ft=ruby : |
|
||||
|
|
||||
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! |
|
||||
VAGRANTFILE_API_VERSION = "2" |
|
||||
|
|
||||
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| |
|
||||
# All Vagrant configuration is done here. The most common configuration |
|
||||
# options are documented and commented below. For a complete reference, |
|
||||
# please see the online documentation at vagrantup.com. |
|
||||
|
|
||||
# Every Vagrant virtual environment requires a box to build off of. |
|
||||
config.vm.box = "ubuntu/trusty64" |
|
||||
config.vm.hostname = "bikeshop-dev" |
|
||||
config.vm.provider "virtualbox" do |v| |
|
||||
v.name = "bikeshop-dev" |
|
||||
v.memory = 1024 |
|
||||
end |
|
||||
|
|
||||
# Create a forwarded port mapping which allows access to a specific port |
|
||||
# within the machine from a port on the host machine. In the example below, |
|
||||
# accessing "localhost:8080" will access port 80 on the guest machine. |
|
||||
# config.vm.network "forwarded_port", guest: 8000, host: 80 |
|
||||
|
|
||||
# Create a private network, which allows host-only access to the machine |
|
||||
# using a specific IP. |
|
||||
config.vm.network "private_network", ip: "192.168.33.45" |
|
||||
config.vm.hostname = "bikeshop.local" |
|
||||
|
|
||||
# Share an additional folder to the guest VM. The first argument is |
|
||||
# the path on the host to the actual folder. The second argument is |
|
||||
# the path on the guest to mount the folder. And the optional third |
|
||||
# argument is a set of non-required options. |
|
||||
config.vm.synced_folder '.', '/vagrant', disabled: true |
|
||||
config.vm.synced_folder "bikeshop_project", "/srv/bikeshop", type: "nfs" |
|
||||
|
|
||||
config.vm.provision "ansible" do |ansible| |
|
||||
ansible.groups = { |
|
||||
"development" => "default" |
|
||||
} |
|
||||
ansible.playbook = "provision/site.yml" |
|
||||
# ansible.ask_sudo_pass = true |
|
||||
# ansible.verbose = "v" |
|
||||
ansible.sudo = true |
|
||||
ansible.host_key_checking = false |
|
||||
ansible.limit = "default" |
|
||||
ansible.extra_vars = { |
|
||||
ansible_ssh_user: 'vagrant', |
|
||||
ansible_connection: 'ssh', |
|
||||
ansible_ssh_args: '-o ForwardAgent=yes', |
|
||||
} |
|
||||
end |
|
||||
end |
|
@ -0,0 +1,3 @@ |
|||||
|
{ |
||||
|
presets: ['es2015', 'stage-0', 'react'] |
||||
|
} |
@ -0,0 +1,12 @@ |
|||||
|
{ |
||||
|
"extends": "airbnb", |
||||
|
"parser": "babel-eslint", |
||||
|
"plugins": [ |
||||
|
"react", |
||||
|
"jsx-a11y", |
||||
|
"import" |
||||
|
], |
||||
|
"rules": { |
||||
|
"sort-imports": "error" |
||||
|
} |
||||
|
} |
File diff suppressed because one or more lines are too long
@ -0,0 +1,21 @@ |
|||||
|
import AutoComplete from 'material-ui/AutoComplete'; |
||||
|
import React from 'react'; |
||||
|
|
||||
|
export default class Member extends React.Component { |
||||
|
render() { |
||||
|
return ( |
||||
|
<AutoComplete |
||||
|
dataSource={this.props.members} |
||||
|
onUpdateInput={this.props.handleUpdate.bind(this)} |
||||
|
openOnFocus |
||||
|
filter={AutoComplete.noFilter} |
||||
|
onNewRequest={this.props.signIn.bind(this)} |
||||
|
errorText={this.props.error} |
||||
|
hintText="Search members" |
||||
|
searchText={this.props.searchText} |
||||
|
fullWidth |
||||
|
textFieldStyle={{ textAlign: 'center', fontSize: '32px', lineHeight: '48px' }} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
} |
@ -0,0 +1,24 @@ |
|||||
|
import React, { PropTypes } from 'react'; |
||||
|
import MenuItem from 'material-ui/MenuItem'; |
||||
|
import SelectField from 'material-ui/SelectField'; |
||||
|
|
||||
|
const Purpose = ({ initialValue, handleChange }) => ( |
||||
|
<SelectField |
||||
|
value={initialValue} |
||||
|
onChange={handleChange} |
||||
|
fullWidth |
||||
|
> |
||||
|
<MenuItem value={'VOLUNTEER'} primaryText="Volunteer" /> |
||||
|
<MenuItem value={'FIX'} primaryText="Fix" /> |
||||
|
<MenuItem value={'WORKSHOP'} primaryText="Workshop" /> |
||||
|
<MenuItem value={'DONATE'} primaryText="Donate" /> |
||||
|
<MenuItem value={'STAFF'} primaryText="Staff" /> |
||||
|
</SelectField> |
||||
|
); |
||||
|
|
||||
|
Purpose.propTypes = { |
||||
|
initialValue: PropTypes.string, |
||||
|
handleChange: PropTypes.func, |
||||
|
}; |
||||
|
|
||||
|
export default Purpose; |
@ -0,0 +1,154 @@ |
|||||
|
import fetch from 'isomorphic-fetch'; |
||||
|
import moment from 'moment'; |
||||
|
import { polyFill } from 'es6-promise'; |
||||
|
import RaisedButton from 'material-ui/RaisedButton'; |
||||
|
import React from 'react'; |
||||
|
|
||||
|
import Member from './Member'; |
||||
|
import Purpose from './Purpose'; |
||||
|
import SignedInList from './SignedInList'; |
||||
|
|
||||
|
export default class SignIn extends React.Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
|
||||
|
this.state = { |
||||
|
members: [], |
||||
|
signOn: { purpose: 'FIX', member: undefined }, |
||||
|
error: '', |
||||
|
signedIn: [], |
||||
|
searchText: '', |
||||
|
}; |
||||
|
|
||||
|
this.handleUpdate = this.handleUpdate.bind(this); |
||||
|
this.signIn = this.signIn.bind(this); |
||||
|
this.chooseMember = this.chooseMember.bind(this); |
||||
|
this.handlePurposeChoice = this.handlePurposeChoice.bind(this); |
||||
|
} |
||||
|
|
||||
|
componentDidMount() { |
||||
|
fetch('/members/signin/') |
||||
|
.then(response => response.json()) |
||||
|
.then((data) => { |
||||
|
const visits = JSON.parse(data); |
||||
|
this.setState({ signedIn: visits.map(visit => ({ |
||||
|
id: visit.member.id, |
||||
|
purpose: visit.purpose, |
||||
|
text: `${visit.member.first_name} ${visit.member.last_name}`, |
||||
|
value: `${visit.member.first_name} ${visit.member.last_name} <${visit.member.email}>`, |
||||
|
at: moment(visit.created_at), |
||||
|
})) }); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
onUpdateSearchText(searchText) { |
||||
|
this.setState({ searchText }); |
||||
|
} |
||||
|
|
||||
|
chooseMember(chosenRequest, index) { |
||||
|
const member = this.state.members[index]; |
||||
|
const purpose = this.state.signOn.purpose; |
||||
|
|
||||
|
this.setState({ ...this.state, signOn: { member, purpose } }); |
||||
|
} |
||||
|
|
||||
|
signIn() { |
||||
|
const purpose = this.state.signOn.purpose; |
||||
|
const member = this.state.signOn.member; |
||||
|
|
||||
|
if (!this.state.signedIn.find(signedInMember => signedInMember.id === member.id)) { |
||||
|
fetch('/members/signin/', { |
||||
|
method: 'post', |
||||
|
body: `id=${member.id}&purpose=${purpose}`, |
||||
|
headers: { |
||||
|
'Content-Type': 'application/x-www-form-urlencoded', |
||||
|
} }) |
||||
|
.then((response) => { |
||||
|
if (response.status === 201) { |
||||
|
return response.json(); |
||||
|
} |
||||
|
}) |
||||
|
.then((data) => { |
||||
|
const signedIn = this.state.signedIn; |
||||
|
const parsedData = JSON.parse(data); |
||||
|
|
||||
|
signedIn.push({ ...member, purpose, at: moment(parsedData.results.created_at) }); |
||||
|
this.setState({ |
||||
|
...this.state, |
||||
|
signedIn, |
||||
|
signOn: { purpose: 'FIX', member: undefined }, |
||||
|
searchText: '', |
||||
|
members: [], |
||||
|
}); |
||||
|
}); |
||||
|
} else { |
||||
|
this.setState({ ...this.state, error: 'Member already signed in.' }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
handlePurposeChoice(event, index, value) { |
||||
|
this.setState({ ...this.state, signOn: { ...this.state.signOn, purpose: value } }); |
||||
|
} |
||||
|
|
||||
|
handleUpdate(text) { |
||||
|
const self = this; |
||||
|
self.setState({ searchText: text }); |
||||
|
fetch(`/members/search/${text}/`) |
||||
|
.then((response) => { |
||||
|
if (response.status === 200) { |
||||
|
return response.json(); |
||||
|
} |
||||
|
throw new Error('Bad response from server'); |
||||
|
}) |
||||
|
.then((data) => { |
||||
|
if (data.results.length > 0) { |
||||
|
self.setState({ |
||||
|
...this.state, |
||||
|
error: '', |
||||
|
members: data.results.map(result => ({ text: `${result.name}`, value: `${result.name} <${result.email}>`, id: result.id })), |
||||
|
}); |
||||
|
} else { |
||||
|
self.setState({ ...this.state, error: 'Member not found.' }); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
return ( |
||||
|
<div> |
||||
|
<div className="mdl-grid"> |
||||
|
<div className="mdl-cell--12-col mdl-cell"> |
||||
|
<h1 className="mdl-typography--display-4">Sign-in</h1> |
||||
|
<h2 className="mdl-typography--display-3">{moment().format('MMM DD, YYYY')}</h2> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div className="mdl-grid"> |
||||
|
<div className="mdl-cell--8-col mdl-cell"> |
||||
|
<Member |
||||
|
handleUpdate={this.handleUpdate} |
||||
|
signIn={this.chooseMember} |
||||
|
error={this.state.error} |
||||
|
members={this.state.members} |
||||
|
searchText={this.state.searchText} |
||||
|
/> |
||||
|
</div> |
||||
|
<div className="mdl-cell mdl-cell--4-col"> |
||||
|
<Purpose |
||||
|
handleChange={this.handlePurposeChoice} |
||||
|
initialValue={this.state.signOn.purpose} |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div className="mdl-grid"> |
||||
|
<div className="mdl-cell mdl-cell--2-col mdl-cell--10-offset"> |
||||
|
<RaisedButton onClick={this.signIn} label="Sign-in" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div className="mdl-grid"> |
||||
|
<SignedInList members={this.state.signedIn} /> |
||||
|
</div> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
} |
@ -0,0 +1,75 @@ |
|||||
|
import _ from 'lodash'; |
||||
|
import React, { PropTypes } from 'react'; |
||||
|
import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn } from 'material-ui/Table'; |
||||
|
import moment from 'moment'; |
||||
|
|
||||
|
export default class SignedInList extends React.Component { |
||||
|
static propTypes = { |
||||
|
members: PropTypes.arrayOf(PropTypes.shape({ |
||||
|
id: PropTypes.number, |
||||
|
purpose: PropTypes.string, |
||||
|
at: PropTypes.instanceOf(moment), |
||||
|
})), |
||||
|
} |
||||
|
|
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
this.state = { tick: 0 }; |
||||
|
this.componentDidMount = this.componentDidMount.bind(this); |
||||
|
this.componentWillUnmount = this.componentWillUnmount.bind(this); |
||||
|
this.tick = this.tick.bind(this); |
||||
|
} |
||||
|
|
||||
|
componentDidMount() { |
||||
|
this.timer = setInterval(this.tick, 50); |
||||
|
} |
||||
|
|
||||
|
componentWillUnmount() { |
||||
|
clearInterval(this.timer); |
||||
|
} |
||||
|
|
||||
|
sortMembers(members) { |
||||
|
return _.sortBy(members, m => m.at.valueOf()).reverse(); |
||||
|
} |
||||
|
|
||||
|
tick() { |
||||
|
this.setState({ tick: this.state.tick += 1 }); |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const memberRows = this.sortMembers(this.props.members) |
||||
|
.map(member => ( |
||||
|
<TableRow selectable={false} key={member.id}> |
||||
|
<TableRowColumn>{member.text}</TableRowColumn> |
||||
|
<TableRowColumn>{member.purpose}</TableRowColumn> |
||||
|
<TableRowColumn>{member.at.fromNow()}</TableRowColumn> |
||||
|
<TableRowColumn><a href={`/members/edit/${member.id}/`}>Profile</a></TableRowColumn> |
||||
|
</TableRow> |
||||
|
)); |
||||
|
|
||||
|
return ( |
||||
|
<div className="mdl-cell mdl-cell--12-col"> |
||||
|
<h3>Members signed in</h3> |
||||
|
<Table selectable={false}> |
||||
|
<TableHeader adjustForCheckbox={false} displaySelectAll={false}> |
||||
|
<TableRow> |
||||
|
<TableHeaderColumn>Name</TableHeaderColumn> |
||||
|
<TableHeaderColumn>Purpose</TableHeaderColumn> |
||||
|
<TableHeaderColumn>Signed-in At</TableHeaderColumn> |
||||
|
<TableHeaderColumn/> |
||||
|
</TableRow> |
||||
|
</TableHeader> |
||||
|
<TableBody displayRowCheckbox={false}> |
||||
|
{memberRows.length ? |
||||
|
memberRows : |
||||
|
<TableRow> |
||||
|
<TableRowColumn>{'No members currently signed in.'}</TableRowColumn> |
||||
|
</TableRow> |
||||
|
} |
||||
|
</TableBody> |
||||
|
</Table> |
||||
|
|
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
} |
@ -0,0 +1,25 @@ |
|||||
|
import React from 'react'; |
||||
|
import ReactDOM from 'react-dom'; |
||||
|
import injectTapEventPlugin from 'react-tap-event-plugin'; |
||||
|
|
||||
|
// Needed for onTouchTap |
||||
|
// http://stackoverflow.com/a/34015469/988941 |
||||
|
injectTapEventPlugin(); |
||||
|
|
||||
|
import getMuiTheme from 'material-ui/styles/getMuiTheme'; |
||||
|
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; |
||||
|
import SignIn from './components/SignIn'; |
||||
|
|
||||
|
|
||||
|
|
||||
|
class App extends React.Component { |
||||
|
render () { |
||||
|
return ( |
||||
|
<MuiThemeProvider muiTheme={getMuiTheme()}> |
||||
|
<SignIn /> |
||||
|
</MuiThemeProvider> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ReactDOM.render(<App />, document.getElementById('root')); |
@ -0,0 +1,48 @@ |
|||||
|
from .base import * |
||||
|
|
||||
|
|
||||
|
# SECURITY WARNING: keep the secret key used in production secret! |
||||
|
WSGI_APPLICATION = 'bikeshop.wsgi.application' |
||||
|
|
||||
|
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'secret') |
||||
|
|
||||
|
# SECURITY WARNING: don't run with debug turned on in production! |
||||
|
DEBUG = False |
||||
|
|
||||
|
ALLOWED_HOSTS = ['shop.bcbc.bike'] |
||||
|
|
||||
|
LOGGING = { |
||||
|
'version': 1, |
||||
|
'disable_existing_loggers': False, |
||||
|
'handlers': { |
||||
|
'console': { |
||||
|
'class': 'logging.StreamHandler', |
||||
|
'formatter': 'verbose' |
||||
|
}, |
||||
|
}, |
||||
|
'formatters': { |
||||
|
'verbose': { |
||||
|
'format': '%(levelname)s %(asctime)s %(pathname)s %(message)s' |
||||
|
}, |
||||
|
}, |
||||
|
'loggers': { |
||||
|
'django': { |
||||
|
'handlers': ['console'], |
||||
|
'level': os.getenv('DJANGO_LOG_LEVEL', 'ERROR'), |
||||
|
}, |
||||
|
'bikeshop': { |
||||
|
'handlers': ['console'], |
||||
|
'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'), |
||||
|
} |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
WEBPACK_LOADER = { |
||||
|
'DEFAULT': { |
||||
|
'CACHE': False, |
||||
|
'BUNDLE_DIR_NAME': 'dist/', |
||||
|
'STATS_FILE': os.path.join(BASE_DIR, '../webpack-stats-prod.json'), |
||||
|
'POLL_INTERVAL': 0.1, |
||||
|
'IGNORE': ['.+\.hot-update.js', '.+\.map'] |
||||
|
} |
||||
|
} |
@ -0,0 +1,85 @@ |
|||||
|
import csv |
||||
|
import json |
||||
|
import sys |
||||
|
import os |
||||
|
|
||||
|
import requests |
||||
|
from django.utils import timezone |
||||
|
from django.db import IntegrityError |
||||
|
import dateutil.parser |
||||
|
|
||||
|
from core.models import Membership, Payment |
||||
|
from registration.models import Member |
||||
|
|
||||
|
|
||||
|
|
||||
|
def email_generator(): |
||||
|
url = 'http://randomword.setgetgo.com/get.php' |
||||
|
local = [] |
||||
|
for idx in range(2): |
||||
|
r = requests.get(url) |
||||
|
local.append(r.text) |
||||
|
|
||||
|
return '{0}.{1}@example.com'.format(*local) |
||||
|
|
||||
|
|
||||
|
def get_payment_type(pt): |
||||
|
payment_types = Payment.payment_choices |
||||
|
try: |
||||
|
return [payment_type for payment_type in payment_types if payment_type[1].lower() == pt.lower()][0] |
||||
|
except IndexError: |
||||
|
return 'UNKNOWN', 'Unknown' |
||||
|
|
||||
|
|
||||
|
def member_import(): |
||||
|
filename = os.path.join(os.getcwd(), '2016 BCBC Membership Agreement (Responses) - Form Responses 1.csv') |
||||
|
with open(filename, newline='') as csvfile: |
||||
|
reader = csv.DictReader(csvfile) |
||||
|
for row in reader: |
||||
|
try: |
||||
|
dob = dateutil.parser.parse(row.get('dob', None)) |
||||
|
except ValueError: |
||||
|
dob = None |
||||
|
|
||||
|
try: |
||||
|
waiver = dateutil.parser.parse(row.get('signed', None)) |
||||
|
except ValueError: |
||||
|
waiver = None |
||||
|
|
||||
|
try: |
||||
|
renewed_at = dateutil.parser.parse(row.get('timestamp', None)) |
||||
|
except ValueError: |
||||
|
renewed_at = timezone.now() |
||||
|
|
||||
|
try: |
||||
|
new_member = Member.objects.create( |
||||
|
email=row.get('email', None), |
||||
|
email_consent=row.get('email_consent', False), |
||||
|
first_name=row.get('first_name'), |
||||
|
last_name=row.get('last_name'), |
||||
|
preferred_name=row.get('handle', None), |
||||
|
date_of_birth=dob, |
||||
|
guardian_name=row.get('guardian', None), |
||||
|
phone=row.get('phone', None), |
||||
|
street=row.get('address', None), |
||||
|
city=row.get('city', None), |
||||
|
province=row.get('province', None), |
||||
|
country=row.get('country', None), |
||||
|
post_code=row.get('postal', None), |
||||
|
waiver=waiver |
||||
|
) |
||||
|
|
||||
|
payment = Payment.objects.create( |
||||
|
type=get_payment_type(row.get('payment'))[0], |
||||
|
) |
||||
|
|
||||
|
Membership.objects.create( |
||||
|
renewed_at=renewed_at, |
||||
|
self_identification=row.get('self_identification', None), |
||||
|
gender=row.get('gender', None), |
||||
|
member=new_member, |
||||
|
payment=payment |
||||
|
) |
||||
|
except IntegrityError as e: |
||||
|
print(e) |
||||
|
print(row.get('first_name'), row.get('last_name'), row.get('email')) |
@ -1,3 +1,11 @@ |
|||||
from django.contrib import admin |
from django.contrib import admin |
||||
|
from .models import Membership, Payment, Visit |
||||
|
|
||||
# Register your models here. |
# Register your models here. |
||||
|
admin.site.register([Membership, Payment]) |
||||
|
|
||||
|
|
||||
|
@admin.register(Visit) |
||||
|
class VistAdmin(admin.ModelAdmin): |
||||
|
ordering = ('created_at',) |
||||
|
list_display = ('member', 'purpose', 'created_at') |
||||
|
@ -0,0 +1,77 @@ |
|||||
|
import logging |
||||
|
from django.forms import BooleanField, CharField, CheckboxInput, RadioSelect, ModelForm, TextInput, HiddenInput, ChoiceField |
||||
|
|
||||
|
from registration.models import Member |
||||
|
|
||||
|
from .models import Membership, Payment |
||||
|
|
||||
|
logger = logging.getLogger('bikeshop') |
||||
|
|
||||
|
|
||||
|
class MembershipForm(ModelForm): |
||||
|
member = CharField(required=True, widget=HiddenInput()) |
||||
|
self_ident_other = CharField(required=False, label='Self identification', |
||||
|
widget=TextInput(attrs={'class': 'mdl-textfield__input'})) |
||||
|
gender_other = CharField(required=False, label='Other', widget=TextInput(attrs={'class': 'mdl-textfield__input'})) |
||||
|
safe_space = BooleanField(required=True, widget=CheckboxInput( |
||||
|
attrs={'class': 'mdl-checkbox__input'} |
||||
|
)) |
||||
|
respect_community = BooleanField(required=True, widget=CheckboxInput( |
||||
|
attrs={'class': 'mdl-checkbox__input'} |
||||
|
)) |
||||
|
give_back = BooleanField(required=True, widget=CheckboxInput( |
||||
|
attrs={'class': 'mdl-checkbox__input'} |
||||
|
)) |
||||
|
respect_shop = BooleanField(required=True, widget=CheckboxInput( |
||||
|
attrs={'class': 'mdl-checkbox__input'} |
||||
|
)) |
||||
|
|
||||
|
class Meta: |
||||
|
model = Membership |
||||
|
fields = ['renewed_at', 'self_identification', 'gender'] |
||||
|
|
||||
|
self_ident_choices = ( |
||||
|
('First Nations; Métis; or Inuit', 'First Nations; Métis; or Inuit'), |
||||
|
('visible minority', 'Visible Minority'), |
||||
|
('caucasian', 'Caucasian'), |
||||
|
('other', 'Other') |
||||
|
) |
||||
|
|
||||
|
gender_choices = ( |
||||
|
('male', 'Male'), |
||||
|
('female', 'Female'), |
||||
|
('other', 'Other') |
||||
|
) |
||||
|
|
||||
|
widgets = { |
||||
|
'self_identification': RadioSelect(choices=self_ident_choices, attrs={'class': 'mdl-radio__button'}), |
||||
|
'gender': RadioSelect(choices=gender_choices, attrs={'class': 'mdl-radio__button'}), |
||||
|
'renewed_at': TextInput(attrs={'class': 'mdl-textfield__input'}), |
||||
|
} |
||||
|
|
||||
|
def save(self, commit=True): |
||||
|
instance = super(MembershipForm, self).save(commit=False) |
||||
|
member = Member.objects.get(id=self.cleaned_data['member']) |
||||
|
instance.member = member |
||||
|
logger.debug(self.cleaned_data['self_identification']) |
||||
|
logger.debug(self.cleaned_data['gender']) |
||||
|
|
||||
|
if self.cleaned_data['gender_other']: |
||||
|
instance.gender = self.cleaned_data['gender_other'] |
||||
|
|
||||
|
if self.cleaned_data['self_ident_other']: |
||||
|
instance.self_identification = self.cleaned_data['self_ident_other'] |
||||
|
|
||||
|
if commit: |
||||
|
instance.save() |
||||
|
|
||||
|
return instance |
||||
|
|
||||
|
|
||||
|
class PaymentForm(ModelForm): |
||||
|
class Meta: |
||||
|
model = Payment |
||||
|
fields = ['type'] |
||||
|
widgets = { |
||||
|
'type': RadioSelect(attrs={'class': 'mdl-radio__button'}) |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Generated by Django 1.9.7 on 2017-01-01 20:54 |
||||
|
from __future__ import unicode_literals |
||||
|
|
||||
|
from django.db import migrations, models |
||||
|
import django.utils.timezone |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
('core', '0001_initial'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.AlterField( |
||||
|
model_name='visit', |
||||
|
name='created_at', |
||||
|
field=models.DateTimeField(default=django.utils.timezone.now), |
||||
|
), |
||||
|
] |
@ -1,25 +0,0 @@ |
|||||
# -*- coding: utf-8 -*- |
|
||||
# Generated by Django 1.9.4 on 2016-03-23 02:27 |
|
||||
from __future__ import unicode_literals |
|
||||
|
|
||||
from django.db import migrations, models |
|
||||
import django.db.models.deletion |
|
||||
|
|
||||
|
|
||||
class Migration(migrations.Migration): |
|
||||
|
|
||||
dependencies = [ |
|
||||
('core', '0001_initial'), |
|
||||
] |
|
||||
|
|
||||
operations = [ |
|
||||
migrations.CreateModel( |
|
||||
name='Payment', |
|
||||
fields=[ |
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
|
||||
('type', models.CharField(choices=[('CASH', 'cash'), ('CHEQUE', 'cheque'), ('VOLUNTEERING', 'volunteering'), ('STRIPE', 'stripe'), ('PAYPAL', 'paypal')], max_length=12)), |
|
||||
('created_at', models.DateTimeField(auto_now_add=True)), |
|
||||
('membership', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Membership')), |
|
||||
], |
|
||||
), |
|
||||
] |
|
@ -1,27 +0,0 @@ |
|||||
# -*- coding: utf-8 -*- |
|
||||
# Generated by Django 1.9.4 on 2016-03-23 02:34 |
|
||||
from __future__ import unicode_literals |
|
||||
|
|
||||
from django.conf import settings |
|
||||
from django.db import migrations, models |
|
||||
import django.db.models.deletion |
|
||||
|
|
||||
|
|
||||
class Migration(migrations.Migration): |
|
||||
|
|
||||
dependencies = [ |
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
|
||||
('core', '0002_payment'), |
|
||||
] |
|
||||
|
|
||||
operations = [ |
|
||||
migrations.CreateModel( |
|
||||
name='Visit', |
|
||||
fields=[ |
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
|
||||
('created_at', models.DateTimeField(auto_now_add=True)), |
|
||||
('purpose', models.CharField(choices=[('VOLUNTEER', 'volunteer'), ('WORK', 'work on bike'), ('WORKSHOP', 'workshop')], max_length=50)), |
|
||||
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), |
|
||||
], |
|
||||
), |
|
||||
] |
|
@ -1,49 +1,75 @@ |
|||||
from django.db import models |
from django.db import models |
||||
from django.utils import timezone |
from django.utils import timezone |
||||
|
from django.utils.functional import cached_property |
||||
|
|
||||
|
from dateutil.relativedelta import relativedelta |
||||
|
|
||||
|
|
||||
class Membership(models.Model): |
class Membership(models.Model): |
||||
created_at = models.DateTimeField(auto_now_add=True) |
created_at = models.DateTimeField(auto_now_add=True) |
||||
modified_at = models.DateTimeField(auto_now=True) |
modified_at = models.DateTimeField(auto_now=True) |
||||
renewed_at = models.DateTimeField(default=timezone.now) |
renewed_at = models.DateTimeField(default=timezone.now) |
||||
member = models.OneToOneField( |
self_identification = models.CharField(max_length=255, null=True, blank=True) |
||||
|
gender = models.CharField(max_length=255, null=True, blank=True) |
||||
|
involvement = models.CharField(max_length=255, null=True, blank=True) |
||||
|
member = models.ForeignKey( |
||||
'registration.Member', |
'registration.Member', |
||||
on_delete=models.CASCADE, |
on_delete=models.CASCADE, |
||||
related_name='membership' |
related_name='memberships', |
||||
|
blank=True, |
||||
|
null=True |
||||
) |
) |
||||
safe_space = models.BooleanField(default=False) |
payment = models.OneToOneField( |
||||
community = models.BooleanField(default=False) |
'Payment', |
||||
space = models.BooleanField(default=False) |
on_delete=models.CASCADE, |
||||
give_back = models.BooleanField(default=False) |
related_name='membership', |
||||
# this should be a form field that requires the new member to type out there full name |
blank=False, |
||||
acknowledgement = models.BooleanField(default=False) |
null=True |
||||
|
) |
||||
|
|
||||
|
@cached_property |
||||
|
def expires_at(self): |
||||
|
return self.renewed_at + relativedelta(years=1) |
||||
|
|
||||
|
|
||||
class Payment(models.Model): |
class Payment(models.Model): |
||||
membership = models.ForeignKey( |
|
||||
'Membership', |
|
||||
on_delete=models.CASCADE, |
|
||||
) |
|
||||
payment_choices = ( |
payment_choices = ( |
||||
('CASH', 'cash'), |
('NONE', 'None'), |
||||
('CHEQUE', 'cheque'), |
('CASH', 'Cash'), |
||||
('VOLUNTEERING', 'volunteering'), |
('CHEQUE', 'Cheque'), |
||||
('STRIPE', 'stripe'), |
('VOLUNTEERING', 'Volunteering'), |
||||
('PAYPAL', 'paypal') |
('SQUARE', 'Square'), |
||||
|
('PAYPAL', 'PayPal'), |
||||
|
('UNKNOWN', 'Unknown') |
||||
) |
) |
||||
type = models.CharField(max_length=12, choices=payment_choices) |
type = models.CharField(max_length=12, choices=payment_choices, default='NONE') |
||||
created_at = models.DateTimeField(auto_now_add=True) |
created_at = models.DateTimeField(auto_now_add=True) |
||||
|
|
||||
|
|
||||
class Visit(models.Model): |
class Visit(models.Model): |
||||
|
VOLUNTEER = 'VOLUNTEER' |
||||
|
FIX = 'FIX' |
||||
|
WORKSHOP = 'WORKSHOP' |
||||
|
VISIT = 'VISIT' |
||||
|
DONATE = 'DONATE' |
||||
|
STAFF = 'STAFF' |
||||
|
|
||||
visit_choices = ( |
visit_choices = ( |
||||
('VOLUNTEER', 'volunteer'), |
(VOLUNTEER, 'volunteer'), |
||||
('WORK', 'work on bike'), |
(FIX, 'fix bike'), # fix |
||||
('WORKSHOP', 'workshop') |
(WORKSHOP, 'workshop'), |
||||
|
(VISIT, 'visit'), |
||||
|
(DONATE, 'donate'), |
||||
|
(STAFF, 'staff'), |
||||
) |
) |
||||
|
|
||||
member = models.ForeignKey( |
member = models.ForeignKey( |
||||
'registration.Member', |
'registration.Member', |
||||
on_delete=models.CASCADE |
on_delete=models.CASCADE |
||||
) |
) |
||||
created_at = models.DateTimeField(auto_now_add=True) |
created_at = models.DateTimeField(default=timezone.now) |
||||
purpose = models.CharField(max_length=50, choices=visit_choices) |
purpose = models.CharField(max_length=50, choices=visit_choices) |
||||
|
|
||||
|
def __str__(self): |
||||
|
return '<Visit purpose: {purpose} created_at: {created_at}>'.format(purpose=self.purpose, |
||||
|
created_at=self.created_at.isoformat()) |
||||
|
@ -0,0 +1,59 @@ |
|||||
|
{% extends 'base.html' %} |
||||
|
{% load render_bundle from webpack_loader %} |
||||
|
{% load staticfiles %} |
||||
|
|
||||
|
{% block header %} |
||||
|
<header class="demo-header mdl-layout__header mdl-color--grey-100 mdl-color-text--grey-600"> |
||||
|
<div class="mdl-layout__header-row"> |
||||
|
<span class="mdl-layout-title">BCBC</span> |
||||
|
<div class="mdl-layout-spacer"></div> |
||||
|
{# <div class="mdl-textfield mdl-js-textfield mdl-textfield--expandable">#} |
||||
|
{# <label class="mdl-button mdl-js-button mdl-button--icon" for="search">#} |
||||
|
{# <i class="material-icons">search</i>#} |
||||
|
{# </label>#} |
||||
|
{# <div class="mdl-textfield__expandable-holder">#} |
||||
|
{# <input class="mdl-textfield__input" type="text" id="search">#} |
||||
|
{# <label class="mdl-textfield__label" for="search">Enter your query...</label>#} |
||||
|
{# </div>#} |
||||
|
{# </div>#} |
||||
|
{# <button class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--icon" id="hdrbtn">#} |
||||
|
{# <i class="material-icons">more_vert</i>#} |
||||
|
{# </button>#} |
||||
|
{# <ul class="mdl-menu mdl-js-menu mdl-js-ripple-effect mdl-menu--bottom-right" for="hdrbtn">#} |
||||
|
{# <li class="mdl-menu__item">About</li>#} |
||||
|
{# <li class="mdl-menu__item">Contact</li>#} |
||||
|
{# <li class="mdl-menu__item">Legal information</li>#} |
||||
|
{# </ul>#} |
||||
|
</div> |
||||
|
</header> |
||||
|
<div class="demo-drawer mdl-layout__drawer mdl-color--blue-grey-900 mdl-color-text--blue-grey-50"> |
||||
|
<header class="demo-drawer-header"> |
||||
|
<div class="demo-avatar-dropdown"> |
||||
|
|
||||
|
<span>{{ request.user.get_full_name }}</span> |
||||
|
|
||||
|
<div class="mdl-layout-spacer"></div> |
||||
|
<button id="accbtn" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--icon"> |
||||
|
<i class="material-icons" role="presentation">arrow_drop_down</i> |
||||
|
<span class="visuallyhidden">Accounts</span> |
||||
|
</button> |
||||
|
<ul class="mdl-menu mdl-menu--bottom-right mdl-js-menu mdl-js-ripple-effect" for="accbtn"> |
||||
|
<li class="mdl-menu__item"><a href="{% url 'member_edit' member_id=request.user.id %}">Edit profile</a></li> |
||||
|
<li class="mdl-menu__item"><a href="{% url 'logout' %}">Logout</a></li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</header> |
||||
|
<nav class="demo-navigation mdl-navigation mdl-color--blue-grey-800"> |
||||
|
<a class="mdl-navigation__link" href="{% url 'home' %}"><i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">home</i>Home</a> |
||||
|
<a class="mdl-navigation__link" href="{% url 'member_new' %}"><i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">person</i>New Member</a> |
||||
|
<a class="mdl-navigation__link" href="{% url 'members' %}"><i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">people</i>Members</a> |
||||
|
<div class="mdl-layout-spacer"></div> |
||||
|
{# <a class="mdl-navigation__link" href=""><i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">help_outline</i><span class="visuallyhidden">Help</span></a>#} |
||||
|
</nav> |
||||
|
</div> |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block content %} |
||||
|
<div id="root"></div> |
||||
|
{% render_bundle 'main' %} |
||||
|
{% endblock %} |
@ -0,0 +1,193 @@ |
|||||
|
{% extends 'dashboard.html' %} |
||||
|
{% load staticfiles %} |
||||
|
|
||||
|
{% block styles %} |
||||
|
<link rel="stylesheet" href="{% static 'vendor/md-date-time-picker/dist/css/mdDateTimePicker.min.css' %}"> |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block content %} |
||||
|
<div class="mdl-cell mdl-cell--8-col"> |
||||
|
<h1>New Membership</h1> |
||||
|
<form method="post"> |
||||
|
<formset> |
||||
|
{% csrf_token %} |
||||
|
{{ membership_form.member }} |
||||
|
<p>The Bridge City Bicycle Cooperative aims to be a safe and respectful environment geared towards education, empowerment and communitybuilding. In order to do so we need your input and support.</p> |
||||
|
|
||||
|
<h4>Member Privileges</h4> |
||||
|
<ul> |
||||
|
<li>Access to the BCBC tools, stands, and workspace</li> |
||||
|
<li>Access to friendly mechanical assistance and education when available</li> |
||||
|
<li>Opportunity to engage in decisions and help to build and develop the community's vision</li> |
||||
|
<li>The Bridge City Bicycle Cooperative (BCBC) values the trust of its volunteers, staff and members and is committed to protecting the privacy of all personal information entrusted to it. As such, collected information will be used in accordance with our privacy policy outlined on our website and in our shop.</li> |
||||
|
</ul> |
||||
|
{% if membership_form.non_field_errors %} |
||||
|
<div> |
||||
|
<span class="error">{{ membership_form.errors }}</span> |
||||
|
</div> |
||||
|
{% endif %} |
||||
|
<div> |
||||
|
<h4>Member Responsibilities</h4> |
||||
|
<h5>Respect and Maintaining a Safe Space</h5> |
||||
|
<ul> |
||||
|
<li>Respect others and self</li> |
||||
|
<li>Help others</li> |
||||
|
<li>Racist, ableist, ageist, homophobic, sexist, and classist behavior and language will not be tolerated</li> |
||||
|
<li>The BCBC seeks to build a healthy lifestyle community, and behavior seen as hindering this objective will not be tolerated</li> |
||||
|
<li>Ask for help: With tools, processes, and even emotions</li> |
||||
|
</ul> |
||||
|
<label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="{{ membership_form.safe_space.id_for_label }}"> |
||||
|
{{ membership_form.safe_space }} |
||||
|
<span class="mdl-checkbox__label">I acknowledge the BCBC is a safe space and agree to maintain it.</span> |
||||
|
</label> |
||||
|
</div> |
||||
|
<div> |
||||
|
<h5>Respect the Community</h5> |
||||
|
<ul> |
||||
|
<li>Build positive relationships with Community Members</li> |
||||
|
<li>Build positive relationships with CNYC employees, volunteers, and patrons</li> |
||||
|
</ul> |
||||
|
<label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="{{ membership_form.respect_community.id_for_label }}"> |
||||
|
{{ membership_form.respect_community }} |
||||
|
<span class="mdl-checkbox__label">I will respect the community.</span> |
||||
|
</label> |
||||
|
</div> |
||||
|
<div> |
||||
|
<h5>Giving back</h5> |
||||
|
<p> |
||||
|
Our services are free and Members are encouraged to contribute in any way they can. Our vibrancy comes from the volunteer work of a large community with diverse skills and passions. There are so many ways to be a part of this community, regardless of whether or not you know how to change a tire (yet!). Ask how you can help out or get in touch with our volunteer coordinator (<a href="mailto:volunteer@bridgecitybicyclecoop.com">volunteer@bridgecitybicyclecoop.com</a>) |
||||
|
</p> |
||||
|
<label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="{{ membership_form.give_back.id_for_label }}"> |
||||
|
{{ membership_form.give_back }} |
||||
|
<span class="mdl-checkbox__label">I acknowledge that giving back is important</span> |
||||
|
</label> |
||||
|
</div> |
||||
|
<div> |
||||
|
<h5>Respect the Space</h5> |
||||
|
<ul> |
||||
|
<li>Replace tools when not using them so that others can play too.</li> |
||||
|
<li>Do not steal or borrow articles within the space for personal use.</li> |
||||
|
<li>If you don't know what it is, how to use it, or where it goes, ask someone</li> |
||||
|
<li>Ensure you always leave time to clean up after yourself and others, and then do so</li> |
||||
|
<li>If you have to leave in the middle of a project, put pieces into a small bag and attach it to the bike</li> |
||||
|
<li>Personal projects may be kept at the BCBC for a maximum of one day; however, this is highly discouraged due to lack of space and the concern for theft.</li> |
||||
|
<li>Label your bike with your name, phone number, and the last date you worked on it.</li> |
||||
|
<li>Do not force tools and use them only for their intended use. If you need help or guidance, ask someone! It’s what we’re here for!</li> |
||||
|
</ul> |
||||
|
<label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="{{ membership_form.respect_shop.id_for_label }}"> |
||||
|
{{ membership_form.respect_shop }} |
||||
|
<span class="mdl-checkbox__label">I will respect the shop.</span> |
||||
|
</label> |
||||
|
</div> |
||||
|
<div class=""> |
||||
|
<h2 class="template__header mdl-typography--title">Voluntary Self Identification</h2> |
||||
|
<p>We want to make sure that all members of our community, regardless of race, ethnicity, and gender |
||||
|
are able to participate fully in the BCBC. Please share information about your race and/or |
||||
|
ethnicity so that we can track how well we are including all communities and whether there may be |
||||
|
barriers to certain groups’ participation. Thank you! Do you identify as: (In each category, check |
||||
|
all that apply)</p> |
||||
|
{% for checkbox in membership_form.self_identification %} |
||||
|
<label class="mdl-radio mdl-js-radio mdl-js-ripple-effect" for="{{ checkbox.id_for_label }}"> |
||||
|
{{ checkbox }} |
||||
|
<span class="mdl-radio__label">{{ checkbox.label }}</span> |
||||
|
</label> |
||||
|
{% endfor %} |
||||
|
{% if membership_form.self_identification.errors %} |
||||
|
<span class="mdl-textfield__error">{{ membership_form.self_identification.errors }}</span> |
||||
|
{% else %} |
||||
|
<span class="mdl-textfield__error">Hmm</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if membership_form.gender_other.errors %}is-invalid{% endif %}"> |
||||
|
{{ membership_form.self_ident_other }} |
||||
|
<label class="mdl-textfield__label" for="{{ membership_form.self_ident_other.id_for_label }}">{{ membership_form.self_ident_other.label }}</label> |
||||
|
{% if membership_form.self_ident_other.errors %} |
||||
|
<span class="mdl-textfield__error">{{ membership_form.self_ident_other.errors }}</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class=""> |
||||
|
<h3 class="template__header mdl-typography--body-2">Gender Identification</h3> |
||||
|
{% for checkbox in membership_form.gender %} |
||||
|
<label class="mdl-radio mdl-js-radio mdl-js-ripple-effect" for="{{ checkbox.id_for_label }}"> |
||||
|
{{ checkbox }} |
||||
|
<span class="mdl-radio__label">{{ checkbox.label }}</span> |
||||
|
</label> |
||||
|
{% endfor %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if membership_form.gender_other.errors %}is-invalid{% endif %}"> |
||||
|
{{ membership_form.gender_other }} |
||||
|
<label class="mdl-textfield__label" for="{{ membership_form.gender_other.id_for_label }}">{{ membership_form.gender_other.label }}</label> |
||||
|
{% if membership_form.gender_other.errors %} |
||||
|
<span class="mdl-textfield__error">{{ membership_form.gender_other.errors }}</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if membership_form.renewed_at.errors %}is-invalid{% endif %}"> |
||||
|
{{ membership_form.renewed_at }} |
||||
|
<label class="mdl-textfield__label" for="{{ membership_form.renewed_at.id_for_label }}">{{ membership_form.renewed_at.label }}</label> |
||||
|
{% if membership_form.renewed_at %} |
||||
|
<span class="mdl-textfield__error">{{ membership_form.renewed_at.errors }}</span> |
||||
|
{% else %} |
||||
|
<span class="mdl-textfield__error">Incorrect date.</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
</formset> |
||||
|
<formset> |
||||
|
<div class=""> |
||||
|
<h3 class="template__header">Payment Type</h3> |
||||
|
{% for checkbox in payment_form.type %} |
||||
|
<label class="mdl-radio mdl-js-radio mdl-js-ripple-effect" for="{{ checkbox.id_for_label }}"> |
||||
|
{{ checkbox }} |
||||
|
<span class="mdl-radio__label">{{ checkbox.label }}</span> |
||||
|
</label> |
||||
|
{% endfor %} |
||||
|
</div> |
||||
|
</formset> |
||||
|
<div> |
||||
|
<button disabled="true" id="submit" type="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored">Submit</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block scripts %} |
||||
|
<script src="{% static 'vendor/moment/min/moment.min.js' %}"></script> |
||||
|
<script src="{% static 'vendor/draggabilly/dist/draggabilly.pkgd.min.js' %}"></script> |
||||
|
<script src="{% static 'vendor/md-date-time-picker/dist/js/mdDateTimePicker.min.js' %}"></script> |
||||
|
<script> |
||||
|
var renewedAt = new mdDateTimePicker.default({ |
||||
|
type: 'date', |
||||
|
past: moment().subtract(100, 'years'), |
||||
|
trigger: document.getElementById('{{ membership_form.renewed_at.id_for_label }}') |
||||
|
}); |
||||
|
document.getElementById('{{ membership_form.renewed_at.id_for_label }}').addEventListener('focus', function() { |
||||
|
console.log('Toggle!'); |
||||
|
renewedAt.toggle(); |
||||
|
}); |
||||
|
document.getElementById('{{ membership_form.renewed_at.id_for_label }}').addEventListener('onOk', function () { |
||||
|
console.log('onOk'); |
||||
|
this.parentNode.classList.add('is-dirty'); |
||||
|
this.value = renewedAt.time.format('YYYY-MM-DD'); |
||||
|
}) |
||||
|
|
||||
|
var responsibilities = [ |
||||
|
document.getElementById('{{ membership_form.safe_space.id_for_label }}'), |
||||
|
document.getElementById('{{ membership_form.respect_community.id_for_label }}'), |
||||
|
document.getElementById('{{ membership_form.give_back.id_for_label }}'), |
||||
|
document.getElementById('{{ membership_form.respect_shop.id_for_label }}') |
||||
|
]; |
||||
|
|
||||
|
var checkResponsiblities = function () { |
||||
|
allAgreed = responsibilities.every(function (checkbox) { |
||||
|
return checkbox.checked |
||||
|
}); |
||||
|
var submitButton = document.getElementById('submit'); |
||||
|
submitButton.disabled = !allAgreed; |
||||
|
}; |
||||
|
|
||||
|
document.addEventListener("DOMContentLoaded", checkResponsiblities); |
||||
|
|
||||
|
responsibilities.forEach(function (checkbox) { |
||||
|
checkbox.addEventListener('click', checkResponsiblities) |
||||
|
}) |
||||
|
</script> |
||||
|
{% endblock %} |
@ -1,6 +1,7 @@ |
|||||
from django.conf.urls import url |
from django.conf.urls import url |
||||
|
|
||||
from .views import DashboardView |
from .views import DashboardView, NewMembershipView |
||||
urlpatterns = [ |
urlpatterns = [ |
||||
url(r'^', DashboardView.as_view()), |
url(r'^member/(?P<member_id>[0-9]+)/membership/new/$', NewMembershipView.as_view(), name='new_membership'), |
||||
|
url(r'^$', DashboardView.as_view(), name='home') |
||||
] |
] |
||||
|
@ -1,7 +1,45 @@ |
|||||
|
import logging |
||||
|
|
||||
|
from django.contrib import messages |
||||
|
from django.core.urlresolvers import reverse |
||||
|
from django.http import HttpResponseRedirect |
||||
from django.template.response import TemplateResponse |
from django.template.response import TemplateResponse |
||||
from django.views.generic import View |
from django.views.generic import TemplateView, View |
||||
|
from django.utils.decorators import method_decorator |
||||
|
from django.contrib.auth.decorators import login_required |
||||
|
|
||||
|
from registration.models import Member |
||||
|
|
||||
|
from .forms import MembershipForm, PaymentForm |
||||
|
|
||||
|
logger = logging.getLogger(__name__) |
||||
|
|
||||
|
|
||||
|
@method_decorator(login_required, name='dispatch') |
||||
class DashboardView(View): |
class DashboardView(View): |
||||
def get(self, request): |
def get(self, request): |
||||
return TemplateResponse(request, 'base.html') |
return TemplateResponse(request, 'dashboard.html') |
||||
|
|
||||
|
@method_decorator(login_required, name='dispatch') |
||||
|
class NewMembershipView(TemplateView): |
||||
|
template_name = 'membership_form.html' |
||||
|
|
||||
|
def get(self, request, member_id): |
||||
|
membership_form = MembershipForm(initial=dict(member=member_id)) |
||||
|
payment_form = PaymentForm() |
||||
|
return self.render_to_response(dict(membership_form=membership_form, payment_form=payment_form)) |
||||
|
|
||||
|
def post(self, request, member_id): |
||||
|
membership_form = MembershipForm(request.POST, initial=dict(member=member_id)) |
||||
|
payment_form = PaymentForm(request.POST) |
||||
|
member = Member.objects.get(id=member_id) |
||||
|
|
||||
|
if membership_form.is_valid() and payment_form.is_valid(): |
||||
|
new_payment = payment_form.save() |
||||
|
new_membership = membership_form.save() |
||||
|
new_membership.payment = new_payment |
||||
|
new_membership.save() |
||||
|
messages.add_message(request, messages.SUCCESS, 'Successfully created our newest member, {first} {last}' |
||||
|
.format(first=member.first_name, last=member.last_name)) |
||||
|
return HttpResponseRedirect(reverse('member_edit', kwargs=dict(member_id=member_id))) |
||||
|
return self.render_to_response(dict(membership_form=membership_form, payment_form=payment_form)) |
||||
|
@ -0,0 +1,49 @@ |
|||||
|
{ |
||||
|
"name": "workstand", |
||||
|
"version": "0.0.1", |
||||
|
"description": "A membership management app for the BCBC.", |
||||
|
"main": "index.js", |
||||
|
"scripts": { |
||||
|
"build": "node_modules/.bin/webpack --config webpack.config.js --progress --colors", |
||||
|
"build-production": "node_modules/.bin/webpack --config webpack.prod.config.js --progress --colors", |
||||
|
"watch": "node server.js" |
||||
|
}, |
||||
|
"author": "", |
||||
|
"license": "ISC", |
||||
|
"dependencies": { |
||||
|
"es6-promise": "^3.2.1", |
||||
|
"isomorphic-fetch": "^2.2.1", |
||||
|
"material-ui": "^0.16.6", |
||||
|
"moment": "^2.13.0", |
||||
|
"react": "^15.4.1", |
||||
|
"react-dom": "^15.4.1", |
||||
|
"react-tap-event-plugin": "^2.0.1", |
||||
|
"webpack": "^1.13.1", |
||||
|
"webpack-bundle-tracker": "0.0.93" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"babel": "^6.5.2", |
||||
|
"babel-core": "^6.9.1", |
||||
|
"babel-loader": "^6.2.4", |
||||
|
"babel-preset-es2015": "^6.9.0", |
||||
|
"babel-preset-react": "^6.5.0", |
||||
|
"babel-preset-stage-0": "^6.5.0", |
||||
|
"css-loader": "^0.23.1", |
||||
|
"extract-text-webpack-plugin": "^1.0.1", |
||||
|
"i": "^0.3.5", |
||||
|
"node-sass": "^3.4.2", |
||||
|
"normalize.css": "^4.1.1", |
||||
|
"postcss-loader": "^0.9.1", |
||||
|
"react-addons-css-transition-group": "^15.4.1", |
||||
|
"react-hot-loader": "^1.3.0", |
||||
|
"react-toolbox": "^0.16.2", |
||||
|
"sass-loader": "^3.2.0", |
||||
|
"style-loader": "^0.13.1", |
||||
|
"toolbox-loader": "0.0.3", |
||||
|
"webpack-dev-server": "^1.14.1", |
||||
|
"eslint": "^3.9.1", |
||||
|
"eslint-plugin-jsx-a11y": "^2.2.3", |
||||
|
"eslint-plugin-import": "^2.1.0", |
||||
|
"eslint-plugin-react": "^6.6.0" |
||||
|
} |
||||
|
} |
@ -1,3 +1,38 @@ |
|||||
from django.contrib import admin |
from django.contrib import admin |
||||
|
from .models import CustomUser, Member |
||||
|
from django.contrib.auth.admin import UserAdmin |
||||
|
from django.contrib.auth.forms import UserChangeForm |
||||
|
|
||||
# Register your models here. |
|
||||
|
class CustomUserChangeForm(UserChangeForm): |
||||
|
class Meta(UserChangeForm.Meta): |
||||
|
model = CustomUser |
||||
|
|
||||
|
|
||||
|
@admin.register(CustomUser) |
||||
|
class CustomUserAdmin(UserAdmin): |
||||
|
form = CustomUserChangeForm |
||||
|
|
||||
|
fieldsets = ( |
||||
|
(None, {'fields': ('email', 'password')}), |
||||
|
('Permissions', {'fields': ('is_active', 'is_superuser', |
||||
|
'groups', 'user_permissions')}), |
||||
|
('Important dates', {'fields': ('last_login',)}), |
||||
|
) |
||||
|
add_fieldsets = ( |
||||
|
(None, { |
||||
|
'classes': ('wide',), |
||||
|
'fields': ('email', 'password1', 'password2'), |
||||
|
}), |
||||
|
) |
||||
|
ordering = ('email',) |
||||
|
list_display = ('email',) |
||||
|
list_filter = ('is_superuser', 'is_active', 'groups') |
||||
|
search_fields = ('email',) |
||||
|
|
||||
|
|
||||
|
@admin.register(Member) |
||||
|
class MemberAdmin(admin.ModelAdmin): |
||||
|
list_display = ('get_full_name',) |
||||
|
ordering = ('last_name',) |
||||
|
search_fields = ('email', 'first_name', 'last_name') |
@ -0,0 +1,47 @@ |
|||||
|
from django.forms import ModelForm, EmailInput, TextInput, DateInput, CheckboxSelectMultiple, CharField, CheckboxInput, BooleanField |
||||
|
from django.utils import timezone |
||||
|
from registration.models import Member |
||||
|
|
||||
|
|
||||
|
class MemberForm(ModelForm): |
||||
|
waiver_substitute = BooleanField(required=False, label='I have read and agree to the above terms & conditions.', widget=CheckboxInput(attrs={'class': 'mdl-checkbox__input'})) |
||||
|
|
||||
|
class Meta: |
||||
|
model = Member |
||||
|
|
||||
|
exclude = ('waiver',) |
||||
|
fields = ['email', 'email_consent', 'first_name', 'last_name', 'preferred_name', 'date_of_birth', |
||||
|
'guardian_name', 'phone', 'street', 'city', 'province', 'country', 'post_code', 'waiver'] |
||||
|
widgets = { |
||||
|
'email': EmailInput(attrs={'class': 'mdl-textfield__input'}), |
||||
|
'email_consent': CheckboxInput(attrs={'class': 'mdl-checkbox__input'}), |
||||
|
'first_name': TextInput(attrs={'class': 'mdl-textfield__input'}), |
||||
|
'last_name': TextInput(attrs={'class': 'mdl-textfield__input'}), |
||||
|
'preferred_name': TextInput(attrs={'class': 'mdl-textfield__input'}), |
||||
|
'date_of_birth': DateInput(attrs={'class': 'mdl-textfield__input'}), |
||||
|
'guardian_name': DateInput(attrs={'class': 'mdl-textfield__input', 'disabled': 'disabled'}), |
||||
|
'phone': TextInput(attrs={'class': 'mdl-textfield__input', 'pattern': '[0-9]*'}), |
||||
|
'street': TextInput(attrs={'class': 'mdl-textfield__input'}), |
||||
|
'city': TextInput(attrs={'class': 'mdl-textfield__input'}), |
||||
|
'province': TextInput(attrs={'class': 'mdl-textfield__input'}), |
||||
|
'country': TextInput(attrs={'class': 'mdl-textfield__input'}), |
||||
|
'post_code': TextInput(attrs={'class': 'mdl-textfield__input', |
||||
|
'pattern': '[A-Za-z][0-9][A-Za-z] [0-9][A-Za-z][0-9]'}), |
||||
|
} |
||||
|
|
||||
|
labels = { |
||||
|
'email_consent': 'I consent to receiving digital communication from the BCBC.' |
||||
|
} |
||||
|
|
||||
|
def clean(self): |
||||
|
super(MemberForm, self).clean() |
||||
|
|
||||
|
def save(self, *args, **kwargs): |
||||
|
commit = kwargs.pop('commit', True) |
||||
|
|
||||
|
instance = super(MemberForm, self).save(*args, commit=False, **kwargs) |
||||
|
if self.cleaned_data['waiver_substitute']: |
||||
|
instance.waiver = timezone.now() |
||||
|
if commit: |
||||
|
instance.save() |
||||
|
return instance |
@ -0,0 +1,19 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Generated by Django 1.9.7 on 2016-11-30 01:57 |
||||
|
from __future__ import unicode_literals |
||||
|
|
||||
|
from django.db import migrations |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
('registration', '0001_initial'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.AlterModelOptions( |
||||
|
name='customuser', |
||||
|
options={'verbose_name': 'User', 'verbose_name_plural': 'Users'}, |
||||
|
), |
||||
|
] |
@ -0,0 +1,9 @@ |
|||||
|
from haystack import indexes |
||||
|
from .models import Member |
||||
|
|
||||
|
|
||||
|
class MemberIndex(indexes.SearchIndex, indexes.Indexable): |
||||
|
text = indexes.EdgeNgramField(document=True, use_template=True) |
||||
|
|
||||
|
def get_model(self): |
||||
|
return Member |
@ -0,0 +1,221 @@ |
|||||
|
{% extends 'dashboard.html' %} |
||||
|
{% load staticfiles %} |
||||
|
|
||||
|
{% block styles %} |
||||
|
<link rel="stylesheet" href="{% static 'vendor/md-date-time-picker/dist/css/mdDateTimePicker.min.css' %}"> |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block scripts %} |
||||
|
<script src="{% static 'vendor/moment/min/moment.min.js' %}"></script> |
||||
|
<script src="{% static 'vendor/object.observe/dist/object-observe-lite.min.js' %}"></script> |
||||
|
<script src="{% static 'vendor/draggabilly/dist/draggabilly.pkgd.min.js' %}"></script> |
||||
|
<script src="{% static 'vendor/md-date-time-picker/dist/js/mdDateTimePicker.min.js' %}"></script> |
||||
|
<script> |
||||
|
var dateOfBirth = new mdDateTimePicker.default({ |
||||
|
type: 'date', |
||||
|
past: moment().subtract(100, 'years') |
||||
|
}); |
||||
|
document.getElementById('{{ form.date_of_birth.id_for_label }}').addEventListener('focus', function() { |
||||
|
dateOfBirth.toggle(); |
||||
|
}); |
||||
|
Object.observe(dateOfBirth, function(changes) { |
||||
|
var input = document.getElementById('{{ form.date_of_birth.id_for_label }}'); |
||||
|
input.value = dateOfBirth.time().format('YYYY-MM-DD'); |
||||
|
input.parentNode.classList.add('is-dirty'); |
||||
|
|
||||
|
var threshold = moment.duration(18, 'years'); |
||||
|
var dob = dateOfBirth.time().clone(); |
||||
|
|
||||
|
if (dob.add(threshold).isAfter(moment())) { |
||||
|
document.getElementById('{{ form.guardian_name.id_for_label }}').disabled = false; |
||||
|
document.getElementById('{{ form.guardian_name.id_for_label }}').parentNode.classList.remove('is-disabled'); |
||||
|
} else { |
||||
|
document.getElementById('{{ form.guardian_name.id_for_label }}').disabled = true; |
||||
|
document.getElementById('{{ form.guardian_name.id_for_label }}').parentNode.classList.add('is-disabled'); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
var waiverCheckBox = document.getElementById('{{ form.waiver_substitute.id_for_label }}'); |
||||
|
var submitButton = document.getElementById('submit'); |
||||
|
var requiredCheckboxes = function() { |
||||
|
return waiverCheckBox.checked; |
||||
|
}; |
||||
|
|
||||
|
waiverCheckBox.addEventListener('change', function() { |
||||
|
if (requiredCheckboxes()) { |
||||
|
submitButton.disabled = false; |
||||
|
} |
||||
|
else { |
||||
|
submitButton.disabled = true; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// On page load check on the check boxes |
||||
|
if (requiredCheckboxes()) { |
||||
|
submitButton.disabled = false; |
||||
|
} |
||||
|
|
||||
|
</script> |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block content %} |
||||
|
<div class="mdl-cell mdl-cell--8-col"> |
||||
|
<h1>{{ form.instance.first_name }} {{ form.instance.last_name }}</h1> |
||||
|
<p> |
||||
|
The Bridge City Bicycle Co-operative (herein referred to as The BCBC and The Community) is a nonprofit, |
||||
|
community bicycle repair education and resource co-operative. We offer our members nonjudgmental repair |
||||
|
space, tools and instruction during business hours (hours on website) by donation, and educational |
||||
|
workshops. We also offer reconditioned/recycled low cost bikes and parts for sale. |
||||
|
The BCBC is operated by volunteers; a medley of professionals, students, bike enthusiasts, activists, |
||||
|
and other community members who share a love for cycling in Saskatoon. Membership is open to all |
||||
|
individuals and costs $20 per year. A receipt will be issued to you once your membership fee has been paid. |
||||
|
</p> |
||||
|
<form method="post"> |
||||
|
{% csrf_token %} |
||||
|
|
||||
|
{% if form.non_field_errors %} |
||||
|
<div> |
||||
|
<span class="error">{{ form.errors }}</span> |
||||
|
</div> |
||||
|
{% endif %} |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.email.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.email }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.email.id_for_label }}">{{ form.email.label }}</label> |
||||
|
{% if form.email.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.email.errors }}</span> |
||||
|
{% else %} |
||||
|
<span class="mdl-textfield__error">Invalid email.</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div> |
||||
|
<label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="{{ form.email_consent.id_for_label }}"> |
||||
|
{{ form.email_consent }} |
||||
|
<span class="mdl-checkbox__label">{{ form.email_consent.label }}</span> |
||||
|
</label> |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.first_name.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.first_name }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.first_name.id_for_label }}">{{ form.first_name.label }}</label> |
||||
|
{% if form.first_name.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.first_name.errors }}</span> |
||||
|
{% else %} |
||||
|
<span class="mdl-textfield__error">Name too long.</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.last_name.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.last_name }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.last_name.id_for_label }}">{{ form.last_name.label }}</label> |
||||
|
{% if form.last_name.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.last_name.errors }}</span> |
||||
|
{% else %} |
||||
|
<span class="mdl-textfield__error">Name too long.</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.preferred_name.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.preferred_name }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.preferred_name.id_for_label }}">{{ form.preferred_name.label }}</label> |
||||
|
{% if form.preferred_name.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.preferred_name.errors }}</span> |
||||
|
{% else %} |
||||
|
<span class="mdl-textfield__error">Name too long.</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.date_of_birth.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.date_of_birth }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.date_of_birth.id_for_label }}">{{ form.date_of_birth.label }}</label> |
||||
|
{% if form.date_of_birth.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.date_of_birth.errors }}</span> |
||||
|
{% else %} |
||||
|
<span class="mdl-textfield__error">Incorrect date.</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.guardian_name.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.guardian_name }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.guardian_name.id_for_label }}">{{ form.guardian_name.label }}</label> |
||||
|
{% if form.guardian_name.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.guardian_name.errors }}</span> |
||||
|
{% else %} |
||||
|
<span class="mdl-textfield__error">Name too long.</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.phone.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.phone }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.phone.id_for_label }}">{{ form.phone.label }}</label> |
||||
|
{% if form.phone.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.phone.errors }}</span> |
||||
|
{% else %} |
||||
|
<span class="mdl-textfield__error">Digits only.</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.post_code.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.post_code }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.post_code.id_for_label }}">{{ form.post_code.label }}</label> |
||||
|
{% if form.post_code.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.post_code.errors }}</span> |
||||
|
{% else %} |
||||
|
<span class="mdl-textfield__error">Format: A0A 0A0</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.street.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.street }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.street.id_for_label }}">{{ form.street.label }}</label> |
||||
|
{% if form.street.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.street.errors }}</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.city.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.city }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.city.id_for_label }}">{{ form.city.label }}</label> |
||||
|
{% if form.city.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.city.errors }}</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.province.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.province }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.province.id_for_label }}">{{ form.province.label }}</label> |
||||
|
{% if form.province.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.province.errors }}</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.country.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.country }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.country.id_for_label }}">{{ form.country.label }}</label> |
||||
|
{% if form.country.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.country.errors }}</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div> |
||||
|
<button id="submit" type="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored">Submit</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
<div class="mdl-cell mdl-cell--8-col"> |
||||
|
{% if form.instance.memberships %} |
||||
|
<table class="mdl-data-table mdl-js-data-table"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th class="mdl-data-table__cell--non-numeric">Renewed at</th> |
||||
|
<th class="mdl-data-table__cell--non-numeric">Created at</th> |
||||
|
<th class="mdl-data-table__cell--non-numeric">Expires at</th> |
||||
|
<th class="mdl-data-table__cell--non-numeric">Paid by</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
<h3>Membership History</h3> |
||||
|
{% for membership in form.instance.memberships.all %} |
||||
|
<tr> |
||||
|
<td class="mdl-data-table__cell--non-numeric">{{ membership.renewed_at }}</td> |
||||
|
<td class="mdl-data-table__cell--non-numeric">{{ membership.created_at }}</td> |
||||
|
<td class="mdl-data-table__cell--non-numeric">{{ membership.expires_at }}</td> |
||||
|
<td class="mdl-data-table__cell--non-numeric">{{ membership.payment.type }}</td> |
||||
|
</tr> |
||||
|
{% endfor %} |
||||
|
</tbody> |
||||
|
</table> |
||||
|
{% else %} |
||||
|
<h3>No membership found.</h3> |
||||
|
{% endif %} |
||||
|
<a class="mdl-button mdl-js-button mdl-button--flat mdl-js-ripple-effect mdl-button--colored" href="{% url 'new_membership' member_id=member.id %}">Add membership</a> |
||||
|
</div> |
||||
|
|
||||
|
{% endblock %} |
@ -0,0 +1,67 @@ |
|||||
|
{% extends 'base.html' %} |
||||
|
|
||||
|
{% load staticfiles compress %} |
||||
|
|
||||
|
{% block styles %} |
||||
|
{{ block.super }} |
||||
|
{% compress css %} |
||||
|
<link href="{% static 'vendor/material-design-lite/src/icon-toggle/_icon-toggle.scss' %}" rel="stylesheet" type="text/x-scss"> |
||||
|
{% endcompress %} |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block scripts %} |
||||
|
{{ block.super }} |
||||
|
<script> |
||||
|
var passwordToggle = document.getElementById('icon-toggle-1'); |
||||
|
passwordToggle.addEventListener('change', function () { |
||||
|
toggle(); |
||||
|
}); |
||||
|
|
||||
|
var passwordField = document.getElementById('password'); |
||||
|
|
||||
|
var toggle = function () { |
||||
|
if (passwordToggle.checked) { |
||||
|
passwordField.type = 'password'; |
||||
|
passwordToggle.nextElementSibling.textContent = 'visibility_off' |
||||
|
} |
||||
|
else { |
||||
|
passwordField.type = 'text'; |
||||
|
passwordToggle.nextElementSibling.textContent = 'visibility' |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
{% endblock %} |
||||
|
|
||||
|
|
||||
|
|
||||
|
{% block content %} |
||||
|
<div class="demo-graphs mdl-shadow--2dp mdl-color--white mdl-cell mdl-cell--8-col mdl-shadow--2dp mdl-cell--2-offset"> |
||||
|
<h2 class="mdl-card__title-text">Login</h2> |
||||
|
<form method="post"> |
||||
|
{% if form.non_field_errors %} |
||||
|
<div> |
||||
|
<span class="error">{{ form.non_field_errors }}</span> |
||||
|
</div> |
||||
|
{% endif %} |
||||
|
{% csrf_token %} |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label"> |
||||
|
<input class="mdl-textfield__input" type="text" id="username" name="username"> |
||||
|
<label class="mdl-textfield__label" for="username">Email address</label> |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label"> |
||||
|
<input class="mdl-textfield__input" type="password" id="password" name="password"> |
||||
|
<label class="mdl-textfield__label" for="password">Password</label> |
||||
|
</div> |
||||
|
<label class="mdl-icon-toggle mdl-js-icon-toggle mdl-js-ripple-effect" for="icon-toggle-1"> |
||||
|
<input type="checkbox" id="icon-toggle-1" class="mdl-icon-toggle__input" checked> |
||||
|
<i class="mdl-icon-toggle__label material-icons">visibility_off</i> |
||||
|
</label> |
||||
|
<div> |
||||
|
<button type="submit" class="mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect"> |
||||
|
Login |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
|
||||
|
</div> |
||||
|
{% endblock %} |
@ -0,0 +1,238 @@ |
|||||
|
{% extends 'dashboard.html' %} |
||||
|
{% load staticfiles %} |
||||
|
|
||||
|
{% block styles %} |
||||
|
<link rel="stylesheet" href="{% static 'vendor/md-date-time-picker/dist/css/mdDateTimePicker.min.css' %}"> |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block scripts %} |
||||
|
<script src="{% static 'vendor/moment/min/moment.min.js' %}"></script> |
||||
|
<script src="{% static 'vendor/object.observe/dist/object-observe-lite.min.js' %}"></script> |
||||
|
<script src="{% static 'vendor/draggabilly/dist/draggabilly.pkgd.min.js' %}"></script> |
||||
|
<script src="{% static 'vendor/md-date-time-picker/dist/js/mdDateTimePicker.min.js' %}"></script> |
||||
|
<script> |
||||
|
var dateOfBirth = new mdDateTimePicker.default({ |
||||
|
type: 'date', |
||||
|
past: moment().subtract(100, 'years') |
||||
|
}); |
||||
|
document.getElementById('{{ form.date_of_birth.id_for_label }}').addEventListener('focus', function() { |
||||
|
dateOfBirth.toggle(); |
||||
|
}); |
||||
|
Object.observe(dateOfBirth, function(changes) { |
||||
|
var input = document.getElementById('{{ form.date_of_birth.id_for_label }}'); |
||||
|
input.value = dateOfBirth.time.format('YYYY-MM-DD'); |
||||
|
input.parentNode.classList.add('is-dirty'); |
||||
|
|
||||
|
var threshold = moment.duration(18, 'years'); |
||||
|
var dob = dateOfBirth.time.clone(); |
||||
|
|
||||
|
if (dob.add(threshold).isAfter(moment())) { |
||||
|
document.getElementById('{{ form.guardian_name.id_for_label }}').disabled = false; |
||||
|
document.getElementById('{{ form.guardian_name.id_for_label }}').parentNode.classList.remove('is-disabled'); |
||||
|
} else { |
||||
|
document.getElementById('{{ form.guardian_name.id_for_label }}').disabled = true; |
||||
|
document.getElementById('{{ form.guardian_name.id_for_label }}').parentNode.classList.add('is-disabled'); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
var waiverCheckBox = document.getElementById('{{ form.waiver_substitute.id_for_label }}'); |
||||
|
var submitButton = document.getElementById('submit'); |
||||
|
var requiredCheckboxes = function() { |
||||
|
return waiverCheckBox.checked; |
||||
|
}; |
||||
|
|
||||
|
waiverCheckBox.addEventListener('change', function() { |
||||
|
if (requiredCheckboxes()) { |
||||
|
submitButton.disabled = false; |
||||
|
} |
||||
|
else { |
||||
|
submitButton.disabled = true; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// On page load check on the check boxes |
||||
|
if (requiredCheckboxes()) { |
||||
|
submitButton.disabled = false; |
||||
|
} |
||||
|
|
||||
|
</script> |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block content %} |
||||
|
<div class="mdl-cell mdl-cell--8-col"> |
||||
|
<h1>New Contact</h1> |
||||
|
<p> |
||||
|
The Bridge City Bicycle Co-operative (herein referred to as The BCBC and The Community) is a nonprofit, |
||||
|
community bicycle repair education and resource co-operative. We offer our members nonjudgmental repair |
||||
|
space, tools and instruction during business hours (hours on website) by donation, and educational |
||||
|
workshops. We also offer reconditioned/recycled low cost bikes and parts for sale. |
||||
|
The BCBC is operated by volunteers; a medley of professionals, students, bike enthusiasts, activists, |
||||
|
and other community members who share a love for cycling in Saskatoon. Membership is open to all |
||||
|
individuals and costs $20 per year. A receipt will be issued to you once your membership fee has been paid. |
||||
|
</p> |
||||
|
<form method="post"> |
||||
|
{% csrf_token %} |
||||
|
|
||||
|
{% if form.non_field_errors %} |
||||
|
<div> |
||||
|
{% for errors in form.non_field_errors %} |
||||
|
<span class="mdl-textfield__error">{{ error }}</span> |
||||
|
{% endfor %} |
||||
|
</div> |
||||
|
{% endif %} |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.email.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.email }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.email.id_for_label }}">{{ form.email.label }}</label> |
||||
|
{% if form.email.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.email.errors }}</span> |
||||
|
{% else %} |
||||
|
<span class="mdl-textfield__error">Invalid email.</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div> |
||||
|
<label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="{{ form.email_consent.id_for_label }}"> |
||||
|
{{ form.email_consent }} |
||||
|
<span class="mdl-checkbox__label">{{ form.email_consent.label }}</span> |
||||
|
</label> |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.first_name.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.first_name }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.first_name.id_for_label }}">{{ form.first_name.label }}</label> |
||||
|
{% if form.first_name.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.first_name.errors }}</span> |
||||
|
{% else %} |
||||
|
<span class="mdl-textfield__error">Name too long.</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.last_name.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.last_name }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.last_name.id_for_label }}">{{ form.last_name.label }}</label> |
||||
|
{% if form.last_name.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.last_name.errors }}</span> |
||||
|
{% else %} |
||||
|
<span class="mdl-textfield__error">Name too long.</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.preferred_name.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.preferred_name }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.preferred_name.id_for_label }}">{{ form.preferred_name.label }}</label> |
||||
|
{% if form.preferred_name.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.preferred_name.errors }}</span> |
||||
|
{% else %} |
||||
|
<span class="mdl-textfield__error">Name too long.</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.date_of_birth.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.date_of_birth }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.date_of_birth.id_for_label }}">{{ form.date_of_birth.label }}</label> |
||||
|
{% if form.date_of_birth.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.date_of_birth.errors }}</span> |
||||
|
{% else %} |
||||
|
<span class="mdl-textfield__error">Incorrect date.</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.guardian_name.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.guardian_name }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.guardian_name.id_for_label }}">{{ form.guardian_name.label }}</label> |
||||
|
{% if form.guardian_name.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.guardian_name.errors }}</span> |
||||
|
{% else %} |
||||
|
<span class="mdl-textfield__error">Name too long.</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.phone.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.phone }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.phone.id_for_label }}">{{ form.phone.label }}</label> |
||||
|
{% if form.phone.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.phone.errors }}</span> |
||||
|
{% else %} |
||||
|
<span class="mdl-textfield__error">Digits only.</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.post_code.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.post_code }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.post_code.id_for_label }}">{{ form.post_code.label }}</label> |
||||
|
{% if form.post_code.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.post_code.errors }}</span> |
||||
|
{% else %} |
||||
|
<span class="mdl-textfield__error">Format: A0A 0A0</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.street.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.street }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.street.id_for_label }}">{{ form.street.label }}</label> |
||||
|
{% if form.street.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.street.errors }}</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.city.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.city }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.city.id_for_label }}">{{ form.city.label }}</label> |
||||
|
{% if form.city.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.city.errors }}</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.province.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.province }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.province.id_for_label }}">{{ form.province.label }}</label> |
||||
|
{% if form.province.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.province.errors }}</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label {% if form.country.errors %}is-invalid{% endif %}"> |
||||
|
{{ form.country }} |
||||
|
<label class="mdl-textfield__label" for="{{ form.country.id_for_label }}">{{ form.country.label }}</label> |
||||
|
{% if form.country.errors %} |
||||
|
<span class="mdl-textfield__error">{{ form.country.errors }}</span> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
|
||||
|
<div class=""> |
||||
|
<h6 class="template__header mdl-typography--title">Liability Waiver</h6> |
||||
|
<p><strong>Children under the age of 18 must have a parent or guardian co-sign the following waiver form. |
||||
|
<br>Children under the age of 13 must have guardian supervision when participating in BCBC activities and events.</strong></p> |
||||
|
<p>By signing this form in the space provided below, I hereby assume all of the risks of participating and/or volunteering in the Bridge City Bicycle Co-operative, hereinafter referred to as the BCBC and the Community. I realize that liability may arise from negligence or carelessness on the part of the persons or entities being released, from dangerous or defective equipment or property owned, maintained or controlled by them or because of their possible liability without fault. I acknowledge that this Accident Waiver and Release of Liability form will be used by the Community, sponsors and organizers, in which I may participate and that it will govern my actions and responsibilities during my use of its services. In consideration of my application and permitting me to participate in this program, I hereby take action for myself, my executors, administrators, heirs, next of kin, successors, and assigns as follows:</p> |
||||
|
<p>(A) Waive, Release and Discharge from any and all liability for my death, disability, personal injury, property damage, property theft or actions of any kind which may hereafter accrue to me including my travelling to and from space or using the shop's bicycle, equipment or other facilities, THE FOLLOWING ENTITIES OR PERSONS: The directors, officers, employees, volunteers, representatives, and agents, the event holders, sponsors, volunteers of the Community;</p> |
||||
|
<p>(B) Indemnify and Hold Harmless the entities and persons set forth in (A) above from any and all liabilities and claims arising from my participation in the Community, including my use of a bicycle belonging to the Community, irrespective of whether the cause of the claims or liability arise from the negligence, acts or omissions of me, a third party, or the Community.</p> |
||||
|
<label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="{{ form.waiver_substitute.id_for_label }}"> |
||||
|
{{ form.waiver_substitute }} |
||||
|
<span class="mdl-checkbox__label">{{ form.waiver_substitute.label }}</span> |
||||
|
</label> |
||||
|
</div> |
||||
|
{# <div>#} |
||||
|
{# <h2 class="template__header mdl-typography--title">Privacy Policy</h2>#} |
||||
|
{# <p>Bridge City Bicycle Co-operative (BCBC) values the trust of its volunteers, staff, and members and#} |
||||
|
{# is committed to protecting the privacy of all personal information entrusted to it. BCBC only#} |
||||
|
{# collects the limited personal information needed to deliver high quality services and programming.#} |
||||
|
{# Collected information will be used only for the purpose expressly identified or for other purposes#} |
||||
|
{# which could be reasonably considered to be consistent with out mission. We do not sell, rent or#} |
||||
|
{# trade personal information. The personal information collected will be protected with appropriate#} |
||||
|
{# physical, organizational, and electronic safeguards to prevent unauthorized use and will be#} |
||||
|
{# retained only for as long as needed to achieve the purposes stated above. BCBC may make personal#} |
||||
|
{# information available to others or to appropriate authorities without permission if the information#} |
||||
|
{# is used to take action during an emergency that threatens the life, health or security of an#} |
||||
|
{# individual. Information no longer required will be destroyed or erased. Upon application to the#} |
||||
|
{# Privacy Officer individuals may access their personal information held by BCBC unless the#} |
||||
|
{# information contains references to other individuals or cannot be disclosed for legal or security#} |
||||
|
{# reasons. BCBC commits to promptly correcting any inaccuracies. Complaints should be made in writing#} |
||||
|
{# to the Privacy Officer who will immediately acknowledge receipt and will respond to the complaint#} |
||||
|
{# within 30 days. Unresolved complaints may be taken to the federal Privacy Commissioner.</p>#} |
||||
|
{# <p>Contact BCBC’s Privacy Officer at:#} |
||||
|
{# <br>905 20th Street West Saskatoon, SK S7M 0Y5 or by:#} |
||||
|
{# <br>Email: <a href="mailto:bridgecitybicyclecoop@gmail.com">bridgecitybicyclecoop@gmail.com</a></p>#} |
||||
|
{# </div>#} |
||||
|
{# <div>#} |
||||
|
{# <h2 class="template__header mdl-typography--title">Membership and Privileges</h2>#} |
||||
|
{# <p>I acknowledge that my Membership and Privileges in the Bridge City Bicycle Co-operative is contingent#} |
||||
|
{# upon fulfilling the above responsibilities.</p>#} |
||||
|
{# <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="{{ form.priveleges.id_for_label }}">#} |
||||
|
{# {{ form.priveleges }}#} |
||||
|
{# <span class="mdl-checkbox__label">{{ form.priveleges.label }}</span>#} |
||||
|
{# </label>#} |
||||
|
{# </div>#} |
||||
|
<div> |
||||
|
<button disabled="true" id="submit" type="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored">Submit</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
{% endblock %} |
@ -0,0 +1,22 @@ |
|||||
|
{% extends 'dashboard.html' %} |
||||
|
|
||||
|
{% block content %} |
||||
|
<div class="mdl-cell mdl-cell--6-col"> |
||||
|
<ul class="demo-list-three mdl-list"> |
||||
|
{% for member in members %} |
||||
|
<li class="mdl-list__item mdl-list__item--three-line"> |
||||
|
<span class="mdl-list__item-primary-content"> |
||||
|
<i class="material-icons mdl-list__item-avatar">person</i> |
||||
|
<span>{{ member.full_name }}</span> |
||||
|
<span class="mdl-list__item-text-body"> |
||||
|
{{ member.email }} |
||||
|
</span> |
||||
|
</span> |
||||
|
<span class="mdl-list__item-secondary-content"> |
||||
|
<a class="mdl-list__item-secondary-action" href="{% url 'member_edit' member_id=member.id %}"><i class="material-icons">edit</i></a> |
||||
|
</span> |
||||
|
</li> |
||||
|
{% endfor %} |
||||
|
</ul> |
||||
|
</div> |
||||
|
{% endblock %} |
@ -0,0 +1,2 @@ |
|||||
|
{{ object.email }} |
||||
|
{{ object.get_full_name }} |
@ -1,3 +0,0 @@ |
|||||
from django.test import TestCase |
|
||||
|
|
||||
# Create your tests here. |
|
@ -0,0 +1,50 @@ |
|||||
|
from django.test import TestCase |
||||
|
|
||||
|
from ..models import CustomUser, Member |
||||
|
|
||||
|
|
||||
|
class TestCustomUserManager(TestCase): |
||||
|
def test_create_user(self): |
||||
|
new_user = CustomUser.objects.create_user('test@example.com') |
||||
|
self.assertTrue(new_user.pk) |
||||
|
|
||||
|
def test_create_user_no_email(self): |
||||
|
with self.assertRaises(ValueError): |
||||
|
CustomUser.objects.create_user(email=None) |
||||
|
|
||||
|
def test_create_superuser(self): |
||||
|
new_user = CustomUser.objects\ |
||||
|
.create_superuser(email='super@example.com', password='password') |
||||
|
self.assertTrue(new_user.is_admin) |
||||
|
self.assertTrue(new_user.is_staff) |
||||
|
self.assertTrue(new_user.check_password('password')) |
||||
|
self.assertTrue(new_user.pk) |
||||
|
|
||||
|
|
||||
|
class TestCustomUser(TestCase): |
||||
|
def setUp(self): |
||||
|
self.new_user = CustomUser.objects.create_user('test@example.com') |
||||
|
|
||||
|
def test_get_short_name(self): |
||||
|
self.assertEqual(self.new_user.get_short_name(), 'test@example.com') |
||||
|
|
||||
|
def test_get_full_name(self): |
||||
|
self.assertEqual(self.new_user.get_full_name(), 'test@example.com') |
||||
|
|
||||
|
|
||||
|
class TestMember(TestCase): |
||||
|
def setUp(self): |
||||
|
self.new_member = Member.objects.create( |
||||
|
first_name='First', |
||||
|
last_name='Last', |
||||
|
post_code='H0H0H0' |
||||
|
) |
||||
|
|
||||
|
def test_get_full_name(self): |
||||
|
self.assertEqual(self.new_member.get_full_name(), 'First Last') |
||||
|
self.assertEqual(self.new_member.get_short_name(), 'Last') |
||||
|
|
||||
|
# add email to instance |
||||
|
self.new_member.email = 'member@example.com' |
||||
|
self.assertEqual(self.new_member.get_short_name(), |
||||
|
'member@example.com') |
@ -0,0 +1,83 @@ |
|||||
|
from datetime import timedelta |
||||
|
|
||||
|
from django.test import TestCase |
||||
|
from django.utils import timezone |
||||
|
from model_mommy import mommy |
||||
|
|
||||
|
from core.models import Visit |
||||
|
from registration.models import Member |
||||
|
from registration.utils import signin_member, AlreadySignedInError, member_signed_in, get_signed_in_members |
||||
|
|
||||
|
|
||||
|
class GetSignedInMembersTests(TestCase): |
||||
|
def setUp(self): |
||||
|
self.now = timezone.now() |
||||
|
|
||||
|
self.member1 = mommy.make(model=Member) |
||||
|
self.member2 = mommy.make(model=Member) |
||||
|
self.member3 = mommy.make(model=Member) |
||||
|
|
||||
|
three_hours_ago = self.now - timedelta(hours=3) |
||||
|
five_hours_ago = self.now - timedelta(hours=5) |
||||
|
|
||||
|
self.visit1 = Visit.objects.create(member=self.member1, purpose=Visit.DONATE, created_at=self.now) |
||||
|
self.visit2 = Visit.objects.create(member=self.member2, purpose=Visit.DONATE, created_at=three_hours_ago) |
||||
|
self.visit3 = Visit.objects.create(member=self.member3, purpose=Visit.DONATE, created_at=five_hours_ago) |
||||
|
|
||||
|
def test_get_signed_in_members(self): |
||||
|
""" |
||||
|
Only members signed-in in the window are returned |
||||
|
""" |
||||
|
result1 = get_signed_in_members(end=self.now) # default window=4 |
||||
|
self.assertEqual(len(result1), 2) |
||||
|
|
||||
|
result2 = get_signed_in_members(window=2, end=self.now) |
||||
|
self.assertEqual(len(result2), 1) |
||||
|
|
||||
|
result3 = get_signed_in_members(window=5, end=self.now) |
||||
|
self.assertEqual(len(result3), 3) |
||||
|
|
||||
|
|
||||
|
class SigninMember(TestCase): |
||||
|
def test_not_signed_in(self): |
||||
|
""" |
||||
|
A member who hasn't signed-in in 4 hours is signed-in. |
||||
|
""" |
||||
|
member = mommy.make(Member) |
||||
|
purpose = Visit.FIX |
||||
|
visit = signin_member(member, purpose) |
||||
|
|
||||
|
self.assertIsInstance(visit, Visit) |
||||
|
|
||||
|
def test_signed_in(self): |
||||
|
""" |
||||
|
A member who has signed-in in 4 hours is not signed-in. |
||||
|
""" |
||||
|
member = mommy.make(Member) |
||||
|
purpose = Visit.FIX |
||||
|
signin_member(member, purpose) |
||||
|
|
||||
|
with self.assertRaises(AlreadySignedInError): |
||||
|
signin_member(member, purpose) |
||||
|
|
||||
|
|
||||
|
class CheckMemberSignedIn(TestCase): |
||||
|
def test_member_not_signed_in(self): |
||||
|
""" |
||||
|
Returns false when member is not signed-in |
||||
|
""" |
||||
|
not_signed_member = mommy.make(model=Member) |
||||
|
result = member_signed_in(not_signed_member) |
||||
|
|
||||
|
self.assertFalse(result) |
||||
|
|
||||
|
def test_member_signed_in(self): |
||||
|
""" |
||||
|
Returns true when member is signed-in |
||||
|
""" |
||||
|
|
||||
|
member = mommy.make(model=Member) |
||||
|
Visit.objects.create(member=member, purpose=Visit.DONATE) |
||||
|
result = member_signed_in(member) |
||||
|
|
||||
|
self.assertTrue(result) |
@ -0,0 +1,141 @@ |
|||||
|
import json |
||||
|
import logging |
||||
|
from datetime import datetime, timedelta |
||||
|
|
||||
|
from django.core.urlresolvers import reverse |
||||
|
from django.http import JsonResponse |
||||
|
from django.test import Client, TestCase |
||||
|
|
||||
|
from core.models import Visit |
||||
|
from model_mommy import mommy |
||||
|
from copy import copy |
||||
|
|
||||
|
from ..models import CustomUser, Member |
||||
|
from ..views import MemberFormView |
||||
|
|
||||
|
logger = logging.getLogger('bikeshop') |
||||
|
|
||||
|
|
||||
|
class TestMemberFormView(TestCase): |
||||
|
def setUp(self): |
||||
|
self.user = mommy.make(CustomUser) |
||||
|
self.member = mommy.make(Member) |
||||
|
|
||||
|
def test_get_member_new(self): |
||||
|
url = reverse('member_new') |
||||
|
c = Client() |
||||
|
c.force_login(self.user) |
||||
|
response = c.get(url) |
||||
|
self.assertEqual(response.status_code, 200) |
||||
|
|
||||
|
def test_post_member_new(self): |
||||
|
url = reverse('member_new') |
||||
|
c = Client() |
||||
|
c.force_login(self.user) |
||||
|
member_data = { |
||||
|
'first_name': 'First', |
||||
|
'last_name': 'Last', |
||||
|
'post_code': 'H0H0H0', |
||||
|
} |
||||
|
response = c.post(url, data=member_data) |
||||
|
new_member = Member.objects.get(first_name='First', last_name='Last') |
||||
|
self.assertTrue(new_member) |
||||
|
|
||||
|
def test_get_member_edit(self): |
||||
|
url = reverse('member_edit', kwargs=dict(member_id=self.member.id)) |
||||
|
c = Client() |
||||
|
c.force_login(self.user) |
||||
|
response = c.get(url) |
||||
|
self.assertEqual(response.status_code, 200) |
||||
|
self.assertEqual(response.context['form'].instance, self.member) |
||||
|
|
||||
|
def test_post_member_edit(self): |
||||
|
url = reverse('member_edit', kwargs=dict(member_id=self.member.id)) |
||||
|
c = Client() |
||||
|
c.force_login(self.user) |
||||
|
member_data = { |
||||
|
'first_name': 'First2', |
||||
|
'last_name': 'Last', |
||||
|
'post_code': 'H0H0H0', |
||||
|
} |
||||
|
response = c.post(url, member_data, follow=True) |
||||
|
self.assertEqual(response.status_code, 200) |
||||
|
self.assertEqual(response.context['form'].instance.first_name, 'First2') |
||||
|
|
||||
|
|
||||
|
class TestMemberSearchView(TestCase): |
||||
|
def setUp(self): |
||||
|
self.user = mommy.make(CustomUser) |
||||
|
self.members = mommy.make(Member, _quantity=10) |
||||
|
|
||||
|
def test_search_first_name(self): |
||||
|
self.query = self.members[0].first_name[0:-10] |
||||
|
url = reverse('member_search', kwargs=dict(query=self.query)) |
||||
|
c = Client() |
||||
|
c.force_login(self.user) |
||||
|
response = c.get(url) |
||||
|
|
||||
|
self.assertEqual(response.status_code, 200) |
||||
|
|
||||
|
data = json.loads(response.content.decode(encoding='utf-8')) |
||||
|
results = data['results'] |
||||
|
|
||||
|
# Check if our made up first name is in the name returned. |
||||
|
self.assertTrue([result['name'] for result in results |
||||
|
if self.query in result['name']]) |
||||
|
|
||||
|
|
||||
|
class TestMemberSignIn(TestCase): |
||||
|
def setUp(self): |
||||
|
self.user = mommy.make(CustomUser) |
||||
|
self.members = mommy.make(Member, _quantity=4) |
||||
|
|
||||
|
def test_post(self): |
||||
|
""" Test to make sure a new visit is created when a member is signed in. |
||||
|
""" |
||||
|
url = reverse('member_signin') |
||||
|
c = Client() |
||||
|
c.force_login(self.user) |
||||
|
|
||||
|
response = c.post(url, |
||||
|
data={ |
||||
|
'id': self.members[0].id, |
||||
|
'purpose': Visit.visit_choices[0] |
||||
|
}) |
||||
|
visit = Visit.objects.filter(member=self.members[0]).first() |
||||
|
|
||||
|
self.assertEqual(response.status_code, 201) |
||||
|
self.assertIsInstance(response, JsonResponse) |
||||
|
self.assertTrue(visit) |
||||
|
|
||||
|
def test_post_no_member(self): |
||||
|
""" A non-existent member should produce a 404. |
||||
|
""" |
||||
|
url = reverse('member_signin') |
||||
|
c = Client() |
||||
|
c.force_login(self.user) |
||||
|
|
||||
|
response = c.post(url, data={'id': 343}) |
||||
|
|
||||
|
self.assertTrue(response.status_code, 404) |
||||
|
|
||||
|
def test_get(self): |
||||
|
""" Test signed in members. Should only return three since one visit |
||||
|
passed the four hour threshold. |
||||
|
""" |
||||
|
for member in self.members: |
||||
|
if member is self.members[0]: |
||||
|
Visit.objects.create(member=member, purpose=Visit.visit_choices[0], |
||||
|
created_at=datetime.now() - timedelta(hours=5)) |
||||
|
else: |
||||
|
Visit.objects.create(member=member, purpose=Visit.visit_choices[0]) |
||||
|
|
||||
|
url = reverse('member_signin') |
||||
|
c = Client() |
||||
|
c.force_login(self.user) |
||||
|
|
||||
|
response = c.get(url) |
||||
|
data_string = response.content.decode('utf-8') |
||||
|
data = json.loads(data_string) |
||||
|
|
||||
|
self.assertTrue(len(data), 3) |
@ -0,0 +1,10 @@ |
|||||
|
from django.conf.urls import url |
||||
|
|
||||
|
from .views import MemberFormView, MemberSearchView, MemberSignIn, Members |
||||
|
urlpatterns = [ |
||||
|
url(r'^new/$', MemberFormView.as_view(), name='member_new'), |
||||
|
url(r'^search/(?P<query>[\w@\.\+]+)/$', MemberSearchView.as_view(), name='member_search'), |
||||
|
url(r'^edit/(?P<member_id>[0-9]+)/$', MemberFormView.as_view(), name='member_edit'), |
||||
|
url(r'^signin/$', MemberSignIn.as_view(), name='member_signin'), |
||||
|
url(r'^$', Members.as_view(), name='members'), |
||||
|
] |
@ -0,0 +1,37 @@ |
|||||
|
from datetime import datetime, timedelta |
||||
|
from typing import Optional |
||||
|
|
||||
|
from django.db.models import QuerySet |
||||
|
from django.utils import timezone |
||||
|
|
||||
|
from core.models import Visit |
||||
|
from registration.models import Member |
||||
|
|
||||
|
|
||||
|
class AlreadySignedInError(ValueError): |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
def signin_member(member: Member, purpose: str) -> Visit: |
||||
|
""" |
||||
|
Signs in a member, creating a new `Visit` |
||||
|
:param member: the member to be signed in |
||||
|
:param purpose: The reason for visit. E.g. Fix a bike or volunteer |
||||
|
:return: a new `Visit` |
||||
|
:raise: `AlreadySignedInError` or `ValidationError` |
||||
|
""" |
||||
|
if not member_signed_in(member): |
||||
|
return Visit.objects.create(member=member, purpose=purpose) |
||||
|
|
||||
|
raise AlreadySignedInError |
||||
|
|
||||
|
|
||||
|
def member_signed_in(member: Member, window: int = 4) -> bool: |
||||
|
return get_signed_in_members(window=window).filter(id__in=[member.id]).exists() |
||||
|
|
||||
|
|
||||
|
def get_signed_in_members(window: int = 4, end: Optional[datetime] = None) -> QuerySet: |
||||
|
new_end = end if end else timezone.now() |
||||
|
start = new_end - timedelta(hours=window) |
||||
|
visits = Visit.objects.filter(created_at__lte=new_end, created_at__gte=start) |
||||
|
return visits |
@ -1,3 +1,111 @@ |
|||||
from django.shortcuts import render |
import json |
||||
|
|
||||
# Create your views here. |
from django.contrib.auth.decorators import login_required |
||||
|
from django.core.urlresolvers import reverse |
||||
|
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse |
||||
|
from django.shortcuts import get_object_or_404 |
||||
|
from django.template.response import TemplateResponse |
||||
|
from django.utils.decorators import method_decorator |
||||
|
from django.views.decorators.csrf import csrf_exempt |
||||
|
from django.views.generic import TemplateView, View |
||||
|
from haystack.query import SearchQuerySet |
||||
|
from rest_framework import serializers |
||||
|
from rest_framework.renderers import JSONRenderer |
||||
|
from rest_framework.serializers import ModelSerializer |
||||
|
|
||||
|
from core.models import Visit |
||||
|
from registration.utils import signin_member, get_signed_in_members |
||||
|
from .forms import MemberForm |
||||
|
from .models import Member |
||||
|
|
||||
|
|
||||
|
@method_decorator(login_required, name='dispatch') |
||||
|
class MemberFormView(View): |
||||
|
def get(self, request, member_id=None): |
||||
|
try: |
||||
|
member = Member.objects.get(id=member_id) |
||||
|
form = MemberForm(instance=member) |
||||
|
except Member.DoesNotExist: |
||||
|
form = MemberForm() |
||||
|
member = None |
||||
|
|
||||
|
context = dict(form=form) |
||||
|
if member: |
||||
|
context['member'] = member |
||||
|
return TemplateResponse(request, 'edit_member_form.html', context=context) |
||||
|
|
||||
|
return TemplateResponse(request, 'member_form.html', context=context) |
||||
|
|
||||
|
def post(self, request, member_id=None): |
||||
|
try: |
||||
|
member = Member.objects.get(id=member_id) |
||||
|
form = MemberForm(request.POST, instance=member) |
||||
|
except Member.DoesNotExist: |
||||
|
member = None |
||||
|
form = MemberForm(request.POST) |
||||
|
|
||||
|
if form.is_valid(): |
||||
|
member_instance = form.save() |
||||
|
return HttpResponseRedirect(reverse('member_edit', kwargs=dict(member_id=member_instance.id))) |
||||
|
|
||||
|
context = {'form': form} |
||||
|
if member: |
||||
|
context['member'] = member |
||||
|
return TemplateResponse(request, 'member_form.html', context=context) |
||||
|
|
||||
|
|
||||
|
class MemberSearchView(View): |
||||
|
def get(self, request, query): |
||||
|
sqs = SearchQuerySet().models(Member).autocomplete(text=query)[:5] |
||||
|
results = [dict(name=result.object.get_full_name(), email=result.object.email, id=result.object.id) for result in sqs] |
||||
|
|
||||
|
data = json.dumps(dict(results=results)) |
||||
|
|
||||
|
return HttpResponse(data, content_type='application/json') |
||||
|
|
||||
|
|
||||
|
class MemberSerializer(ModelSerializer): |
||||
|
first_name = serializers.CharField(allow_blank=True, required=False) |
||||
|
last_name = serializers.CharField(allow_blank=True, required=False) |
||||
|
|
||||
|
class Meta: |
||||
|
model = Member |
||||
|
fields = ('first_name', 'last_name', 'email', 'id') |
||||
|
|
||||
|
|
||||
|
class VisitSerializer(ModelSerializer): |
||||
|
member = MemberSerializer() |
||||
|
|
||||
|
class Meta: |
||||
|
model = Visit |
||||
|
fields = ('created_at', 'purpose', 'member') |
||||
|
depth = 1 |
||||
|
|
||||
|
|
||||
|
class MemberSignIn(View): |
||||
|
@method_decorator(csrf_exempt) |
||||
|
def dispatch(self, request, *args, **kwargs): |
||||
|
return super(MemberSignIn, self).dispatch(request, *args, **kwargs) |
||||
|
|
||||
|
def post(self, request): |
||||
|
member = get_object_or_404(Member, id=request.POST.get('id')) |
||||
|
visit = signin_member(member, request.POST.get('purpose')) |
||||
|
data = json.dumps(dict(results=dict(id=member.id, created_at=visit.created_at.isoformat()))) |
||||
|
|
||||
|
return JsonResponse(data=data, safe=False, status=201) |
||||
|
|
||||
|
def get(self, request): |
||||
|
visits = get_signed_in_members().prefetch_related() |
||||
|
serializer = VisitSerializer(visits, many=True) |
||||
|
results_json = JSONRenderer().render(serializer.data) |
||||
|
|
||||
|
return JsonResponse(data=results_json.decode(), safe=False, status=200) |
||||
|
|
||||
|
|
||||
|
@method_decorator(login_required, name='dispatch') |
||||
|
class Members(TemplateView): |
||||
|
template_name = 'members.html' |
||||
|
|
||||
|
def get(self, request): |
||||
|
members = Member.objects.all() |
||||
|
return self.render_to_response(dict(members=members)) |
||||
|
@ -0,0 +1,16 @@ |
|||||
|
var webpack = require('webpack') |
||||
|
var WebpackDevServer = require('webpack-dev-server') |
||||
|
var config = require('./webpack.dev.config') |
||||
|
|
||||
|
new WebpackDevServer(webpack(config), { |
||||
|
publicPath: config.output.publicPath, |
||||
|
hot: true, |
||||
|
inline: true, |
||||
|
historyApiFallback: true |
||||
|
}).listen(3000, '0.0.0.0', function (err, result) { |
||||
|
if (err) { |
||||
|
console.log(err) |
||||
|
} |
||||
|
|
||||
|
console.log('Listening at 0.0.0.0:3000') |
||||
|
}) |
@ -0,0 +1,35 @@ |
|||||
|
var path = require("path") |
||||
|
var webpack = require('webpack') |
||||
|
var BundleTracker = require('webpack-bundle-tracker') |
||||
|
|
||||
|
const autoprefixer = require('autoprefixer'); |
||||
|
|
||||
|
module.exports = { |
||||
|
context: __dirname, |
||||
|
|
||||
|
entry: './assets/js/index', |
||||
|
|
||||
|
output: { |
||||
|
path: path.resolve('./assets/bundles/'), |
||||
|
filename: "[name]-[hash].js" |
||||
|
}, |
||||
|
|
||||
|
plugins: [ |
||||
|
|
||||
|
], // add all common plugins here
|
||||
|
|
||||
|
module: { |
||||
|
loaders: [ |
||||
|
|
||||
|
] |
||||
|
}, |
||||
|
|
||||
|
resolve: { |
||||
|
modulesDirectories: [ |
||||
|
'node_modules', |
||||
|
'bower_components' |
||||
|
], |
||||
|
extensions: ['', '.js', '.jsx', '.scss'] |
||||
|
}, |
||||
|
postcss: [autoprefixer] |
||||
|
} |
@ -0,0 +1,48 @@ |
|||||
|
const path = require("path"); |
||||
|
const webpack = require('webpack'); |
||||
|
const BundleTracker = require('webpack-bundle-tracker'); |
||||
|
const ExtractTextPlugin = require('extract-text-webpack-plugin'); |
||||
|
const autoprefixer = require('autoprefixer'); |
||||
|
|
||||
|
module.exports = { |
||||
|
context: __dirname, |
||||
|
devtool: 'inline-source-map', |
||||
|
entry: './assets/js/index', // entry point of our app. assets/js/index.js should require other js modules and dependencies it needs
|
||||
|
|
||||
|
output: { |
||||
|
path: path.resolve('./assets/bundles/'), |
||||
|
filename: "[name]-[hash].js" |
||||
|
}, |
||||
|
|
||||
|
plugins: [ |
||||
|
new BundleTracker({filename: './webpack-stats.json'}), |
||||
|
new ExtractTextPlugin('react-toolbox.css', { allChunks: true }), |
||||
|
new webpack.NoErrorsPlugin() |
||||
|
], |
||||
|
|
||||
|
module: { |
||||
|
loaders: [ |
||||
|
{ |
||||
|
test: /\.jsx?$/, |
||||
|
exclude: /node_modules/, |
||||
|
loader: 'babel-loader', |
||||
|
query: { |
||||
|
presets: ['es2015', 'stage-0', 'react'] |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
test: /(\.scss|\.css)$/, |
||||
|
loader: ExtractTextPlugin.extract('style', 'css?sourceMap&modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss!sass?sourceMap!toolbox') |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
resolve: { |
||||
|
modulesDirectories: [ |
||||
|
'node_modules', |
||||
|
'bower_components', |
||||
|
path.resolve(__dirname, './node_modules') |
||||
|
], |
||||
|
extensions: ['', '.js', '.jsx', '.scss'] |
||||
|
}, |
||||
|
postcss: [autoprefixer] |
||||
|
} |
@ -0,0 +1,42 @@ |
|||||
|
var path = require("path") |
||||
|
var webpack = require('webpack') |
||||
|
var BundleTracker = require('webpack-bundle-tracker') |
||||
|
const ExtractTextPlugin = require('extract-text-webpack-plugin'); |
||||
|
|
||||
|
var config = require('./webpack.base.config.js') |
||||
|
|
||||
|
// Use webpack dev server
|
||||
|
config.entry = [ |
||||
|
'webpack-dev-server/client?http://webpack.docker:3000', |
||||
|
'webpack/hot/only-dev-server', |
||||
|
'./assets/js/index' |
||||
|
] |
||||
|
|
||||
|
// override django's STATIC_URL for webpack bundles
|
||||
|
config.output.publicPath = 'http://webpack.docker:3000/assets/bundles/' |
||||
|
|
||||
|
config.devtool = 'eval-source-map'; |
||||
|
|
||||
|
// Add HotModuleReplacementPlugin and BundleTracker plugins
|
||||
|
config.plugins = config.plugins.concat([ |
||||
|
new webpack.HotModuleReplacementPlugin(), |
||||
|
new webpack.NoErrorsPlugin(), |
||||
|
new BundleTracker({filename: './webpack-stats.json'}), |
||||
|
new ExtractTextPlugin('react-toolbox.css', {allChunks: true}), |
||||
|
]) |
||||
|
|
||||
|
// Add a loader for JSX files with react-hot enabled
|
||||
|
config.module.loaders.push( |
||||
|
{ |
||||
|
test: /\.jsx?$/, |
||||
|
exclude: /node_modules/, |
||||
|
loaders: ['react-hot','babel-loader'] |
||||
|
|
||||
|
}, |
||||
|
{ |
||||
|
test: /(\.scss|\.css)$/, |
||||
|
loader: ExtractTextPlugin.extract('style', 'css?sourceMap&modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss!sass?sourceMap!toolbox') |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
module.exports = config |
@ -0,0 +1,39 @@ |
|||||
|
var webpack = require('webpack') |
||||
|
var BundleTracker = require('webpack-bundle-tracker') |
||||
|
const ExtractTextPlugin = require('extract-text-webpack-plugin') |
||||
|
|
||||
|
var config = require('./webpack.base.config.js') |
||||
|
|
||||
|
config.output.path = require('path').resolve('./assets/dist') |
||||
|
|
||||
|
config.plugins = config.plugins.concat([ |
||||
|
new BundleTracker({filename: './webpack-stats-prod.json'}), |
||||
|
new ExtractTextPlugin('react-toolbox.css', {allChunks: true}), |
||||
|
|
||||
|
// removes a lot of debugging code in React
|
||||
|
new webpack.DefinePlugin({ |
||||
|
'process.env': { |
||||
|
'NODE_ENV': JSON.stringify('production') |
||||
|
}}), |
||||
|
|
||||
|
// keeps hashes consistent between compilations
|
||||
|
new webpack.optimize.OccurenceOrderPlugin(), |
||||
|
|
||||
|
// minifies your code
|
||||
|
new webpack.optimize.UglifyJsPlugin({ |
||||
|
compressor: { |
||||
|
warnings: false |
||||
|
} |
||||
|
}) |
||||
|
]) |
||||
|
|
||||
|
// Add a loader for JSX files
|
||||
|
config.module.loaders.push( |
||||
|
{ test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel' }, |
||||
|
{ |
||||
|
test: /(\.scss|\.css)$/, |
||||
|
loader: ExtractTextPlugin.extract('style', 'css?sourceMap&modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss!sass?sourceMap!toolbox') |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
module.exports = config |
@ -0,0 +1,25 @@ |
|||||
|
version: "2" |
||||
|
services: |
||||
|
workstand: |
||||
|
build: |
||||
|
context: . |
||||
|
command: python manage.py runserver 0.0.0.0:8000 |
||||
|
ports: |
||||
|
- "8000:8000" |
||||
|
- "62260:62260" |
||||
|
volumes: |
||||
|
- ./bikeshop_project:/code |
||||
|
redis: |
||||
|
restart: always |
||||
|
db: |
||||
|
restart: always |
||||
|
webpack: |
||||
|
build: |
||||
|
context: . |
||||
|
dockerfile: Dockerfile-webpack |
||||
|
command: "npm run watch" |
||||
|
ports: |
||||
|
- "3000:3000" |
||||
|
restart: always |
||||
|
volumes_from: |
||||
|
- workstand |
@ -0,0 +1,35 @@ |
|||||
|
version: "2" |
||||
|
services: |
||||
|
nginx: |
||||
|
build: |
||||
|
context: docker/nginx |
||||
|
image: bcbc/nginx:production |
||||
|
ports: |
||||
|
- "80:80" |
||||
|
links: |
||||
|
- workstand |
||||
|
volumes_from: |
||||
|
- workstand:ro |
||||
|
workstand: |
||||
|
build: |
||||
|
context: . |
||||
|
dockerfile: Dockerfile-prod |
||||
|
image: bcbc/workstand:production |
||||
|
env_file: |
||||
|
- workstand.env |
||||
|
command: gunicorn --log-file=- -b 0.0.0.0:8000 bikeshop.wsgi:application |
||||
|
environment: |
||||
|
- DJANGO_SETTINGS_MODULE=bikeshop.settings.production |
||||
|
volumes: |
||||
|
- static:/code/static |
||||
|
depends_on: |
||||
|
- redis |
||||
|
- db |
||||
|
redis: |
||||
|
restart: always |
||||
|
db: |
||||
|
restart: always |
||||
|
|
||||
|
volumes: |
||||
|
static: |
||||
|
external: false |
@ -0,0 +1,17 @@ |
|||||
|
version: "2" |
||||
|
services: |
||||
|
workstand: |
||||
|
image: bcbc/workstand |
||||
|
depends_on: |
||||
|
- redis |
||||
|
- db |
||||
|
redis: |
||||
|
image: redis:latest |
||||
|
db: |
||||
|
image: postgres:latest |
||||
|
volumes: |
||||
|
- pgdata:/var/lib/postgresql/data/ |
||||
|
|
||||
|
volumes: |
||||
|
pgdata: |
||||
|
external: false |
@ -0,0 +1,2 @@ |
|||||
|
FROM nginx:alpine |
||||
|
COPY conf/* /etc/nginx/conf.d/ |
@ -0,0 +1,46 @@ |
|||||
|
# server { |
||||
|
# listen 80; |
||||
|
# server_name www.shop.bcbc.bike; |
||||
|
# # $scheme will get the http protocol |
||||
|
# # and 301 is best practice for tablet, phone, desktop and seo |
||||
|
# return 301 https://shop.bcbc.bike$request_uri; |
||||
|
#} |
||||
|
#server { |
||||
|
# listen 80; |
||||
|
# server_name shop.bcbc.bike; |
||||
|
# # $scheme will get the http protocol |
||||
|
# # and 301 is best practice for tablet, phone, desktop and seo |
||||
|
# return 301 https://shop.bcbc.bike$request_uri; |
||||
|
#} |
||||
|
server { |
||||
|
# listen 443 ssl; |
||||
|
listen 80; |
||||
|
server_name shop.bcbc.bike; |
||||
|
|
||||
|
# ssl_certificate /etc/letsencrypt/live/{{ app_domain_name }}/fullchain.pem; |
||||
|
# ssl_certificate_key /etc/letsencrypt/live/{{ app_domain_name }}/privkey.pem; |
||||
|
|
||||
|
# ssl_protocols TLSv1 TLSv1.1 TLSv1.2; |
||||
|
# ssl_prefer_server_ciphers on; |
||||
|
# ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; |
||||
|
|
||||
|
location = /favicon.ico { access_log off; log_not_found off; } |
||||
|
keepalive_timeout 5; |
||||
|
root /code; |
||||
|
|
||||
|
location / { |
||||
|
# checks for static file, if not found proxy to app |
||||
|
try_files $uri @proxy_to_app; |
||||
|
} |
||||
|
|
||||
|
location @proxy_to_app { |
||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
||||
|
# enable this if and only if you use HTTPS |
||||
|
# proxy_set_header X-Forwarded-Proto https; |
||||
|
proxy_set_header Host $http_host; |
||||
|
# we don't want nginx trying to do something clever with |
||||
|
# redirects, we set the Host: header above already. |
||||
|
proxy_redirect off; |
||||
|
proxy_pass http://workstand:8000; |
||||
|
} |
||||
|
} |
@ -1,14 +0,0 @@ |
|||||
sudo: yes |
|
||||
# Application settings |
|
||||
app_name: bikeshop |
|
||||
app_user: "{{ app_name }}" |
|
||||
webapps_dir: /srv |
|
||||
settings_module: bikeshop.settings.{{ group_names[0] }} |
|
||||
app_dir: "{{ webapps_dir }}/{{ app_name }}" |
|
||||
venv: /opt/venv/{{ app_name }}-{{ group_names[0] }} |
|
||||
|
|
||||
db_name: "{{ app_name }}_development" |
|
||||
db_user: "{{ app_name }}" |
|
||||
|
|
||||
db_user_password: "{{ secret_db_user_password }}" |
|
||||
secret_db_user_password: password |
|
@ -1,11 +0,0 @@ |
|||||
postgresql_postgis: yes |
|
||||
app_dir: "{{ webapps_dir }}/{{ app_name }}" |
|
||||
|
|
||||
db_name: "{{ app_name }}_production" |
|
||||
db_user: "{{ app_name }}" |
|
||||
|
|
||||
db_user_password: "{{ secret_db_user_password }}" |
|
||||
django_key: "{{ secret_django_key }}" |
|
||||
|
|
||||
app_domain_name: shop.bcbc.bike |
|
||||
app_port: 9999 |
|
@ -1,10 +0,0 @@ |
|||||
$ANSIBLE_VAULT;1.1;AES256 |
|
||||
32363235643965636135663037643239343762353631383934636533373537666561373063353261 |
|
||||
6363393337636562653565303362366437313038633765330a396239336337356130353932656563 |
|
||||
64623464623637323736376665343539353634326636333134363961653431303065373337616238 |
|
||||
6633346461346266620a383536383331663262373736353230643736356134323136363230316136 |
|
||||
61353131326165346564626237306630323962366239653837323463653639616363303562396662 |
|
||||
32626165613563313130376438656561656664663231303839303935376335396533646534326661 |
|
||||
38306561303432326236346365353034663736376335616434633332313735383236396431636532 |
|
||||
36643163333762613065373861333265326363613365623330323135363739663931666466373961 |
|
||||
6362 |
|
@ -1,10 +0,0 @@ |
|||||
postgresql_postgis: yes |
|
||||
|
|
||||
db_name: "{{ app_name }}_staging" |
|
||||
db_user: "{{ app_name }}" |
|
||||
|
|
||||
db_user_password: "{{ secret_db_user_password }}" |
|
||||
django_key: "{{ secret_django_key }}" |
|
||||
|
|
||||
app_domain_name: 9mile.local |
|
||||
app_port: 9998 |
|
@ -1,2 +0,0 @@ |
|||||
secret_db_user_password: password |
|
||||
secret_django_key: somekey |
|
@ -1,5 +0,0 @@ |
|||||
[production] |
|
||||
1.1.1.1 ansible_ssh_private_key_file=~/.ssh/id_rsa |
|
||||
|
|
||||
[servers:children] |
|
||||
production |
|
@ -1,3 +0,0 @@ |
|||||
- include: prepare.yml |
|
||||
- include: virtualenv.yml |
|
||||
|
|
@ -1,5 +0,0 @@ |
|||||
--- |
|
||||
- name: create webapps log directory |
|
||||
action: file dest={{ webapps_dir }}/{{ app_name }}/log state=directory |
|
||||
- name: create webapps directory |
|
||||
action: file dest={{ webapps_dir }} state=directory |
|
@ -1,15 +0,0 @@ |
|||||
--- |
|
||||
- name: install virtualenv |
|
||||
action: pip executable=pip3 name=virtualenv state=present |
|
||||
|
|
||||
- name: copy requirements |
|
||||
action: copy src=../requirements dest=/tmp |
|
||||
|
|
||||
- name: create virtual environment |
|
||||
action: file dest={{ venv }} state=directory |
|
||||
|
|
||||
- name: install requirements |
|
||||
action: pip executable=pip3 virtualenv={{ venv }} virtualenv_python=python3 requirements=/tmp/requirements/{{ group_names[0] }}.txt extra_args='--upgrade --force-reinstall' |
|
||||
register: requirements |
|
||||
tags: |
|
||||
- reqs |
|
@ -1,2 +0,0 @@ |
|||||
ninemilelegacy_project/core |
|
||||
ninemilelegacy_project/ninemilelegacy |
|
@ -1,19 +0,0 @@ |
|||||
--- |
|
||||
- name: install packages needed to for building modules |
|
||||
action: apt update_cache=yes pkg={{ item }} state=installed |
|
||||
with_items: |
|
||||
- build-essential |
|
||||
- autoconf |
|
||||
- git-core |
|
||||
|
|
||||
- name: install nginx webserver |
|
||||
action: apt update_cache=no pkg=nginx state=installed |
|
||||
|
|
||||
- name: install supervisord |
|
||||
action: apt update_cache=no pkg=supervisor state=installed |
|
||||
|
|
||||
- name: install redis |
|
||||
action: apt name=redis-server update_cache=yes state=latest |
|
||||
|
|
||||
- name: create app user |
|
||||
action: user createhome=no name={{ app_name }} state=present |
|
@ -1,18 +0,0 @@ |
|||||
--- |
|
||||
- name: create a postgresql database |
|
||||
sudo: yes |
|
||||
sudo_user: postgres |
|
||||
action: postgresql_db name={{ db_name }} template=template0 state=present |
|
||||
|
|
||||
- name: add a user to postgresql database |
|
||||
sudo: yes |
|
||||
sudo_user: postgres |
|
||||
action: postgresql_user db={{ db_name }} name={{ db_user }} password={{ db_user_password }} priv=ALL |
|
||||
|
|
||||
- name: ensure database backkup directory is present |
|
||||
sudo: yes |
|
||||
action: file path=/var/backups/{{ db_name }} state=directory owner=postgres |
|
||||
|
|
||||
- name: add a cron job to backup database every 1hr |
|
||||
sudo: yes |
|
||||
action: cron name='database backup' special_time=hourly user=postgres job='/usr/bin/pg_dump -Ft {{ db_name }} > /var/backups/{{ db_name }}/$(date +"\%Y\%m\%d\%H\%M\%S").tar' |
|
@ -1,36 +0,0 @@ |
|||||
--- |
|
||||
- name: upload code |
|
||||
action: synchronize src=../bikeshop_project/ dest={{ app_dir }} |
|
||||
notify: restart supervisor |
|
||||
tags: |
|
||||
- deploy |
|
||||
|
|
||||
- name: user owns directory |
|
||||
action: file path={{ app_dir }} owner={{ app_name }} state=directory recurse=yes |
|
||||
tags: |
|
||||
- deploy |
|
||||
|
|
||||
- name: migrate database |
|
||||
action: django_manage command=migrate app_path={{ app_dir }} virtualenv={{ venv }} settings={{ settings_module }} |
|
||||
environment: |
|
||||
DJANGO_DB_PASSWORD: "{{ db_user_password }}" |
|
||||
DJANGO_SECRET_KEY: "{{ django_key }}" |
|
||||
tags: |
|
||||
- deploy |
|
||||
|
|
||||
|
|
||||
- name: collect static |
|
||||
action: django_manage command=collectstatic app_path={{ app_dir }} virtualenv={{ venv }} settings={{ settings_module }} |
|
||||
environment: |
|
||||
DJANGO_DB_PASSWORD: "{{ db_user_password }}" |
|
||||
DJANGO_SECRET_KEY: "{{ django_key }}" |
|
||||
tags: |
|
||||
- deploy |
|
||||
|
|
||||
- name: load initial data in to database |
|
||||
action: django_manage command=loaddata fixtures=authentication/fixtures/initial_data.json app_path={{ app_dir }} virtualenv={{ venv }} settings={{ settings_module }} |
|
||||
environment: |
|
||||
DJANGO_DB_PASSWORD: "{{ db_user_password }}" |
|
||||
DJANGO_SECRET_KEY: "{{ django_key }}" |
|
||||
tags: |
|
||||
- deploy |
|
@ -1,14 +0,0 @@ |
|||||
- name: migrate app |
|
||||
action: django_manage command=migrate app_path={{ app_dir }} settings={{ settings_module }} virtualenv={{ venv }} |
|
||||
ignore_errors: yes |
|
||||
environment: |
|
||||
SECRET_KEY: "{{ django_key }}" |
|
||||
DB_PASSWORD: "{{ db_user_password }}" |
|
||||
|
|
||||
|
|
||||
- name: collect static |
|
||||
action: django_manage command="collectstatic --noinput" app_path={{ app_dir }} settings={{ settings_module }} virtualenv={{ venv }} |
|
||||
ignore_errors: yes |
|
||||
environment: |
|
||||
SECRET_KEY: "{{ django_key }}" |
|
||||
DB_PASSWORD: "{{ db_user_password }}" |
|
@ -1,3 +0,0 @@ |
|||||
--- |
|
||||
- name: restart supervisor |
|
||||
service: name=supervisor state=restarted |
|
@ -1,2 +0,0 @@ |
|||||
- name: restart nginx |
|
||||
service: name=nginx state=restarted |
|
@ -1,12 +0,0 @@ |
|||||
--- |
|
||||
- name: Configure nginx to run proxypass {{ app_name }} |
|
||||
action: template src={{ group_names[0] }}/nginx-site.conf dest=/etc/nginx/sites-available/{{ app_domain_name }} |
|
||||
notify: restart nginx |
|
||||
tags: |
|
||||
- nginx |
|
||||
|
|
||||
- name: Enable {{ app_name }} nginx config |
|
||||
action: file src=/etc/nginx/sites-available/{{ app_domain_name }} dest=/etc/nginx/sites-enabled/{{ app_domain_name }} state=link |
|
||||
notify: restart nginx |
|
||||
tags: |
|
||||
- nginx |
|
@ -1,61 +0,0 @@ |
|||||
server { |
|
||||
listen 80; |
|
||||
server_name www.{{ app_domain_name }}; |
|
||||
# $scheme will get the http protocol |
|
||||
# and 301 is best practice for tablet, phone, desktop and seo |
|
||||
return 301 https://{{ app_domain_name }}$request_uri; |
|
||||
} |
|
||||
server { |
|
||||
listen 80; |
|
||||
server_name {{ app_domain_name }}; |
|
||||
# $scheme will get the http protocol |
|
||||
# and 301 is best practice for tablet, phone, desktop and seo |
|
||||
return 301 https://{{ app_domain_name }}$request_uri; |
|
||||
} |
|
||||
server { |
|
||||
listen 443 ssl; |
|
||||
server_name {{ app_domain_name }}; |
|
||||
|
|
||||
ssl_certificate /etc/letsencrypt/live/{{ app_domain_name }}/fullchain.pem; |
|
||||
ssl_certificate_key /etc/letsencrypt/live/{{ app_domain_name }}/privkey.pem; |
|
||||
|
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; |
|
||||
ssl_prefer_server_ciphers on; |
|
||||
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; |
|
||||
|
|
||||
location = /favicon.ico { access_log off; log_not_found off; } |
|
||||
location /static/ { |
|
||||
root {{ app_dir }}/{{ app_name }}; |
|
||||
} |
|
||||
|
|
||||
location /media/ { |
|
||||
root {{ app_dir }}/{{ app_name }}; |
|
||||
} |
|
||||
|
|
||||
location / { |
|
||||
proxy_pass_header Server; |
|
||||
proxy_set_header Host $http_host; |
|
||||
proxy_redirect off; |
|
||||
proxy_set_header X-Real-IP $remote_addr; |
|
||||
proxy_set_header X-Scheme $scheme; |
|
||||
proxy_connect_timeout 10; |
|
||||
proxy_read_timeout 10; |
|
||||
proxy_pass http://localhost:{{ app_port }}/; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
server { |
|
||||
listen 4051; |
|
||||
server_name events.{{ app_domain_name }}; |
|
||||
|
|
||||
location /incoming/spikeEvent.php { |
|
||||
proxy_pass_header Server; |
|
||||
proxy_set_header Host $http_host; |
|
||||
proxy_redirect off; |
|
||||
proxy_set_header X-Real-IP $remote_addr; |
|
||||
proxy_set_header X-Scheme $scheme; |
|
||||
proxy_connect_timeout 10; |
|
||||
proxy_read_timeout 10; |
|
||||
proxy_pass http://localhost:{{ app_port }}/incoming/; |
|
||||
} |
|
||||
} |
|
@ -1,31 +0,0 @@ |
|||||
server { |
|
||||
listen 80; |
|
||||
server_name www.{{ app_domain_name }}; |
|
||||
# $scheme will get the http protocol |
|
||||
# and 301 is best practice for tablet, phone, desktop and seo |
|
||||
return 301 $scheme://{{ app_domain_name }}$request_uri; |
|
||||
} |
|
||||
server { |
|
||||
listen 80; |
|
||||
server_name {{ app_domain_name }}; |
|
||||
|
|
||||
location = /favicon.ico { access_log off; log_not_found off; } |
|
||||
location /static/ { |
|
||||
root {{ app_dir }}; |
|
||||
} |
|
||||
|
|
||||
location /media/ { |
|
||||
root {{ app_dir }}; |
|
||||
} |
|
||||
|
|
||||
location / { |
|
||||
proxy_pass_header Server; |
|
||||
proxy_set_header Host $http_host; |
|
||||
proxy_redirect off; |
|
||||
proxy_set_header X-Real-IP $remote_addr; |
|
||||
proxy_set_header X-Scheme $scheme; |
|
||||
proxy_connect_timeout 10; |
|
||||
proxy_read_timeout 10; |
|
||||
proxy_pass http://localhost:{{ app_port }}/; |
|
||||
} |
|
||||
} |
|
@ -1,14 +0,0 @@ |
|||||
--- |
|
||||
- name: install common packages needed for Python application development with PostgreSQL |
|
||||
action: apt update_cache=yes pkg={{ item }} state=installed |
|
||||
with_items: |
|
||||
- python3-dev |
|
||||
- python3-psycopg2 |
|
||||
- python3-setuptools |
|
||||
- python3-pip |
|
||||
- libpq-dev |
|
||||
- libxml2-dev |
|
||||
- libxslt1-dev |
|
||||
|
|
||||
#- name: install superlance (supervisor plugins) |
|
||||
# action: pip3 name=superlance |
|
@ -1,3 +0,0 @@ |
|||||
--- |
|
||||
- name: start supervisor |
|
||||
service: name=supervisor state=started |
|
@ -1,12 +0,0 @@ |
|||||
--- |
|
||||
- name: Configure supvervisor to run {{ app_name }} |
|
||||
action: template src={{ group_names[0] }}/supervisor-program.conf dest=/etc/supervisor/conf.d/{{ app_name }}.conf |
|
||||
tags: |
|
||||
- supervisor |
|
||||
- deploy |
|
||||
|
|
||||
- name: Load and re-read group configs |
|
||||
action: supervisorctl name='{{ app_name }}:' state=present config=/etc/supervisor/supervisord.conf |
|
||||
tags: |
|
||||
- supvervisor |
|
||||
- deploy |
|
@ -1,4 +0,0 @@ |
|||||
--- |
|
||||
- name: Configure supvervisor to run {{ app_name }} |
|
||||
action: template src=staging/supervisor-program.conf dest=/etc/supervisor/conf.d/{{ app_name }}.conf |
|
||||
notify: restart supervisor |
|
@ -1,45 +0,0 @@ |
|||||
[group:{{ app_name }}] |
|
||||
programs=django,celerybeat,celeryworker |
|
||||
[program:django] |
|
||||
command={{ venv }}/bin/gunicorn -b 127.0.0.1:{{ app_port }} {{ app_name }}.wsgi:application |
|
||||
directory={{ app_dir }} |
|
||||
environment=DJANGO_DB_PASSWORD="{{ db_user_password }}",DJANGO_SECRET_KEY="{{ django_key }}",DJANGO_SETTINGS_MODULE="{{ settings_module }}" |
|
||||
autostart=true |
|
||||
autorestart=true |
|
||||
redirect_stderr=true |
|
||||
user={{ app_name }} |
|
||||
|
|
||||
[program:celerybeat] |
|
||||
command={{ venv }}/bin/celery -A {{ app_name }} beat |
|
||||
environment=DJANGO_DB_PASSWORD="{{ db_user_password }}",DJANGO_SECRET_KEY="{{ django_key }}",DJANGO_SETTINGS_MODULE="{{ settings_module }}" |
|
||||
directory={{ app_dir }} |
|
||||
autostart=true |
|
||||
autorestart=true |
|
||||
redirect_stderr=true |
|
||||
user={{ app_name }} |
|
||||
|
|
||||
[program:celeryworker] |
|
||||
command={{ venv }}/bin/celery -A {{ app_name }} worker |
|
||||
environment=DJANGO_DB_PASSWORD="{{ db_user_password }}",DJANGO_SECRET_KEY="{{ django_key }}",DJANGO_SETTINGS_MODULE="{{ settings_module }}",PYTHON_PATH={{ app_dir }} |
|
||||
directory={{ app_dir }} |
|
||||
autostart=true |
|
||||
autorestart=true |
|
||||
stdout_logfile=/var/log/celeryd/{{ app_name }}.log |
|
||||
stderr_logfile=/var/log/celeryd/{{ app_name }}.log |
|
||||
user={{ app_name }} |
|
||||
numprocs=1 |
|
||||
|
|
||||
startsecs=10 |
|
||||
|
|
||||
; Need to wait for currently executing tasks to finish at shutdown. |
|
||||
; Increase this if you have very long running tasks. |
|
||||
stopwaitsecs = 600 |
|
||||
|
|
||||
; When resorting to send SIGKILL to the program to terminate it |
|
||||
; send SIGKILL to its whole process group instead, |
|
||||
; taking care of its children as well. |
|
||||
killasgroup=true |
|
||||
|
|
||||
; Set Celery priority higher than default (999) |
|
||||
; so, if rabbitmq is supervised, it will start first. |
|
||||
priority=1000 |
|
@ -1,10 +0,0 @@ |
|||||
[program:{{ app_name }}] |
|
||||
command={{ venv }}/bin/gunicorn -b 127.0.0.1:{{ app_port }} {{ app_name }}.wsgi:application |
|
||||
directory={{ app_dir }} |
|
||||
environment=DB_PASSWORD="{{ db_user_password }}",DJANGO_SECRET_KEY="{{ django_key }}",DJANGO_SETTINGS_MODULE="{{settings_module}}" |
|
||||
autorestart=true |
|
||||
redirect_stderr=true |
|
||||
|
|
||||
[eventlistener:memmon] |
|
||||
command=memmon -p {{ app_name }}=50MB -m vagrant@{{ app_domain_name}} |
|
||||
events=TICK_60 |
|
@ -1,4 +0,0 @@ |
|||||
--- |
|
||||
- name: Configure supvervisor to run {{ app_name }} |
|
||||
action: template src={{ group_names[0] }}/supervisor-program.conf dest=/etc/supervisor/conf.d/{{ app_name }}.conf |
|
||||
notify: restart supervisor |
|
@ -1,4 +0,0 @@ |
|||||
--- |
|
||||
- name: Configure supvervisor to run {{ app_name }} |
|
||||
action: template src=staging/supervisor-program.conf dest=/etc/supervisor/conf.d/{{ app_name }}.conf |
|
||||
notify: restart supervisor |
|
@ -1,11 +0,0 @@ |
|||||
[program:{{ app_name }}] |
|
||||
command={{ venv }}/bin/gunicorn -b 127.0.0.1:{{ app_port }} boxoffice.wsgi:application |
|
||||
directory={{ app_dir }} |
|
||||
environment=DB_PASSWORD="{{ db_user_password }}",OTB_SECRET_KEY="{{ django_key }}" |
|
||||
autostart=true |
|
||||
autorestart=true |
|
||||
redirect_stderr=true |
|
||||
|
|
||||
[eventlistener:memmon] |
|
||||
command=memmon -p {{ app_name }}=50MB -m root |
|
||||
events=TICK_60 |
|
@ -1,10 +0,0 @@ |
|||||
[program:{{ app_name }}] |
|
||||
command={{ venv }}/bin/gunicorn -b 127.0.0.1:{{ app_port }} boxoffice.wsgi:application |
|
||||
directory={{ app_dir }} |
|
||||
environment=DB_PASSWORD="{{ db_user_password }}",OTB_SECRET_KEY="{{ django_key }}",DJANGO_SETTINGS_MODULE="boxoffice.settings.staging" |
|
||||
autorestart=true |
|
||||
redirect_stderr=true |
|
||||
|
|
||||
[eventlistener:memmon] |
|
||||
command=memmon -p {{ app_name }}=50MB -m vagrant@ontheboards.local |
|
||||
events=TICK_60 |
|
@ -1 +0,0 @@ |
|||||
/.vagrant/ |
|
@ -1,28 +0,0 @@ |
|||||
--- |
|
||||
|
|
||||
# Configuration directives for Travis CI |
|
||||
# -------------------------------------- |
|
||||
|
|
||||
# See the Travis documentation for help on writing this file. |
|
||||
|
|
||||
# [Travis CI] http://travis-ci.org |
|
||||
# [configuration] http://about.travis-ci.org/docs/user/build-configuration/ |
|
||||
|
|
||||
|
|
||||
language: python # Needed to run ansible |
|
||||
|
|
||||
python: |
|
||||
- "2.7" |
|
||||
|
|
||||
install: |
|
||||
- pip install ansible |
|
||||
|
|
||||
script: |
|
||||
# Syntax check every ansible playbook in root |
|
||||
# Don't check main, though, as vars_prompt still |
|
||||
# trigger during syntax checks, and travis fails |
|
||||
- find . -maxdepth 1 -type f -name '*.yml' -not -name '.*' -not -name 'main.yml' | xargs -t -n1 ansible-playbook --syntax-check -i inventory |
|
||||
|
|
||||
notifications: |
|
||||
email: |
|
||||
- zenoamaro@gmail.com |
|
@ -1,21 +0,0 @@ |
|||||
The MIT License (MIT) |
|
||||
|
|
||||
Copyright (c) 2015, zenoamaro <zenoamaro@gmail.com> |
|
||||
|
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|
||||
of this software and associated documentation files (the "Software"), to deal |
|
||||
in the Software without restriction, including without limitation the rights |
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
||||
copies of the Software, and to permit persons to whom the Software is |
|
||||
furnished to do so, subject to the following conditions: |
|
||||
|
|
||||
The above copyright notice and this permission notice shall be included in |
|
||||
all copies or substantial portions of the Software. |
|
||||
|
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|
||||
THE SOFTWARE. |
|
@ -1,117 +0,0 @@ |
|||||
PostgreSQL role for Ansible |
|
||||
=========================== |
|
||||
|
|
||||
A role for deploying and configuring [PostgreSQL](http://www.postgresql.org/) and extensions on unix hosts using [Ansible](http://www.ansibleworks.com/). |
|
||||
|
|
||||
It can additionally be used as a playbook for quickly provisioning hosts. |
|
||||
|
|
||||
Vagrant machines are provided to produce a boxed install of PostgreSQL or a VM for integration testing. |
|
||||
|
|
||||
|
|
||||
Supports |
|
||||
-------- |
|
||||
Supported PostgreSQL versions: |
|
||||
|
|
||||
- PostgreSQL 9.4 |
|
||||
- PostgreSQL 9.3 |
|
||||
|
|
||||
Supported targets: |
|
||||
- Ubuntu 14.04 LTS "Trusty Tahr" |
|
||||
- Ubuntu 12.04 LTS "Precise Pangolin" |
|
||||
- Debian (untested) |
|
||||
- RedHat (untested) |
|
||||
|
|
||||
Installation methods: |
|
||||
|
|
||||
- Binary packages from the official repositories at [postgresql.org](http://www.postgresql.org/download/) |
|
||||
|
|
||||
Available extensions (under a switch): |
|
||||
|
|
||||
- Development headers - `postgresql_dev_headers` |
|
||||
- Contrib modules - `postgresql_contrib` |
|
||||
- [PostGIS](http://postgis.net/) - `postgresql_postgis` |
|
||||
|
|
||||
|
|
||||
Usage |
|
||||
----- |
|
||||
Clone this repo into your roles directory: |
|
||||
|
|
||||
$ git clone https://github.com/zenoamaro/ansible-postgresql.git roles/postgresql |
|
||||
|
|
||||
And add it to your play's roles: |
|
||||
|
|
||||
- hosts: ... |
|
||||
roles: |
|
||||
- postgresql |
|
||||
- ... |
|
||||
|
|
||||
This roles comes preloaded with almost every available default. You can override each one in your hosts/group vars, in your inventory, or in your play. See the annotated defaults in `defaults/main.yml` for help in configuration. All provided variables start with `postgresql_`. |
|
||||
|
|
||||
You can also use the role as a playbook. You will be asked which hosts to provision, and you can further configure the play by using `--extra-vars`. |
|
||||
|
|
||||
$ ansible-playbook -i inventory --extra-vars='{...}' main.yml |
|
||||
|
|
||||
To provision a standalone PostgreSQL box, start the `boxed` VM, which is a Ubuntu 12.04 box: |
|
||||
|
|
||||
$ vagrant up boxed |
|
||||
|
|
||||
You will find PostgreSQL listening on the VM's `5432` port on address `192.168.33.20` in the private network. You can then connect to it as any user. Please note that this is highly insecure, so if you're going to publish this VM you'll want to provide actual authentication. |
|
||||
|
|
||||
Run the tests by provisioning the appropriate VM: |
|
||||
|
|
||||
$ vagrant up test-ubuntu-trusty |
|
||||
|
|
||||
At the moment, the following test boxes are available: |
|
||||
|
|
||||
- `test-ubuntu-precise` |
|
||||
- `test-ubuntu-trusty` |
|
||||
|
|
||||
|
|
||||
Still to do |
|
||||
----------- |
|
||||
- Add repositories, tasks and test VMs for other distros |
|
||||
- Allow installation from sources if requested |
|
||||
- Add support for different PostgreSQL versions (9.4+) |
|
||||
- Add support for PostgreSQL clusters |
|
||||
- Add support for [PgBouncer](http://wiki.postgresql.org/wiki/PgBouncer) |
|
||||
- Try to make the boxed VM more secure by default |
|
||||
- Add links to the relevant documentation in configuration files |
|
||||
- Provide a library of custom modules |
|
||||
|
|
||||
|
|
||||
Changelog |
|
||||
--------- |
|
||||
### 0.1.2 |
|
||||
- Installing less dependencies, and later in the process. |
|
||||
|
|
||||
### 0.1.1 |
|
||||
- The package list is not being updated in playbooks anymore. |
|
||||
- Added more test machines. |
|
||||
|
|
||||
### 0.1 |
|
||||
Initial version. |
|
||||
|
|
||||
|
|
||||
License |
|
||||
------- |
|
||||
The MIT License (MIT) |
|
||||
|
|
||||
Copyright (c) 2015, zenoamaro <zenoamaro@gmail.com> |
|
||||
|
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|
||||
of this software and associated documentation files (the "Software"), to deal |
|
||||
in the Software without restriction, including without limitation the rights |
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
||||
copies of the Software, and to permit persons to whom the Software is |
|
||||
furnished to do so, subject to the following conditions: |
|
||||
|
|
||||
The above copyright notice and this permission notice shall be included in |
|
||||
all copies or substantial portions of the Software. |
|
||||
|
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|
||||
THE SOFTWARE. |
|
@ -1,62 +0,0 @@ |
|||||
# -*- mode: ruby -*- |
|
||||
# vi: set ft=ruby : |
|
||||
|
|
||||
Vagrant.configure '2' do |config| |
|
||||
|
|
||||
|
|
||||
# Standalone box |
|
||||
# -------------- |
|
||||
|
|
||||
# Provision this machine to obtain a standalone box |
|
||||
# listening on the default ports. |
|
||||
|
|
||||
config.vm.define 'boxed' do |box| |
|
||||
box.vm.box = "ubuntu/trusty64" |
|
||||
# Configure the network topology to your needs |
|
||||
config.vm.network :private_network, ip: "192.168.33.10" |
|
||||
# config.vm.network :public_network |
|
||||
config.vm.provision :ansible do |ansible| |
|
||||
ansible.playbook = './boxed.yml' |
|
||||
ansible.inventory_path = './inventory' |
|
||||
end |
|
||||
# Forward this postgres to a port on the host |
|
||||
config.vm.network :forwarded_port, guest: 5432, host: 15432 |
|
||||
# TODO: What about also forwarding the data storage? |
|
||||
end |
|
||||
|
|
||||
|
|
||||
# Test machines |
|
||||
# ------------- |
|
||||
|
|
||||
# These test machines will configure the installation with all |
|
||||
# its extensions enabled, in order to test the validity |
|
||||
# of the role. |
|
||||
|
|
||||
# Ubuntu machines are available: |
|
||||
# - "test-ubuntu-precise" |
|
||||
# - "test-ubuntu-trusty" |
|
||||
|
|
||||
def apply_test_ansible_defaults(ansible) |
|
||||
ansible.playbook = './test.yml' |
|
||||
ansible.inventory_path = './inventory' |
|
||||
end |
|
||||
|
|
||||
config.vm.define 'test-ubuntu-trusty', autostart:false do |box| |
|
||||
box.vm.box = "ubuntu/trusty64" |
|
||||
config.vm.network :private_network, ip: "192.168.33.21" |
|
||||
config.vm.provision :ansible do |ansible| |
|
||||
apply_test_ansible_defaults ansible |
|
||||
ansible.extra_vars = {} |
|
||||
end |
|
||||
end |
|
||||
|
|
||||
config.vm.define 'test-ubuntu-precise', autostart:false do |box| |
|
||||
box.vm.box = "ubuntu/precise64" |
|
||||
config.vm.network :private_network, ip: "192.168.33.20" |
|
||||
config.vm.provision :ansible do |ansible| |
|
||||
apply_test_ansible_defaults ansible |
|
||||
ansible.extra_vars = {} |
|
||||
end |
|
||||
end |
|
||||
|
|
||||
end |
|
@ -1,26 +0,0 @@ |
|||||
--- |
|
||||
|
|
||||
# Boxed installation playbook |
|
||||
# --------------------------- |
|
||||
|
|
||||
# A Simple, straight playbook for producing |
|
||||
# a boxed installation in a vagrant VM. |
|
||||
|
|
||||
|
|
||||
- name: 'PostgreSQL boxed installation' |
|
||||
|
|
||||
hosts: vagrant |
|
||||
|
|
||||
vars: |
|
||||
postgresql_listen_addresses: |
|
||||
- '*' |
|
||||
postgresql_authentication: |
|
||||
- type: host |
|
||||
user: all |
|
||||
database: all |
|
||||
address: '0.0.0.0/0' |
|
||||
method: trust |
|
||||
|
|
||||
roles: |
|
||||
- '.' # This role itself |
|
||||
|
|
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue