Browse Source

Merge pull request #11 from BridgeCityBicycleCoop/development

A bunch of stuff
feature/python-error-tracking
Drew Larson 8 years ago
committed by GitHub
parent
commit
cb6abc540e
  1. 9
      .gitignore
  2. 8
      Dockerfile
  3. 14
      Dockerfile-prod
  4. 9
      Dockerfile-webpack
  5. 21
      README.md
  6. 53
      Vagrantfile
  7. 3
      bikeshop_project/.babelrc
  8. 12
      bikeshop_project/.eslintrc
  9. 47
      bikeshop_project/assets/dist/main-c9f2076e75bd0a70cef5.js
  10. 21
      bikeshop_project/assets/js/components/Member.jsx
  11. 24
      bikeshop_project/assets/js/components/Purpose.jsx
  12. 154
      bikeshop_project/assets/js/components/SignIn.jsx
  13. 75
      bikeshop_project/assets/js/components/SignedInList.jsx
  14. 25
      bikeshop_project/assets/js/index.jsx
  15. 29
      bikeshop_project/bikeshop/settings/base.py
  16. 31
      bikeshop_project/bikeshop/settings/development.py
  17. 48
      bikeshop_project/bikeshop/settings/production.py
  18. 13
      bikeshop_project/bikeshop/urls.py
  19. 0
      bikeshop_project/bikeshop/utils/__init__.py
  20. 85
      bikeshop_project/bikeshop/utils/member_import.py
  21. 8
      bikeshop_project/core/admin.py
  22. 77
      bikeshop_project/core/forms.py
  23. 37
      bikeshop_project/core/migrations/0001_initial.py
  24. 21
      bikeshop_project/core/migrations/0002_auto_20170101_2054.py
  25. 25
      bikeshop_project/core/migrations/0002_payment.py
  26. 27
      bikeshop_project/core/migrations/0003_visit.py
  27. 70
      bikeshop_project/core/models.py
  28. 181
      bikeshop_project/core/static/scss/screen.scss
  29. 159
      bikeshop_project/core/templates/base.html
  30. 59
      bikeshop_project/core/templates/dashboard.html
  31. 193
      bikeshop_project/core/templates/membership_form.html
  32. 5
      bikeshop_project/core/urls.py
  33. 42
      bikeshop_project/core/views.py
  34. 49
      bikeshop_project/package.json
  35. 37
      bikeshop_project/registration/admin.py
  36. 47
      bikeshop_project/registration/forms.py
  37. 33
      bikeshop_project/registration/migrations/0001_initial.py
  38. 19
      bikeshop_project/registration/migrations/0002_auto_20161130_0157.py
  39. 78
      bikeshop_project/registration/models.py
  40. 9
      bikeshop_project/registration/search_indexes.py
  41. 221
      bikeshop_project/registration/templates/edit_member_form.html
  42. 67
      bikeshop_project/registration/templates/login.html
  43. 238
      bikeshop_project/registration/templates/member_form.html
  44. 22
      bikeshop_project/registration/templates/members.html
  45. 2
      bikeshop_project/registration/templates/search/indexes/registration/member_text.txt
  46. 3
      bikeshop_project/registration/tests.py
  47. 0
      bikeshop_project/registration/tests/__init__.py
  48. 50
      bikeshop_project/registration/tests/test_models.py
  49. 83
      bikeshop_project/registration/tests/test_utils.py
  50. 141
      bikeshop_project/registration/tests/test_views.py
  51. 10
      bikeshop_project/registration/urls.py
  52. 37
      bikeshop_project/registration/utils.py
  53. 112
      bikeshop_project/registration/views.py
  54. 16
      bikeshop_project/server.js
  55. 0
      bikeshop_project/theme.scss
  56. 35
      bikeshop_project/webpack.base.config.js
  57. 48
      bikeshop_project/webpack.config.js
  58. 42
      bikeshop_project/webpack.dev.config.js
  59. 39
      bikeshop_project/webpack.prod.config.js
  60. 4
      bower.json
  61. 25
      docker-compose.dev.yml
  62. 35
      docker-compose.prod.yml
  63. 17
      docker-compose.yml
  64. 2
      docker/nginx/Dockerfile
  65. 46
      docker/nginx/conf/nginx-site.conf
  66. 14
      provision/group_vars/all
  67. 11
      provision/group_vars/production/non-secrets.yml
  68. 10
      provision/group_vars/production/secrets.yml
  69. 10
      provision/group_vars/staging/non-secrets.yml
  70. 2
      provision/group_vars/staging/secrets.yml
  71. 5
      provision/inventories/production
  72. 3
      provision/roles/app/tasks/main.yml
  73. 5
      provision/roles/app/tasks/prepare.yml
  74. 15
      provision/roles/app/tasks/virtualenv.yml
  75. 2
      provision/roles/app/templates/sparse-checkout
  76. 19
      provision/roles/common/tasks/main.yml
  77. 18
      provision/roles/database/tasks/main.yml
  78. 36
      provision/roles/deploy-code/tasks/main.yml
  79. 14
      provision/roles/django/tasks/main.yml
  80. 3
      provision/roles/handlers/main.yml
  81. 2
      provision/roles/nginx/handlers/main.yml
  82. 12
      provision/roles/nginx/tasks/main.yml
  83. 61
      provision/roles/nginx/templates/production/nginx-site.conf
  84. 31
      provision/roles/nginx/templates/staging/nginx-site.conf
  85. 14
      provision/roles/python/tasks/main.yml
  86. 3
      provision/roles/supervisor/handlers/main.yml
  87. 12
      provision/roles/supervisor/tasks/main.yml
  88. 4
      provision/roles/supervisor/tasks/staging/main.yml
  89. 45
      provision/roles/supervisor/templates/production/supervisor-program.conf
  90. 10
      provision/roles/supervisor/templates/staging/supervisor-program.conf
  91. 4
      provision/roles/tasks/main.yml
  92. 4
      provision/roles/tasks/staging/main.yml
  93. 11
      provision/roles/templates/production/supervisor-program.conf
  94. 10
      provision/roles/templates/staging/supervisor-program.conf
  95. 1
      provision/roles/zenoamaro.postgresql/.gitignore
  96. 28
      provision/roles/zenoamaro.postgresql/.travis.yml
  97. 21
      provision/roles/zenoamaro.postgresql/LICENSE.md
  98. 117
      provision/roles/zenoamaro.postgresql/README.md
  99. 62
      provision/roles/zenoamaro.postgresql/Vagrantfile
  100. 26
      provision/roles/zenoamaro.postgresql/boxed.yml

9
.gitignore

@ -8,3 +8,12 @@ trailersafeguard_project/celerybeat.pid
bikeshop_project/static/CACHE
.bundle
bikeshop_project/vendor
bikeshop_project/assets/bundles
bikeshop_project/webpack-stats.json
/bikeshop_project/htmlcov
/bikeshop_project/.coverage
node_modules
whoosh_index
bikeshop_project/assets/dist
npm-debug.log
bikeshop_project/webpack-stats-prod.json

8
Dockerfile

@ -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

14
Dockerfile-prod

@ -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

9
Dockerfile-webpack

@ -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

21
README.md

@ -1,19 +1,22 @@
## Quickstart
Maybe this isn't such a quick start, but it's the best I have right now. After the following steps are completed, you will have a working development version of the Trailer-Safeguard Django application connectable on [bikeshop-dev.local:8000/](http://bikeshop-dev.local:8000/).
1. Make sure Virtualbox is installed and updated
2. Make sure Vagrant is installed and updated
3. Verify *pip* is installed `which pip`
4. Verify *ansible* is installed `which ansible`
1. Make sure Virtualbox is installed and updated ([https://www.virtualbox.org/](https://www.virtualbox.org/))
2. Make sure Vagrant is installed and updated ([https://www.vagrantup.com/](https://www.vagrantup.com/))
3. Verify *pip* is installed `which pip` (eg. using [http://brew.sh/](homebrew) `brew install pip`)
4. Verify *ansible* is installed `which ansible` (eg. using [http://brew.sh/](homebrew) `brew install ansible`)
5. 4. Verify *bower* is installed `which ansible` (eg. using [http://brew.sh/](homebrew) `brew install bower`)
5. Clone source `git clone AHHHHHH`
6. cd to root of repo & `bower install`
6. `ansible-galaxy install zenoamaro.postgresql -p provision/roles`
7. `vagrant plugin install vagrant-hostsupdater`
8. `vagrant up`
9. `vagrant ssh`
10. `cd /srv/bikeshop && . /opt/venv/bikeshop_development/bin/activate`
10. `cd /srv/bikeshop && . /opt/venv/bikeshop-development/bin/activate`
11. `./manage.py migrate`
12. `./manage.py loaddata core/migrations/initial_data.yaml && ./manage.py loaddata authentication/migrations/initial_data.yaml`
13. `./manage.py runserver 0.0.0.0:8000`
13. try to load [http://bikeshop.local/](http://bikeshop.local/)
13. may need to restart supervisor on vagrant machine `service supervisor restart` if receiving 502
### Example of dumpdata command
./manage.py dumpdata --exclude=auth --exclude=contenttypes --exclude=incoming --format=yaml
@ -31,3 +34,9 @@ GRANT ALL ON SCHEMA public TO public;
COMMENT ON SCHEMA public IS 'standard public schema';
```
6. Steps 11 and 12 from **Quickstart**.
### Starting & Stopping Application
1. cd to root of repo
2. `vagrant up`
3. try to load [http://bikeshop.local/](http://bikeshop.local/)
5. `vagrant halt`

53
Vagrantfile

@ -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

3
bikeshop_project/.babelrc

@ -0,0 +1,3 @@
{
presets: ['es2015', 'stage-0', 'react']
}

12
bikeshop_project/.eslintrc

@ -0,0 +1,12 @@
{
"extends": "airbnb",
"parser": "babel-eslint",
"plugins": [
"react",
"jsx-a11y",
"import"
],
"rules": {
"sort-imports": "error"
}
}

47
bikeshop_project/assets/dist/main-c9f2076e75bd0a70cef5.js

File diff suppressed because one or more lines are too long

21
bikeshop_project/assets/js/components/Member.jsx

@ -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' }}
/>
);
}
}

24
bikeshop_project/assets/js/components/Purpose.jsx

@ -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;

154
bikeshop_project/assets/js/components/SignIn.jsx

@ -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>
);
}
}

75
bikeshop_project/assets/js/components/SignedInList.jsx

@ -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>
);
}
}

25
bikeshop_project/assets/js/index.jsx

@ -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'));

29
bikeshop_project/bikeshop/settings/base.py

@ -29,7 +29,11 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'haystack',
'webpack_loader',
'compressor',
'rest_framework',
'registration',
'core',
]
@ -120,12 +124,35 @@ STATICFILES_FINDERS = (
)
STATICFILES_DIRS = [
('vendor', os.path.join(BASE_DIR, '../vendor')),
os.path.join(BASE_DIR, '../assets')
]
STATIC_ROOT = 'static'
STATIC_URL = '/static/'
AUTH_USER_MODEL = 'registration.Member'
AUTH_USER_MODEL = 'registration.CustomUser'
COMPRESS_PRECOMPILERS = (
('text/x-scss', 'django_libsass.SassCompiler'),
)
WEBPACK_LOADER = {
'DEFAULT': {
'CACHE': False,
'BUNDLE_DIR_NAME': 'bundles/', # must end with slash
'STATS_FILE': os.path.join(BASE_DIR, '../webpack-stats.json'),
'POLL_INTERVAL': 0.1,
'IGNORE': ['.+\.hot-update.js', '.+\.map']
}
}
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
},
}
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
LOGIN_REDIRECT_URL = 'home'
LOGIN_URL = 'login'

31
bikeshop_project/bikeshop/settings/development.py

@ -1,3 +1,4 @@
import sys
from .base import *
@ -12,14 +13,16 @@ ALLOWED_HOSTS = []
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'bikeshop_development',
'USER': 'bikeshop',
'PASSWORD': 'password',
'HOST': '127.0.0.1',
'NAME': 'postgres',
'USER': 'postgres',
'HOST': 'workstand_db_1',
'PORT': '5432',
}
}
if 'test' in sys.argv or 'test_coverage' in sys.argv: #Covers regular testing and django-coverage
DATABASES['default']['ENGINE'] = 'django.db.backends.sqlite3'
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
@ -45,3 +48,23 @@ LOGGING = {
}
},
}
INSTALLED_APPS += [
'corsheaders',
# 'debug_toolbar'
]
MIDDLEWARE_CLASSES.insert(0, 'django.middleware.common.CommonMiddleware')
# MIDDLEWARE_CLASSES += [
# 'debug_toolbar.middleware.DebugToolbarMiddleware'
# ]
# Don't worry about IP addresses, just show the toolbar.
DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': lambda *args: True
}
CORS_ORIGIN_ALLOW_ALL = True
ALLOWED_HOSTS = ['workstand.docker']

48
bikeshop_project/bikeshop/settings/production.py

@ -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']
}
}

13
bikeshop_project/bikeshop/urls.py

@ -13,11 +13,22 @@ Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls import include, url
from django.contrib import admin
from django.contrib.auth.views import login, logout_then_login
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from core import urls as core_urls
from registration import urls as member_urls
urlpatterns = [
url('^', include(core_urls)),
url(r'^', include(core_urls)),
url(r'^login/', login, {'template_name': 'login.html'}, name='login'),
url(r'^logout/', logout_then_login, name='logout'),
url(r'^members/', include(member_urls)),
url(r'^admin/', admin.site.urls),
]
if getattr(settings, 'DEBUG'):
urlpatterns += staticfiles_urlpatterns()

0
bikeshop_project/bikeshop/utils/__init__.py

85
bikeshop_project/bikeshop/utils/member_import.py

@ -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'))

8
bikeshop_project/core/admin.py

@ -1,3 +1,11 @@
from django.contrib import admin
from .models import Membership, Payment, Visit
# 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')

77
bikeshop_project/core/forms.py

@ -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'})
}

37
bikeshop_project/core/migrations/0001_initial.py

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2016-03-23 02:01
# Generated by Django 1.9.4 on 2016-07-04 22:08
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
@ -13,7 +12,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('registration', '0001_initial'),
]
operations = [
@ -24,12 +23,32 @@ class Migration(migrations.Migration):
('created_at', models.DateTimeField(auto_now_add=True)),
('modified_at', models.DateTimeField(auto_now=True)),
('renewed_at', models.DateTimeField(default=django.utils.timezone.now)),
('safe_space', models.BooleanField(default=False)),
('community', models.BooleanField(default=False)),
('space', models.BooleanField(default=False)),
('give_back', models.BooleanField(default=False)),
('acknowledgement', models.BooleanField(default=False)),
('member', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='membership', to=settings.AUTH_USER_MODEL)),
('self_identification', models.CharField(blank=True, max_length=255, null=True)),
('gender', models.CharField(blank=True, max_length=255, null=True)),
('involvement', models.CharField(blank=True, max_length=255, null=True)),
('member', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='registration.Member')),
],
),
migrations.CreateModel(
name='Payment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.CharField(choices=[('NONE', 'None'), ('CASH', 'Cash'), ('CHEQUE', 'Cheque'), ('VOLUNTEERING', 'Volunteering'), ('SQUARE', 'Square'), ('PAYPAL', 'PayPal'), ('UNKNOWN', 'Unknown')], default='NONE', max_length=12)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
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'), ('FIX', 'fix bike'), ('WORKSHOP', 'workshop'), ('VISIT', 'visit'), ('DONATE', 'donate'), ('STAFF', 'staff')], max_length=50)),
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='registration.Member')),
],
),
migrations.AddField(
model_name='membership',
name='payment',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='membership', to='core.Payment'),
),
]

21
bikeshop_project/core/migrations/0002_auto_20170101_2054.py

@ -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),
),
]

25
bikeshop_project/core/migrations/0002_payment.py

@ -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')),
],
),
]

27
bikeshop_project/core/migrations/0003_visit.py

@ -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)),
],
),
]

70
bikeshop_project/core/models.py

@ -1,49 +1,75 @@
from django.db import models
from django.utils import timezone
from django.utils.functional import cached_property
from dateutil.relativedelta import relativedelta
class Membership(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now=True)
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',
on_delete=models.CASCADE,
related_name='membership'
related_name='memberships',
blank=True,
null=True
)
payment = models.OneToOneField(
'Payment',
on_delete=models.CASCADE,
related_name='membership',
blank=False,
null=True
)
safe_space = models.BooleanField(default=False)
community = models.BooleanField(default=False)
space = models.BooleanField(default=False)
give_back = models.BooleanField(default=False)
# this should be a form field that requires the new member to type out there full name
acknowledgement = models.BooleanField(default=False)
@cached_property
def expires_at(self):
return self.renewed_at + relativedelta(years=1)
class Payment(models.Model):
membership = models.ForeignKey(
'Membership',
on_delete=models.CASCADE,
)
payment_choices = (
('CASH', 'cash'),
('CHEQUE', 'cheque'),
('VOLUNTEERING', 'volunteering'),
('STRIPE', 'stripe'),
('PAYPAL', 'paypal')
('NONE', 'None'),
('CASH', 'Cash'),
('CHEQUE', 'Cheque'),
('VOLUNTEERING', 'Volunteering'),
('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)
class Visit(models.Model):
VOLUNTEER = 'VOLUNTEER'
FIX = 'FIX'
WORKSHOP = 'WORKSHOP'
VISIT = 'VISIT'
DONATE = 'DONATE'
STAFF = 'STAFF'
visit_choices = (
('VOLUNTEER', 'volunteer'),
('WORK', 'work on bike'),
('WORKSHOP', 'workshop')
(VOLUNTEER, 'volunteer'),
(FIX, 'fix bike'), # fix
(WORKSHOP, 'workshop'),
(VISIT, 'visit'),
(DONATE, 'donate'),
(STAFF, 'staff'),
)
member = models.ForeignKey(
'registration.Member',
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)
def __str__(self):
return '<Visit purpose: {purpose} created_at: {created_at}>'.format(purpose=self.purpose,
created_at=self.created_at.isoformat())

181
bikeshop_project/core/static/scss/screen.scss

@ -1,24 +1,145 @@
/**
* Copyright 2015 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "vendor/material-design-lite/src/material-design-lite.scss";
@import "vendor/material-design-lite/src/textfield/textfield";
@import "vendor/material-design-lite/src/typography/typography";
@import "vendor/material-design-lite/src/shadow/shadow";
@import "vendor/material-design-lite/src/ripple/ripple";
@import "vendor/material-design-lite/src/grid/grid";
@import "vendor/material-design-lite/src/layout/layout";
@import "vendor/material-design-lite/src/footer/mega_footer";
@import "vendor/material-design-lite/src/checkbox/checkbox";
@import "vendor/material-design-lite/src/radio/radio";
@import "vendor/material-design-lite/src/button/button";
@import "vendor/material-design-lite/src/palette/palette";
@import "vendor/material-design-lite/src/menu/menu";
@import "vendor/material-design-lite/src/card/card";
@import "vendor/material-design-lite/src/data-table/data-table";
@import "vendor/material-design-lite/src/list/list";
html, body {
font-family: 'Roboto', 'Helvetica', sans-serif;
}
/* ==========================================================================
Helper classes
========================================================================== */
/*
* Hide visually and from screen readers:
*/
.hidden {
display: none !important; }
/*
* Hide only visually, but have it available for screen readers:
* http://snook.ca/archives/html_and_css/hiding-content-for-accessibility
*/
.visuallyhidden {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px; }
/*
* Extends the .visuallyhidden class to allow the element
* to be focusable when navigated to via the keyboard:
* https://www.drupal.org/node/897638
*/
.visuallyhidden.focusable:active,
.visuallyhidden.focusable:focus {
clip: auto;
height: auto;
margin: 0;
overflow: visible;
position: static;
width: auto; }
/*
* Hide visually and from screen readers, but maintain layout
*/
.invisible {
visibility: hidden; }
/*
* Clearfix: contain floats
*
* For modern browsers
* 1. The space content is one way to avoid an Opera bug when the
* `contenteditable` attribute is included anywhere else in the document.
* Otherwise it causes space to appear at the top and bottom of elements
* that receive the `clearfix` class.
* 2. The use of `table` rather than `block` is only necessary if using
* `:before` to contain the top-margins of child elements.
*/
.clearfix:before,
.clearfix:after {
content: " ";
/* 1 */
display: table;
/* 2 */ }
.clearfix:after {
clear: both; }
/* ==========================================================================
EXAMPLE Media Queries for Responsive Design.
These examples override the primary ('mobile first') styles.
Modify as content requires.
========================================================================== */
/* ==========================================================================
Print styles.
Inlined to avoid the additional HTTP request:
http://www.phpied.com/delay-loading-your-print-css/
========================================================================== */
@media print {
*,
*:before,
*:after,
*:first-letter {
background: transparent !important;
color: #000 !important;
/* Black prints faster: http://www.sanbeiji.com/archives/953 */
box-shadow: none !important; }
a,
a:visited {
text-decoration: underline; }
a[href]:after {
content: " (" attr(href) ")"; }
abbr[title]:after {
content: " (" attr(title) ")"; }
/*
* Don't show links that are fragment identifiers,
* or use the `javascript:` pseudo protocol
*/
a[href^="#"]:after,
a[href^="javascript:"]:after {
content: ""; }
pre,
blockquote {
border: 1px solid #999;
page-break-inside: avoid; }
/*
* Printing Tables:
* http://css-discuss.incutio.com/wiki/Printing_Tables
*/
thead {
display: table-header-group; }
tr,
img {
page-break-inside: avoid; }
img {
max-width: 100% !important; }
p,
h2,
h3 {
orphans: 3;
widows: 3; }
h2,
h3 {
page-break-after: avoid; } }
.demo-avatar {
width: 48px;
height: 48px;
@ -224,3 +345,31 @@ _:-ms-input-placeholder, :root .demo-graph {
-ms-flex-align: center;
align-items: center;
}
.mddtp-picker {
z-index: 1;
}
span.mdl-textfield__error, span.error {
ul {
margin: 0;
padding: 0;
}
li {
float: left;
margin-right: 5px;
list-style: none inside none;
}
}
.error {
color: rgb(213,0,0);
font-size: 12px;
margin-top: 3px;
visibility: visible;
display: block;
}
html [type="button"] {
-webkit-appearance: initial !important;
}

159
bikeshop_project/core/templates/base.html

@ -1,28 +1,12 @@
{% load compress staticfiles %}
<!doctype html>
<!--
Material Design Lite
Copyright 2015 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="description" content="A front-end template that helps you build fast, modern mobile web apps.">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<title>Material Design Lite</title>
<title>BCBC Workstand</title>
<!-- Add to homescreen for Chrome on Android -->
<meta name="mobile-web-app-capable" content="yes">
@ -47,9 +31,11 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:regular,bold,italic,thin,light,bolditalic,black,medium&amp;lang=en">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" type="text/css" href="{% static 'vendor/normalize-css/normalize.css' %}">
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static 'scss/screen.scss' %}">
{% endcompress %}
{% block styles %}{% endblock %}
<style>
#view-source {
position: fixed;
@ -64,139 +50,10 @@
</head>
<body>
<div class="demo-layout mdl-layout mdl-js-layout mdl-layout--fixed-drawer mdl-layout--fixed-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">Home</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">
<img src="{% static 'images/user.jpg' %}" class="demo-avatar">
<div class="demo-avatar-dropdown">
<span>hello@example.com</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">hello@example.com</li>
<li class="mdl-menu__item">info@example.com</li>
<li class="mdl-menu__item"><i class="material-icons">add</i>Add another account...</li>
</ul>
</div>
</header>
<nav class="demo-navigation mdl-navigation mdl-color--blue-grey-800">
<a class="mdl-navigation__link" href=""><i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">home</i>Home</a>
<a class="mdl-navigation__link" href=""><i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">inbox</i>Inbox</a>
<a class="mdl-navigation__link" href=""><i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">delete</i>Trash</a>
<a class="mdl-navigation__link" href=""><i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">report</i>Spam</a>
<a class="mdl-navigation__link" href=""><i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">forum</i>Forums</a>
<a class="mdl-navigation__link" href=""><i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">flag</i>Updates</a>
<a class="mdl-navigation__link" href=""><i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">local_offer</i>Promos</a>
<a class="mdl-navigation__link" href=""><i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">shopping_cart</i>Purchases</a>
<a class="mdl-navigation__link" href=""><i class="mdl-color-text--blue-grey-400 material-icons" role="presentation">people</i>Social</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>
{% block header %}{% endblock %}
<main class="mdl-layout__content mdl-color--grey-100">
<div class="mdl-grid demo-content">
<div class="demo-charts mdl-color--white mdl-shadow--2dp mdl-cell mdl-cell--12-col mdl-grid">
<svg fill="currentColor" width="200px" height="200px" viewBox="0 0 1 1" class="demo-chart mdl-cell mdl-cell--4-col mdl-cell--3-col-desktop">
<use xlink:href="#piechart" mask="url(#piemask)" />
<text x="0.5" y="0.5" font-family="Roboto" font-size="0.3" fill="#888" text-anchor="middle" dy="0.1">82<tspan font-size="0.2" dy="-0.07">%</tspan></text>
</svg>
<svg fill="currentColor" width="200px" height="200px" viewBox="0 0 1 1" class="demo-chart mdl-cell mdl-cell--4-col mdl-cell--3-col-desktop">
<use xlink:href="#piechart" mask="url(#piemask)" />
<text x="0.5" y="0.5" font-family="Roboto" font-size="0.3" fill="#888" text-anchor="middle" dy="0.1">82<tspan dy="-0.07" font-size="0.2">%</tspan></text>
</svg>
<svg fill="currentColor" width="200px" height="200px" viewBox="0 0 1 1" class="demo-chart mdl-cell mdl-cell--4-col mdl-cell--3-col-desktop">
<use xlink:href="#piechart" mask="url(#piemask)" />
<text x="0.5" y="0.5" font-family="Roboto" font-size="0.3" fill="#888" text-anchor="middle" dy="0.1">82<tspan dy="-0.07" font-size="0.2">%</tspan></text>
</svg>
<svg fill="currentColor" width="200px" height="200px" viewBox="0 0 1 1" class="demo-chart mdl-cell mdl-cell--4-col mdl-cell--3-col-desktop">
<use xlink:href="#piechart" mask="url(#piemask)" />
<text x="0.5" y="0.5" font-family="Roboto" font-size="0.3" fill="#888" text-anchor="middle" dy="0.1">82<tspan dy="-0.07" font-size="0.2">%</tspan></text>
</svg>
</div>
<div class="demo-graphs mdl-shadow--2dp mdl-color--white mdl-cell mdl-cell--8-col">
<svg fill="currentColor" viewBox="0 0 500 250" class="demo-graph">
<use xlink:href="#chart" />
</svg>
<svg fill="currentColor" viewBox="0 0 500 250" class="demo-graph">
<use xlink:href="#chart" />
</svg>
</div>
<div class="demo-cards mdl-cell mdl-cell--4-col mdl-cell--8-col-tablet mdl-grid mdl-grid--no-spacing">
<div class="demo-updates mdl-card mdl-shadow--2dp mdl-cell mdl-cell--4-col mdl-cell--4-col-tablet mdl-cell--12-col-desktop">
<div class="mdl-card__title mdl-card--expand mdl-color--teal-300">
<h2 class="mdl-card__title-text">Updates</h2>
</div>
<div class="mdl-card__supporting-text mdl-color-text--grey-600">
Non dolore elit adipisicing ea reprehenderit consectetur culpa.
</div>
<div class="mdl-card__actions mdl-card--border">
<a href="#" class="mdl-button mdl-js-button mdl-js-ripple-effect">Read More</a>
</div>
</div>
<div class="demo-separator mdl-cell--1-col"></div>
<div class="demo-options mdl-card mdl-color--deep-purple-500 mdl-shadow--2dp mdl-cell mdl-cell--4-col mdl-cell--3-col-tablet mdl-cell--12-col-desktop">
<div class="mdl-card__supporting-text mdl-color-text--blue-grey-50">
<h3>View options</h3>
<ul>
<li>
<label for="chkbox1" class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect">
<input type="checkbox" id="chkbox1" class="mdl-checkbox__input">
<span class="mdl-checkbox__label">Click per object</span>
</label>
</li>
<li>
<label for="chkbox2" class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect">
<input type="checkbox" id="chkbox2" class="mdl-checkbox__input">
<span class="mdl-checkbox__label">Views per object</span>
</label>
</li>
<li>
<label for="chkbox3" class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect">
<input type="checkbox" id="chkbox3" class="mdl-checkbox__input">
<span class="mdl-checkbox__label">Objects selected</span>
</label>
</li>
<li>
<label for="chkbox4" class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect">
<input type="checkbox" id="chkbox4" class="mdl-checkbox__input">
<span class="mdl-checkbox__label">Objects viewed</span>
</label>
</li>
</ul>
</div>
<div class="mdl-card__actions mdl-card--border">
<a href="#" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-color-text--blue-grey-50">Change location</a>
<div class="mdl-layout-spacer"></div>
<i class="material-icons">location_on</i>
</div>
</div>
</div>
</div>
{% block content %}
{% endblock %}
</main>
</div>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" style="position: fixed; left: -1000px; height: -1000px;">
@ -246,7 +103,7 @@
</g>
</defs>
</svg>
<a href="https://github.com/google/material-design-lite/blob/master/templates/dashboard/" target="_blank" id="view-source" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored mdl-color-text--white">View Source</a>
<script src="{% static 'vendor/material-design-lite/material.js' %}"></script>
{% block scripts %}{% endblock %}
</body>
</html>

59
bikeshop_project/core/templates/dashboard.html

@ -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 %}

193
bikeshop_project/core/templates/membership_form.html

@ -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 Co­operative aims to be a safe and respectful environment geared towards education, empowerment and community­building. 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 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. 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 %}

5
bikeshop_project/core/urls.py

@ -1,6 +1,7 @@
from django.conf.urls import url
from .views import DashboardView
from .views import DashboardView, NewMembershipView
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')
]

42
bikeshop_project/core/views.py

@ -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.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):
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))

49
bikeshop_project/package.json

@ -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"
}
}

37
bikeshop_project/registration/admin.py

@ -1,3 +1,38 @@
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')

47
bikeshop_project/registration/forms.py

@ -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

33
bikeshop_project/registration/migrations/0001_initial.py

@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2016-03-23 01:25
# Generated by Django 1.9.4 on 2016-07-04 22:04
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):
@ -15,13 +17,28 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name='Member',
name='CustomUser',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('email', models.EmailField(max_length=255, unique=True, verbose_name='email address')),
('is_admin', models.BooleanField(default=False)),
('is_active', models.BooleanField(default=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Member',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(blank=True, max_length=255, null=True, verbose_name='email address')),
('email_consent', models.BooleanField(default=False)),
('first_name', models.CharField(max_length=255)),
('last_name', models.CharField(max_length=255)),
('preferred_name', models.CharField(blank=True, max_length=255, null=True)),
@ -32,18 +49,10 @@ class Migration(migrations.Migration):
('city', models.CharField(blank=True, max_length=255, null=True)),
('province', models.CharField(blank=True, max_length=255, null=True)),
('country', models.CharField(blank=True, max_length=255, null=True)),
('post_code', models.CharField(blank=True, max_length=20, null=True)),
('self_identification', models.CharField(blank=True, max_length=255, null=True)),
('gender', models.CharField(blank=True, max_length=255, null=True)),
('involvement', models.CharField(blank=True, max_length=255, null=True)),
('post_code', models.CharField(max_length=20, null=True)),
('waiver', models.DateTimeField(blank=True, null=True)),
('is_active', models.BooleanField(default=True)),
('is_admin', models.BooleanField(default=False)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
('user', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
]

19
bikeshop_project/registration/migrations/0002_auto_20161130_0157.py

@ -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'},
),
]

78
bikeshop_project/registration/models.py

@ -1,15 +1,14 @@
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.contrib.auth.models import (AbstractBaseUser, BaseUserManager,
PermissionsMixin)
from django.db import models
class CustomMemberManager(BaseUserManager):
def create_user(self, email, first_name, last_name, password=None):
class CustomUserManager(BaseUserManager):
def create_user(self, email, password=None):
"""
Creates and saves a User with the given email and password.
:param email: str
:param password: str
:param first_name: str
:param last_name: str
:return: object `CustomUser`
"""
if not email:
@ -17,7 +16,7 @@ class CustomMemberManager(BaseUserManager):
user = self.model(
email=self.normalize_email(email),
)
)
user.set_password(password)
user.save(using=self._db)
@ -38,12 +37,49 @@ class CustomMemberManager(BaseUserManager):
return user
class Member(AbstractBaseUser, PermissionsMixin):
class CustomUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(
verbose_name='email address',
max_length=255,
unique=True,
)
)
is_admin = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
objects = CustomUserManager()
USERNAME_FIELD = 'email'
@property
def is_staff(self):
# Simplest possible answer: All admins are staff
return self.is_admin
def get_short_name(self):
return self.email
def get_full_name(self):
return self.email
def __str__(self): # __unicode__ on Python 2
return self.email
class Meta:
verbose_name = 'User'
verbose_name_plural = 'Users'
class Member(models.Model):
user = models.OneToOneField(CustomUser, on_delete=models.CASCADE,
null=True)
email = models.EmailField(
verbose_name='email address',
max_length=255,
unique=False,
null=True,
blank=True,
)
email_consent = models.BooleanField(default=False)
first_name = models.CharField(max_length=255, null=False)
last_name = models.CharField(max_length=255, null=False)
preferred_name = models.CharField(max_length=255, null=True, blank=True)
@ -54,34 +90,20 @@ class Member(AbstractBaseUser, PermissionsMixin):
city = models.CharField(max_length=255, null=True, blank=True)
province = models.CharField(max_length=255, null=True, blank=True)
country = models.CharField(max_length=255, null=True, blank=True)
post_code = models.CharField(max_length=20, null=True, blank=True)
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)
post_code = models.CharField(max_length=20, null=True, blank=False)
waiver = models.DateTimeField(null=True, blank=True)
is_active = models.BooleanField(default=True)
is_admin = models.BooleanField(default=False)
objects = CustomMemberManager()
USERNAME_FIELD = 'email'
# REQUIRED_FIELDS = []
def get_full_name(self):
# The user is identified by their email address
if self.first_name and self.last_name:
return '{0} {1}'.format(self.first_name, self.last_name)
else:
return self.email
return '{0} {1}'.format(self.first_name, self.last_name)
def get_short_name(self):
# The user is identified by their email address
return self.email
if self.email:
return self.email
else:
return self.last_name
def __str__(self): # __unicode__ on Python 2
return self.email
@property
def is_staff(self):
# Simplest possible answer: All admins are staff
return self.is_admin

9
bikeshop_project/registration/search_indexes.py

@ -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

221
bikeshop_project/registration/templates/edit_member_form.html

@ -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 %}

67
bikeshop_project/registration/templates/login.html

@ -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 %}

238
bikeshop_project/registration/templates/member_form.html

@ -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 %}

22
bikeshop_project/registration/templates/members.html

@ -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 %}

2
bikeshop_project/registration/templates/search/indexes/registration/member_text.txt

@ -0,0 +1,2 @@
{{ object.email }}
{{ object.get_full_name }}

3
bikeshop_project/registration/tests.py

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

0
bikeshop_project/registration/tests/__init__.py

50
bikeshop_project/registration/tests/test_models.py

@ -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')

83
bikeshop_project/registration/tests/test_utils.py

@ -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)

141
bikeshop_project/registration/tests/test_views.py

@ -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)

10
bikeshop_project/registration/urls.py

@ -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'),
]

37
bikeshop_project/registration/utils.py

@ -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

112
bikeshop_project/registration/views.py

@ -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))

16
bikeshop_project/server.js

@ -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
bikeshop_project/theme.scss

35
bikeshop_project/webpack.base.config.js

@ -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]
}

48
bikeshop_project/webpack.config.js

@ -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]
}

42
bikeshop_project/webpack.dev.config.js

@ -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

39
bikeshop_project/webpack.prod.config.js

@ -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

4
bower.json

@ -17,6 +17,8 @@
"tests"
],
"dependencies": {
"material-design-lite": "^1.1.3"
"material-design-lite": "^1.3.0",
"md-date-time-picker": "https://github.com/puranjayjain/md-date-time-picker.git#master",
"normalize-css": "normalize.css#^4.1.1"
}
}

25
docker-compose.dev.yml

@ -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

35
docker-compose.prod.yml

@ -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

17
docker-compose.yml

@ -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

2
docker/nginx/Dockerfile

@ -0,0 +1,2 @@
FROM nginx:alpine
COPY conf/* /etc/nginx/conf.d/

46
docker/nginx/conf/nginx-site.conf

@ -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;
}
}

14
provision/group_vars/all

@ -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

11
provision/group_vars/production/non-secrets.yml

@ -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

10
provision/group_vars/production/secrets.yml

@ -1,10 +0,0 @@
$ANSIBLE_VAULT;1.1;AES256
32363235643965636135663037643239343762353631383934636533373537666561373063353261
6363393337636562653565303362366437313038633765330a396239336337356130353932656563
64623464623637323736376665343539353634326636333134363961653431303065373337616238
6633346461346266620a383536383331663262373736353230643736356134323136363230316136
61353131326165346564626237306630323962366239653837323463653639616363303562396662
32626165613563313130376438656561656664663231303839303935376335396533646534326661
38306561303432326236346365353034663736376335616434633332313735383236396431636532
36643163333762613065373861333265326363613365623330323135363739663931666466373961
6362

10
provision/group_vars/staging/non-secrets.yml

@ -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

2
provision/group_vars/staging/secrets.yml

@ -1,2 +0,0 @@
secret_db_user_password: password
secret_django_key: somekey

5
provision/inventories/production

@ -1,5 +0,0 @@
[production]
1.1.1.1 ansible_ssh_private_key_file=~/.ssh/id_rsa
[servers:children]
production

3
provision/roles/app/tasks/main.yml

@ -1,3 +0,0 @@
- include: prepare.yml
- include: virtualenv.yml

5
provision/roles/app/tasks/prepare.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

15
provision/roles/app/tasks/virtualenv.yml

@ -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

2
provision/roles/app/templates/sparse-checkout

@ -1,2 +0,0 @@
ninemilelegacy_project/core
ninemilelegacy_project/ninemilelegacy

19
provision/roles/common/tasks/main.yml

@ -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

18
provision/roles/database/tasks/main.yml

@ -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'

36
provision/roles/deploy-code/tasks/main.yml

@ -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

14
provision/roles/django/tasks/main.yml

@ -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 }}"

3
provision/roles/handlers/main.yml

@ -1,3 +0,0 @@
---
- name: restart supervisor
service: name=supervisor state=restarted

2
provision/roles/nginx/handlers/main.yml

@ -1,2 +0,0 @@
- name: restart nginx
service: name=nginx state=restarted

12
provision/roles/nginx/tasks/main.yml

@ -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

61
provision/roles/nginx/templates/production/nginx-site.conf

@ -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/;
}
}

31
provision/roles/nginx/templates/staging/nginx-site.conf

@ -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 }}/;
}
}

14
provision/roles/python/tasks/main.yml

@ -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

3
provision/roles/supervisor/handlers/main.yml

@ -1,3 +0,0 @@
---
- name: start supervisor
service: name=supervisor state=started

12
provision/roles/supervisor/tasks/main.yml

@ -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

4
provision/roles/supervisor/tasks/staging/main.yml

@ -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

45
provision/roles/supervisor/templates/production/supervisor-program.conf

@ -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

10
provision/roles/supervisor/templates/staging/supervisor-program.conf

@ -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

4
provision/roles/tasks/main.yml

@ -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

4
provision/roles/tasks/staging/main.yml

@ -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

11
provision/roles/templates/production/supervisor-program.conf

@ -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

10
provision/roles/templates/staging/supervisor-program.conf

@ -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
provision/roles/zenoamaro.postgresql/.gitignore

@ -1 +0,0 @@
/.vagrant/

28
provision/roles/zenoamaro.postgresql/.travis.yml

@ -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

21
provision/roles/zenoamaro.postgresql/LICENSE.md

@ -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.

117
provision/roles/zenoamaro.postgresql/README.md

@ -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.

62
provision/roles/zenoamaro.postgresql/Vagrantfile

@ -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

26
provision/roles/zenoamaro.postgresql/boxed.yml

@ -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…
Cancel
Save