Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 186f257704 | |||
| 6c14e13d08 | |||
| 8db931a4c7 | |||
| 1067b4fc3b | |||
| ae825d0342 | |||
| 4da511a7d5 | |||
| 198cfb4bdb | |||
| 9c42cb7890 | |||
| b5a4493af5 | |||
| c51a4e7f5a | |||
| 4dbdc02d15 | |||
| 6392a582a5 | |||
| dd3ba8cec8 | |||
| dfb4a7a761 |
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2019 Sean Cusack
|
||||
|
||||
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.
|
||||
97
README.md
97
README.md
@@ -1 +1,96 @@
|
||||
This project is to allow homebrew club members to be given an identifying number/letter to put on their bottles for competitions.
|
||||
<!-- PROJECT LOGO -->
|
||||
<br />
|
||||
<p align="center">
|
||||
<img src="logo.png" alt="Logo" width="80" height="80">
|
||||
|
||||
<h3 align="center">Wigan Homebrew Club Competition Entry</h3>
|
||||
|
||||
<p align="center">
|
||||
A project to facilitate anonymous entry into Wigan HBC competitions!
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://wiganhomebrew.cusack.cloud/">View Demo</a>
|
||||
·
|
||||
<a href="https://git.cusack.cloud/acid/wiganhbc-competition/issues">Report Bug</a>
|
||||
·
|
||||
<a href="https://git.cusack.cloud/acid/wiganhbc-competition/issues">Request Feature</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
<!-- TABLE OF CONTENTS -->
|
||||
## Table of Contents
|
||||
* [About the Project](#about-the-project)
|
||||
* [Built With](#built-with)
|
||||
* [Getting Started](#getting-started)
|
||||
* [Prerequisites](#prerequisites)
|
||||
* [Running The Project](#running-the-project)
|
||||
* [Roadmap](#roadmap)
|
||||
* [License](#license)
|
||||
* [Contact](#contact)
|
||||
|
||||
<!-- ABOUT THE PROJECT -->
|
||||
## About The Project
|
||||
|
||||

|
||||
|
||||
This project is to allow homebrew club members to be given an identifying number/letter to put on their bottles for competitions. The goal is to allow all members to take part with no invigilator/organiser required. It support around 100 entries (alphabetical identifiers are given first, then numerical)
|
||||
|
||||
### Built With
|
||||
This project is built in python and deployed in docker.
|
||||
* [Flask](http://flask.pocoo.org/)
|
||||
* [Bootstrap](https://getbootstrap.com)
|
||||
* [Docker](https://www.docker.com/)
|
||||
|
||||
|
||||
|
||||
<!-- GETTING STARTED -->
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
For development you need to have python3 installed along with pip. You can then set up a virtualenv and install requirements
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
### Running the project
|
||||
|
||||
A sample Dockerfile is included. Please note that you must supply an HBC_DB_PATH environmental variable. Below is a sample docker compose that can be used after you clone this repo.
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
wiganhomebrew:
|
||||
container_name: wiganhomebrew
|
||||
build: <relative path to repo directory>
|
||||
restart: always
|
||||
environment:
|
||||
- HBC_DB_PATH=/data
|
||||
volumes:
|
||||
- sqlite-database-volume:/data
|
||||
ports:
|
||||
- 5000:5000
|
||||
|
||||
volumes:
|
||||
sqlite-database-volume:
|
||||
```
|
||||
|
||||
|
||||
<!-- ROADMAP -->
|
||||
## Roadmap
|
||||
|
||||
See the [open issues](https://git.cusack.cloud/acid/wiganhbc-competition/issues) for a list of proposed features (and known issues).
|
||||
|
||||
|
||||
<!-- LICENSE -->
|
||||
## License
|
||||
|
||||
Distributed under the MIT License. See `LICENSE` for more information.
|
||||
|
||||
|
||||
<!-- CONTACT -->
|
||||
## Contact
|
||||
|
||||
Sean Cusack - seancusack@gmail.com
|
||||
|
||||
Project Link: [https://git.cusack.cloud/acid/wiganhbc-competition](https://git.cusack.cloud/acid/wiganhbc-competition)
|
||||
BIN
screenshot.PNG
Normal file
BIN
screenshot.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 932 KiB |
@@ -1,6 +1,15 @@
|
||||
import pytest
|
||||
import sys, os
|
||||
myPath = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append(myPath + '/../web/')
|
||||
try:
|
||||
os.remove('./tests/hbc.db')
|
||||
except:
|
||||
pass
|
||||
|
||||
from web import app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
app.app.config['TESTING'] = True
|
||||
@@ -8,18 +17,24 @@ def client():
|
||||
|
||||
yield client
|
||||
|
||||
def test_root_page(client):
|
||||
|
||||
def test_root_page(client):
|
||||
rv = client.get('/')
|
||||
assert rv.status_code == 200
|
||||
assert b'Please enter your first name and initial' in rv.data
|
||||
|
||||
def test_generate_first(client):
|
||||
rv2 = client.post('/generate', data=dict(name='tester'), follow_redirects=True)
|
||||
assert rv2.status_code == 200
|
||||
assert b'Please mark all of your bottlecaps with the following identifier: <strong>A' in rv2.data
|
||||
|
||||
def test_generate_second(client):
|
||||
def test_generate(client):
|
||||
for identifier in app.my_db.get_identifiers_list():
|
||||
rv = client.post('/generate', data=dict(name='tester'), follow_redirects=True)
|
||||
assert rv.status_code == 200
|
||||
assert b'Please mark all of your bottlecaps with the following identifier: <strong>' \
|
||||
+ str(identifier).encode('UTF-8') in rv.data
|
||||
|
||||
|
||||
def test_generate_over_limit(client):
|
||||
rv = client.post('/generate', data=dict(name='tester'), follow_redirects=True)
|
||||
assert rv.status_code == 200
|
||||
assert b'Please mark all of your bottlecaps with the following identifier: <strong>B' in rv.data
|
||||
assert b'Maximum entry limit reached - please contact Sean or Joe' in rv.data
|
||||
|
||||
|
||||
|
||||
74
web/app.py
74
web/app.py
@@ -1,52 +1,16 @@
|
||||
from flask import Flask, render_template, request
|
||||
from flask_bootstrap import Bootstrap
|
||||
import sqlite3
|
||||
import os
|
||||
import string
|
||||
|
||||
import db
|
||||
import config
|
||||
import utils
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config.BaseConfig)
|
||||
Bootstrap(app)
|
||||
my_db = db.Database(app.config['DB_PATH'])
|
||||
|
||||
|
||||
def get_db_connection():
|
||||
db_path = os.environ.get('HBC_DB_PATH')
|
||||
if not db_path:
|
||||
raise Exception("DB Path not defined")
|
||||
|
||||
return sqlite3.connect(db_path + '/hbc.db')
|
||||
|
||||
|
||||
def db_setup():
|
||||
conn = get_db_connection()
|
||||
|
||||
sql_create_table = """ CREATE TABLE IF NOT EXISTS brewers (
|
||||
id integer PRIMARY KEY,
|
||||
name text NOT NULL,
|
||||
identifier text NOT NULL );"""
|
||||
c = conn.cursor()
|
||||
c.execute(sql_create_table)
|
||||
conn.close()
|
||||
|
||||
|
||||
db_setup()
|
||||
brew_name = "Fruit Beer"
|
||||
brew_month = "October"
|
||||
identifiers = list(string.ascii_uppercase) + list(range(1, 100))
|
||||
|
||||
|
||||
def get_identifier(name):
|
||||
conn = get_db_connection()
|
||||
c = conn.cursor()
|
||||
for identifier in identifiers:
|
||||
c.execute('''SELECT identifier FROM brewers WHERE identifier=?''', (str(identifier),))
|
||||
data = c.fetchone()
|
||||
if data is None:
|
||||
print("Assigning " + identifier + " to " + name)
|
||||
c.execute("INSERT INTO brewers (name,identifier) VALUES(?, ?)",(name, identifier) )
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return identifier
|
||||
|
||||
|
||||
@app.route('/')
|
||||
@@ -56,8 +20,32 @@ def hello_world():
|
||||
|
||||
@app.route('/generate', methods=["POST"])
|
||||
def generate():
|
||||
identifier = get_identifier(request.form['name'])
|
||||
return render_template('generate.html', brew_name=brew_name, brew_month=brew_month, identifier=identifier)
|
||||
try:
|
||||
ip = utils.get_ip(request)
|
||||
pass
|
||||
except:
|
||||
ip = ''
|
||||
pass
|
||||
|
||||
if ip != '5.135.188.148' and ip != '178.32.58.160':
|
||||
try:
|
||||
error = None
|
||||
identifier = my_db.get_identifier(request.form['name'])
|
||||
pass
|
||||
except StopIteration:
|
||||
identifier = ''
|
||||
error = 'Maximum entry limit reached - please contact Sean or Joe'
|
||||
pass
|
||||
else:
|
||||
error = None
|
||||
identifier = 'F'
|
||||
|
||||
return render_template('generate.html', brew_name=brew_name, brew_month=brew_month, identifier=identifier, error=error)
|
||||
|
||||
|
||||
@app.route('/getip')
|
||||
def getip():
|
||||
return utils.get_ip(request)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
6
web/config.py
Normal file
6
web/config.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import os
|
||||
|
||||
|
||||
class BaseConfig(object):
|
||||
TESTING = True
|
||||
DB_PATH = os.environ.get('HBC_DB_PATH', 'tests/')
|
||||
92
web/db.py
Normal file
92
web/db.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import sqlite3
|
||||
import string
|
||||
import utils
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self, db_path):
|
||||
if not db_path:
|
||||
raise Exception("DB Path not defined")
|
||||
self.db_path = db_path
|
||||
self.identifiers = self.get_identifiers_list()
|
||||
self.setup_database_tables()
|
||||
|
||||
@staticmethod
|
||||
def get_identifiers_list():
|
||||
"""Returns a list of non ambiguous identifiers"""
|
||||
|
||||
numbers = list(range(1, 100))
|
||||
identifiers = list(string.ascii_uppercase) + [str(item) for item in
|
||||
numbers] # All identifiers are treated as strings
|
||||
ambiguous = ['I', 'O', 'V', '1', '8', '5', '9', '99']
|
||||
utils.remove_common_elements(identifiers, ambiguous)
|
||||
return identifiers
|
||||
|
||||
def get_connection(self):
|
||||
"""Returns sqlite db connection when provided with base directory"""
|
||||
return sqlite3.connect(self.db_path + '/hbc.db')
|
||||
|
||||
def setup_database_tables(self):
|
||||
"""Creates sqlite database and set up the sqlite table if it doesnt already exist"""
|
||||
conn = self.get_connection()
|
||||
|
||||
sql_create_table = """ CREATE TABLE IF NOT EXISTS brewers (
|
||||
id integer PRIMARY KEY,
|
||||
name text NOT NULL,
|
||||
identifier text NOT NULL );"""
|
||||
c = conn.cursor()
|
||||
c.execute(sql_create_table)
|
||||
conn.close()
|
||||
|
||||
def get_identifier(self, name):
|
||||
"""Returns the next availible identifier, passing the result through record_entry to make sure it is not reused"""
|
||||
conn = self.get_connection()
|
||||
c = conn.cursor()
|
||||
c.execute('''select identifier from brewers ORDER BY id DESC LIMIT 1;''')
|
||||
identifier_search_result = c.fetchone()
|
||||
conn.close()
|
||||
|
||||
if identifier_search_result is None:
|
||||
return self.record_entry(self.identifiers[0], name)
|
||||
else:
|
||||
if identifier_search_result[0] == self.identifiers[-1]:
|
||||
raise StopIteration
|
||||
else:
|
||||
i = self.identifiers.index(identifier_search_result[0])
|
||||
return self.record_entry(self.identifiers[i + 1], name)
|
||||
|
||||
def record_entry(self, identifier, name):
|
||||
"""Returns identifier after recording entry in sqlite database"""
|
||||
conn = self.get_connection()
|
||||
c = conn.cursor()
|
||||
c.execute("INSERT INTO brewers (name,identifier) VALUES(?, ?)", (name, identifier))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return identifier
|
||||
|
||||
|
||||
|
||||
# def get_identifier(name, db_path):
|
||||
# conn = get_connection(db_path)
|
||||
# c = conn.cursor()
|
||||
# c.execute('''select identifier from brewers ORDER BY id DESC LIMIT 1;''')
|
||||
# identifier_search_result = c.fetchone()
|
||||
# conn.close()
|
||||
#
|
||||
# if identifier_search_result is None:
|
||||
# return record_entry(DataStore.identifiers[0], name, db_path)
|
||||
# else:
|
||||
# if identifier_search_result[0] == DataStore.identifiers[-1]:
|
||||
# raise StopIteration
|
||||
# else:
|
||||
# i = DataStore.identifiers.index(identifier_search_result[0])
|
||||
# return record_entry(DataStore.identifiers[i+1], name, db_path)
|
||||
|
||||
|
||||
# def record_entry(identifier, name, db_path):
|
||||
# conn = get_connection(db_path)
|
||||
# c = conn.cursor()
|
||||
# c.execute("INSERT INTO brewers (name,identifier) VALUES(?, ?)", (name, identifier))
|
||||
# conn.commit()
|
||||
# conn.close()
|
||||
# return identifier
|
||||
BIN
web/static/background3.jpg
Normal file
BIN
web/static/background3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
32
web/static/mystyle.css
Normal file
32
web/static/mystyle.css
Normal file
@@ -0,0 +1,32 @@
|
||||
body {
|
||||
background: url('background3.jpg') no-repeat center center fixed;
|
||||
-webkit-background-size: cover;
|
||||
-moz-background-size: cover;
|
||||
background-size: cover;
|
||||
-o-background-size: cover;
|
||||
}
|
||||
|
||||
.content-div {
|
||||
background-color: rgba(255,255,255,0.6);
|
||||
padding-left: 5ch;
|
||||
padding-right: 5ch;
|
||||
padding-top: 5ch;
|
||||
padding-bottom: 5ch;
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
#my-jumbotron {
|
||||
//background-color: rgba(50,50,50,0.9);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 50px;
|
||||
padding: 2rem 1rem;
|
||||
border-radius: 1rem;
|
||||
background: #282020; /* fallback for old browsers */
|
||||
background: -webkit-linear-gradient(to right, rgba(54, 55, 57, 0.9), rgba(75, 87, 90, 0.9), rgba(40, 32, 32, 0.9)); /* Chrome 10-25, Safari 5.1-6 */
|
||||
background: linear-gradient(to right, rgba(54, 55, 57, 0.9), rgba(75, 87, 90, 0.9), rgba(40, 32, 32, 0.9)); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
|
||||
}
|
||||
|
||||
.my-title {
|
||||
color: floralwhite;
|
||||
font-size: 3em;
|
||||
}
|
||||
@@ -1,17 +1,30 @@
|
||||
{% extends "bootstrap/base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<link rel="stylesheet"
|
||||
href="{{url_for('static', filename='mystyle.css')}}">
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}Wigan Homebrew Club Competition Entry{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="jumbotron text-center">
|
||||
<h2>Wigan Homebrew Club Competition Entry</h2>
|
||||
<div class="jumbotron text-center" id="my-jumbotron">
|
||||
<h2 class="my-title">Wigan Homebrew Club Competition Entry</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2"></div>
|
||||
<div class="col-md-8">
|
||||
<p><p>You have been entered into the competition to brew a <strong>{{ brew_name }}</strong>, which will be judged at the meeting in <strong>{{ brew_month }}</strong>.</p>
|
||||
<br/>
|
||||
<p class="bg-success lead text-center" style="padding: 10px 10px 10px 10px">Please mark all of your bottlecaps with the following identifier: <strong>{{ identifier }}</strong></p>
|
||||
<div class="col-md-8 content-div">
|
||||
{% if error == None %}
|
||||
<p>You have been entered into the competition to brew a <strong>{{ brew_name }}</strong>, which will be judged at the meeting in <strong>{{ brew_month }}</strong>.</p>
|
||||
<p>To make sure your beer isn't easily identifiable, most people tend to use brown bottles of either 330ml or 500ml size. You probably don't want to put your own label on the bottle!</p>
|
||||
<br/>
|
||||
<div class="alert alert-success lead" role="alert"> <span class="glyphicon glyphicon-tags" aria-hidden="true"></span> Please mark all of your bottlecaps with the following identifier: <strong>{{ identifier }}</strong></div>
|
||||
{% else %}
|
||||
<p>Unfortunately it has not been possible to enter you into the competition at this time</p>
|
||||
<div class="alert alert-danger lead" role="alert"> {{ error }} </div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
{% extends "bootstrap/base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<link rel="stylesheet"
|
||||
href="{{url_for('static', filename='mystyle.css')}}">
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}Wigan Homebrew Club Competition Entry{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="jumbotron text-center">
|
||||
<h2>Wigan Homebrew Club Competition Entry</h2>
|
||||
<div class="jumbotron text-center" id="my-jumbotron">
|
||||
<h2 class="my-title">Wigan Homebrew Club Competition Entry</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2"></div>
|
||||
<div class="col-md-8">
|
||||
<div class="col-md-8 content-div">
|
||||
<p>The next competition brew will be a <strong>{{ brew_name }}</strong>. It will be judged at the meeting in <strong>{{ brew_month }}</strong></p>
|
||||
<br/>
|
||||
<p>If you would like to enter the competition, please enter your name below to generate a letter/number that will be used to identify your entry</p>
|
||||
|
||||
13
web/utils.py
Normal file
13
web/utils.py
Normal file
@@ -0,0 +1,13 @@
|
||||
def remove_common_elements(a, b):
|
||||
"""Removes the common elements from both supplied lists (in place), doesnt not return a new list"""
|
||||
for e in a[:]:
|
||||
if e in b:
|
||||
a.remove(e)
|
||||
b.remove(e)
|
||||
|
||||
|
||||
def get_ip(request):
|
||||
if request.headers.getlist("X-Forwarded-For"):
|
||||
return request.headers.getlist("X-Forwarded-For")[0]
|
||||
else:
|
||||
return request.remote_addr
|
||||
Reference in New Issue
Block a user