Browse Source

first commit

master
Tom Lee-Gough 1 year ago
commit
857409c68d
  1. 5476
      import/ProductList.csv
  2. 40
      import/import_products.py
  3. 23
      import/mapping
  4. 0
      readme.md
  5. 55
      stock/__init__.py
  6. 110
      stock/auth.py
  7. 128
      stock/db.py
  8. 28
      stock/scratchpad.txt
  9. 1
      stock/static/css/spectre-exp.min.css
  10. 1
      stock/static/css/spectre-icons.min.css
  11. 1
      stock/static/css/spectre.min.css
  12. 7
      stock/static/css/style.css
  13. 8
      stock/static/manifest.webmanifest
  14. 21
      stock/static/stock.js
  15. 29
      stock/templates/auth/login.html
  16. 60
      stock/templates/base.html
  17. 23
      stock/templates/forms/barcode_search.html
  18. 45
      stock/templates/forms/count_create.html
  19. 40
      stock/templates/forms/count_edit.html
  20. 24
      stock/templates/forms/delete_confirm.html
  21. 31
      stock/templates/forms/product_create.html
  22. 38
      stock/templates/forms/stock_create.html
  23. 45
      stock/templates/index.html
  24. 32
      stock/templates/product_results.html
  25. 41
      stock/templates/tables/counts.html
  26. 56
      stock/templates/tables/products.html
  27. 325
      stock/views.py
  28. 64
      tests/conftest.py
  29. 66
      tests/test_auth.py
  30. 92
      tests/test_blog.py
  31. 28
      tests/test_db.py
  32. 12
      tests/test_factory.py

5476
import/ProductList.csv

File diff suppressed because it is too large Load Diff

40
import/import_products.py

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
import csv
import mysql.connector
import os
conn = mysql.connector.connect(
host="localhost",
user="tom",
password="ZQsC644DGh",
database="stock_db"
)
curs = conn.cursor()
file = 'ProductList.csv'
with open(file) as csv_file:
csv_reader = csv.reader(csv_file)
next(csv_reader)
for row in csv_reader:
curs.execute(
'INSERT INTO products('
' prod_id,'
' prod_code,'
' prod_barcode,'
' prod_name,'
' prod_desc,'
' prod_cost'
' ) VALUES ('
' %s, %s, %s, %s, %s, %s)',
(
row[21],
row[15],
row[11],
row[1],
row[2],
row[5]
)
)
conn.commit()
conn.close()

23
import/mapping

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
-- ---------------------------------- ---------------------------------------
0 TillOrder
1 Name AbbeySlim 120mm Gas Man Blk Hlight Coal
2 Description
3 CostPrice 205.51000
4 TaxRate 20VAT
5 CostIncTax 246.61200
6 SalePriceIncTax 0.00000
7 IsVariablePrice False
8 MarginPerc 0.000000
9 TaxExemptEligible False
10 RRPrice 0
11 Barcode
12 Category Abbey
13 Brand Be Modern
14 Supplier Be Modern
15 OrderCode
16 ArticleCode
17 PopupNote
18 MultiChoiceNote
19 ButtonColour
20 SellOnTill True
21 ProductId 13478471

55
stock/__init__.py

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
import os
import datetime
from flask import Flask
import pymysql
pymysql.install_as_MySQLdb()
from stock.db import db
def create_app(test_config=None):
test_config = None
"""Create and configure an instance of the Flask application."""
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
# a default secret that should be overridden by instance config
SECRET_KEY='dev',
SQLALCHEMY_DATABASE_URI = 'mysql://tom:ZQsC644DGh@localhost/stock_db',
SQLALCHEMY_TRACK_MODIFICATIONS=False
)
if test_config is None:
# load the instance config, if it exists, when not testing
app.config.from_pyfile('config.py', silent=True)
else:
# load the test config if passed in
app.config.update(test_config)
# ensure the instance folder exists
try:
os.makedirs(app.instance_path)
except OSError:
pass
@app.route('/hello')
def hello():
return 'Hello, World!'
# register the database commands
from stock import db
db.init_app(app)
# apply the blueprints to the app
from stock import auth, views
app.register_blueprint(auth.bp)
app.register_blueprint(views.bp)
# make url_for('index') == url_for('blog.index')
# in another app, you might define a separate main index here with
# app.route, while giving the blog blueprint a url_prefix, but for
# the tutorial the blog will be the main index
app.add_url_rule('/', endpoint='index')
return app

110
stock/auth.py

@ -0,0 +1,110 @@ @@ -0,0 +1,110 @@
import functools
from flask import (
Blueprint, flash, g, redirect, render_template, request, session, url_for
)
from werkzeug.security import check_password_hash, generate_password_hash
from stock.db import get_db, User
from uuid import uuid4
bp = Blueprint('auth', __name__, url_prefix='/auth')
def login_required(view):
"""View decorator that redirects anonymous users to the login page."""
@functools.wraps(view)
def wrapped_view(**kwargs):
if g.user is None:
return redirect(url_for('auth.login'))
return view(**kwargs)
return wrapped_view
@bp.before_app_request
def load_logged_in_user():
"""If a user id is stored in the session, load the user object from
the database into ``g.user``."""
user_id = session.get('user_id')
if user_id is None:
g.user = None
else:
g.user = User.query.filter_by(user_id=user_id)
@bp.route('/login', methods=('GET', 'POST'))
def login():
"""Log in a registered user by adding the user id to the session."""
if request.method == 'POST':
username = request.form['user_name']
password = request.form['user_pass']
db = get_db()
error = None
user = User.query.filter_by(user_name = username).first()
if user is None:
error = 'Incorrect username.'
elif not check_password_hash(user.user_pass, password):
error = 'Incorrect password.'
if error is None:
# store the user id in a new session and return to the index
session.clear()
session['user_id'] = user.user_id
return redirect(url_for('index'))
flash(error)
return render_template('auth/login.html')
@bp.route('/logout')
def logout():
"""Clear the current session, including the stored user id."""
session.clear()
return redirect(url_for('index'))
@bp.route('/change', methods=['GET', 'POST'])
def change_pass():
db = get_db()
if request.method == 'POST':
user = db.execute(
'SELECT * FROM user WHERE user_id = ?', (session['user_id'],)
).fetchone()
username = user['user_name']
password = user['user_pass']
old_pass = request.form['old_password']
new_pass = request.form['new_password']
conf_pass = request.form['confirm_password']
error = None
if new_pass != conf_pass:
error = 'Passwords do not match'
elif not check_password_hash(password, old_pass):
error = 'Incorrect password'
if error is None:
db.execute(
'UPDATE user'
' SET user_pass = ?'
' WHERE user_name = ?',
(generate_password_hash(new_pass), username,)
)
db.commit()
flash('Password changed successfully')
return redirect(url_for('main.index'))
else:
flash(error)
return render_template('auth/change.html')

128
stock/db.py

@ -0,0 +1,128 @@ @@ -0,0 +1,128 @@
import sqlite3
from werkzeug.security import check_password_hash, generate_password_hash
import click
from flask import current_app, g
from flask.cli import with_appcontext
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
import uuid
class User(db.Model):
user_id = db.Column(
db.String(36),
primary_key = True
)
user_name = db.Column(
db.String(80)
)
user_pass = db.Column(
db.String(1000)
)
class Products(db.Model):
prod_id = db.Column(
db.Integer,
primary_key=True
)
prod_code = db.Column(
db.String(100)
)
prod_barcode = db.Column(
db.String(100)
)
prod_name = db.Column(
db.String(250)
)
prod_desc = db.Column(
db.String(250)
)
prod_cost = db.Column(
db.Float()
)
count = db.relationship(
'Count'
)
class Stock(db.Model):
stock_id = db.Column(
db.String(36),
primary_key=True
)
stock_date = db.Column(
db.DateTime
)
stock_user = db.Column(
db.String(100)
)
site_id_fk = db.Column(
db.String(100),
db.ForeignKey('sites.site_id')
)
count = db.relationship(
'Count',
)
class Count(db.Model):
count_id = db.Column(
db.String(36),
primary_key = True
)
count_count = db.Column(
db.Integer
)
prod_id_fk = db.Column(
db.Integer,
db.ForeignKey('products.prod_id')
)
stock_id_fk = db.Column(
db.String(36),
db.ForeignKey('stock.stock_id')
)
class Sites(db.Model):
site_id = db.Column(
db.String(36),
primary_key = True
)
site_name = db.Column(
db.String(100)
)
stock = db.relationship(
'Stock',
)
def get_db():
if 'db' not in g:
g.db = db
return g.db
def close_db(e=None):
db = d.pop('db', None)
def init_db():
with current_app:
db.create_all()
db.session.commit()
@click.command('init-db')
@with_appcontext
def init_db_command():
init_db()
click.echo('Initialized the database')
def init_app(app):
app.teardown_appcontext(close_db)
app.cli.add_comment(init_db_command)

28
stock/scratchpad.txt

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
User
user_id pk
user_name
user_pass
Products (from EPOS)
prod_id
prod_code
prod_barcode
prod_name
prod_desc
prod_cost
Stock
stock_id pk
stock_date
stock_user
site_id_fk
Count
count_id pk
count_count
prod_id_fk
stock_id_fk
Sites
site_id. pk
site_name

1
stock/static/css/spectre-exp.min.css vendored

File diff suppressed because one or more lines are too long

1
stock/static/css/spectre-icons.min.css vendored

File diff suppressed because one or more lines are too long

1
stock/static/css/spectre.min.css vendored

File diff suppressed because one or more lines are too long

7
stock/static/css/style.css

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
/**{
border:solid 1px lime;
}*/
html {
font-family: "Open Sans";
}

8
stock/static/manifest.webmanifest

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
{
"background_color": "purple",
"description": "Henderson Design Group Stock",
"display": "fullscreen",
"name": "Stock Counter",
"short_name": "Stock Counter",
"start_url": "/"
}

21
stock/static/stock.js

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
function myFunction() {
// Declare variables
var input, filter, table, tr, td, i, txtValue;
input = document.getElementById("filter");
filter = input.value.toUpperCase();
table = document.getElementById("productTable");
tr = table.getElementsByTagName("tr");
// Loop through all table rows, and hide those who don't match the search query
for (i = 0; i < tr.length; i++) {
td = tr[i].getElementsByTagName("td")[0];
if (td) {
txtValue = td.textContent || td.innerText;
if (txtValue.toUpperCase().indexOf(filter) > -1) {
tr[i].style.display = "";
} else {
tr[i].style.display = "none";
}
}
}
}

29
stock/templates/auth/login.html

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
{% extends 'base.html' %}
{% block header %}
<h2 class="text-center">Henderson Design Group Stock Counter</h2>
{% endblock %}
{% block content %}
<form method="post" class="">
<div class="form-group">
<label class="form-label" for="user_name">Username</label>
<input id="user_name" name="user_name" class="form-input" placeholder="username" required>
</div>
<div class="form-group">
<label class="form-label" for="user_pass">Password</label>
<input type="user_pass" id="user_pass" name="user_pass" class="form-input" placeholder="password" required>
</div>
<button type="submit" class="btn btn-block btn-primary">Login</button>
{% if error != '' %}
<p><strong>{{ error }}</strong></p>
{% endif %}
</form>
{% endblock %}

60
stock/templates/base.html

@ -0,0 +1,60 @@ @@ -0,0 +1,60 @@
<!doctype html>
<html land="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Henderson Design | Stock</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/spectre-exp.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/spectre-icons.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/spectre.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="manifest" href="{{ url_for('static',filename='manifest.webmanifest') }}">
</head>
<body>
<div class="container">
<header class=navbar>
<section class="navbar-section">
<a class="navbar-brand" href="{{url_for('views.index')}}">Stock Counter</a>
</section>
{% if g.user != None %}
<section class="navbar-section">
<a href="{{url_for('views.index')}}" class="btn btn-link">Stock</a>
<a href="{{ url_for('views.products') }}" class="btn btn-link">Products</a>
</section>
{% endif %}
</header>
<div class="container">
<div>
{% block header %}{% endblock %}
</div>
<div>
{% block toolbar %}{% endblock %}
</div>
<div>
{% block content %}{% endblock %}
</div>
<div>
{% for message in get_flashed_messages() %}
<div>{{ message }}</div>
{% endfor %}
</div>
</div>
</div>
</body>
<script src="{{url_for('static', filename='stock.js') }}"></script>
</html>

23
stock/templates/forms/barcode_search.html

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
{% extends 'base.html' %}
{% block header %}
<h1 class="text-center">Product Search</h1>
{% endblock %}
{% block content %}
<form method="post" class="">
<div class="form-group">
<label class="form-label" for="prod_barcode">Barcode</label>
<input id="prod_barcode" name="prod_barcode" class="form-input" type="text">
</div>
<button type="submit" class="btn btn-block btn-primary">Search</button>
{% if error != '' %}
<p><strong>{{ error }}</strong></p>
{% endif %}
</form>
{% endblock %}

45
stock/templates/forms/count_create.html

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
{% extends 'base.html' %}
{% block header %}
<h1 class="text-center">Add Product Count</h1>
{% endblock %}
{% block content %}
<div class="columns">
<span class="column">Barcode</span>
<span class="column">{{product.prod_barcode}}</span>
</div>
<div class="columns">
<span class="column">Product Code</span>
<span class="column">{{product.prod_code}}</span>
</div>
<div class="columns">
<span class="column">Name</span>
<span class="column">{{product.prod_name}}</span>
</div>
<form method="post" class="">
<!-- <div class="form-group">
<label class="form-label" for="prod_barcode">Barcode</label>
<input id="prod_barcode" name="prod_barcode" class="form-input" type="text" required>
</div> -->
<div class="form-group">
<label class="form-label" for="prod_count">Count</label>
<input id="prod_count" name="prod_count" type="number" min=0 step=1 class="form-input">
</div>
<button type="submit" class="btn btn-block btn-primary">Create Product Count</button>
{% if error != '' %}
<p><strong>{{ error }}</strong></p>
{% endif %}
</form>
{% endblock %}

40
stock/templates/forms/count_edit.html

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
{% extends 'base.html' %}
{% block header %}
<h1 class="text-center">Edit Product Count</h1>
{% endblock %}
{% block content %}
<div class="columns">
<span class="column">Barcode</span>
<span class="column">{{count.prod_barcode}}</span>
</div>
<div class="columns">
<span class="column">Product Code</span>
<span class="column">{{count.prod_code}}</span>
</div>
<div class="columns">
<span class="column">Name</span>
<span class="column">{{count.prod_name}}</span>
</div>
<form method="post" class="">
<div class="form-group">
<label class="form-label" for="prod_count">Count</label>
<input id="prod_count" name="prod_count" type="number" min=0 step=1 class="form-input" value="{{ count.count_count }}">
</div>
<button type="submit" class="btn btn-block btn-primary">Update Product Count</button>
{% if error != '' %}
<p><strong>{{ error }}</strong></p>
{% endif %}
</form>
{% endblock %}

24
stock/templates/forms/delete_confirm.html

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% block header %}
<h1 class="text-center">Confirm Deletion</h1>
{% endblock %}
{% block content %}
<p>Are you sure you want to delete the following count:</p>
<form method="post" class="">
<button name="submit" type="submit" class="btn btn-block" value="delete">Delete</button>
<br>
<button name="submit" type="submit" class="btn btn-primary btn-block" value="no-delete">Don't Delete</button>
{% if error != '' %}
<p><strong>{{ error }}</strong></p>
{% endif %}
</form>
{% endblock %}

31
stock/templates/forms/product_create.html

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
{% extends 'base.html' %}
{% block header %}
<h1 class="text-center">Create Product</h1>
{% endblock %}
{% block content %}
<p>The product you scanned was not found. Please add it.</p>
<form method="post" class="">
<div class="form-group">
<label class="form-label" for="prod_code">Product Code</label>
<input id="prod_code" name="prod_code" class="form-input" type="text">
</div>
<div class="form-group">
<label class="form-label" for="prod_name">Product Name</label>
<input id="prod_name" name="prod_name" class="form-input" type="text">
</div>
<button type="submit" class="btn btn-block btn-primary">Create New Product</button>
{% if error != '' %}
<p><strong>{{ error }}</strong></p>
{% endif %}
</form>
{% endblock %}

38
stock/templates/forms/stock_create.html

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
{% extends 'base.html' %}
{% block header %}
<h1 class="text-center">Create Stock Count</h1>
{% endblock %}
{% block content %}
<form method="post" class="">
<div class="form-group">
<label class="form-label" for="stock_date">Count Date</label>
<input id="stock_date" name="stock_date" class="form-input" type="date" required>
</div>
<div class="form-group">
<label class="form-label" for="stock_user">User</label>
<input id="stock_user" name="stock_user" class="form-input">
</div>
<div class="form-group">
<label class="form-label" for="stock_site">Site</label>
<select class="form-select" name="stock_site">
<option>---</option>
{% for site in sites %}
<option value="{{site.site_id}}">{{site.site_name}}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-block btn-primary">Create Stock Count</button>
{% if error != '' %}
<p><strong>{{ error }}</strong></p>
{% endif %}
</form>
{% endblock %}

45
stock/templates/index.html

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
{% extends 'base.html' %}
{% block header %}
<h1>Stock Counts</h1>
{% endblock %}
{% block toolbar %}
<a class="btn btn-primary" href="{{url_for('views.stock_create')}}">Create Stock Count</a>
{% endblock %}
{% block content %}
<table class="table">
<thead>
<tr>
<th>Stock Date</th>
<th>Staff</th>
<th>Site</th>
<th></th>
</tr>
</thead>
<tbody>
{% for count in stock_counts %}
<tr>
<td>{{count.stock_date.strftime('%Y-%m-%d')}}</td>
<td>{{count.stock_user}}</td>
<td>{{count.site_name}}</td>
<td>
<a href="{{url_for('views.stock_view', stock_id=count.stock_id)}}">
<i class="icon icon-edit"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

32
stock/templates/product_results.html

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
{% extends 'base.html' %}
{% block header %}
<h2 class="text-center">Product Search</h2>
{% endblock %}
{% block content %}
{{results.prod_id}}
{{results.prod_code}}
{{results.prod_name}}
{{results.prod_barcode}}
<!-- <form method="post" class="">
<div class="form-group">
<label class="form-label" for="prod_barcode">Barcode</label>
<input id="prod_barcode" name="prod_barcode" class="form-input" type="text">
</div>
<button type="submit" class="btn btn-block btn-primary">Search</button>
{% if error != '' %}
<p><strong>{{ error }}</strong></p>
{% endif %}
</form> -->
{% endblock %}

41
stock/templates/tables/counts.html

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
{% extends 'base.html' %}
{% block toolbar %}
<a href="{{url_for( 'views.product_search')}}" class="btn btn-primary">Add Product</a>
{% endblock %}
{% block header%}
<h1> Stock Listing</h1>
{% endblock %}
{% block content %}
{% for count in counts %}
<div class="tile mt-2 bg-secondary p-2">
<div class="tile-content">
<div class="tile-title">{{count.prod_name}}</div>
<div class="columns">
<span class="column col-8">{{count.prod_barcode}}</span>
<span class="column col-4">Qty: {{count.count_count}}</span>
</div>
</div>
<div class="tile-action">
<a class="btn" href="{{url_for('views.count_edit', count_id=count.count_id)}}">
<i class="icon icon-edit"></i>
</a>
<a class="btn" href="{{url_for('views.count_delete', count_id=count.count_id)}}">
<i class="icon icon-cross"></i>
</a>
</div>
</div>
{% endfor %}
{% endblock %}

56
stock/templates/tables/products.html

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
{% extends 'base.html' %}
{% block header %}
<h1>EPOS Products</h1>
{% endblock %}
{% block toolbar %}
<form method="post">
<div class="form-group">
<input type="text" id="search" name="search" class="form-input" placeholder="Search" required>
</div>
<div class="form-group">
<button type="submit" class="btn btn-block btn-primary">Search</button>
</div>
<a class="btn btn-block" href="{{ url_for('views.products') }}">Clear</a>
</form>
{% endblock %}
{% block content %}
{% for prod in products %}
<details class="mt-2 bg-secondary p-1" id="{{prod.prod_name}}">
<summary class="">{{prod.prod_name}}</summary>
<div class="p-2">
<div class="columns">
<span class="column"><strong>Product Code:</strong></span>
<span class="column text-right">{{prod.prod_code}}</span>
</div>
<div class="columns">
<span class="column"><strong>Barcode:</strong></span>
<span class="column text-right">{{prod.prod_barcode}}</span>
</div>
<div class="columns">
<span class="column"><strong>Price:</strong></span>
<span class="column text-right">ยฃ {{prod.prod_cost}}</span>
</div>
<div class="columns">
<span class="column"><strong>Description:</strong></span>
<span class="column text-right">{{prod.prod_desc}}</span>
</div>
<div class="columns">
<span class="column"><strong>EPOS ID:</strong></span>
<span class="column text-right">{{prod.prod_id}}</span>
</div>
</div>
</details>
{% endfor %}
</tbody>
{% endblock %}

325
stock/views.py

@ -0,0 +1,325 @@ @@ -0,0 +1,325 @@
from flask import (
Blueprint, flash, g, redirect, render_template, request, url_for, session
)
from stock.auth import login_required
from stock.db import (
get_db,
db,
Products,
Stock,
Count,
Sites
)
import datetime
from uuid import uuid4
bp = Blueprint('views', __name__)
@bp.route('/')
@login_required
def index():
db = get_db()
stock_counts = Stock.query\
.join(Sites)\
.add_columns(
Stock.stock_id,
Stock.stock_date,
Stock.stock_user,
Sites.site_name
)\
.all()
return render_template(
'index.html',
stock_counts=stock_counts
)
@bp.route(
'/products',
methods=['GET', 'POST']
)
@login_required
def products():
if request.method == 'POST':
products = Products.query.filter(
Products.prod_name.like('%' + request.form['search'] + '%') |
Products.prod_code.like('%' + request.form['search'] + '%')
)
else:
products = Products.query.all()
return render_template(
'tables/products.html',
products=products
)
@bp.route(
'/stock/counts',
)
@login_required
def stock_counts():
stock_counts = Stock.query.all()
return render_template(
'tables/stock_counts.html',
stock_counts= stock_counts
)
@bp.route(
'/stock/create',
methods=['GET', 'POST']
)
@login_required
def stock_create():
sites = Sites.query.all()
if request.method == 'POST':
stock = Stock(
stock_id = str(uuid4()),
stock_date = request.form['stock_date'],
stock_user = request.form['stock_user'],
site_id_fk = request.form['stock_site']
)
db.session.add(stock)
db.session.commit()
return redirect(
url_for('views.index')
)
return render_template(
'forms/stock_create.html',
sites= sites
)
@bp.route(
'/stock/view/<stock_id>'
)
@login_required
def stock_view(stock_id):
session['stock_id'] = stock_id
product_counts = Stock.query\
.join(Count, Count.stock_id_fk==Stock.stock_id)\
.join(Products, Products.prod_id==Count.prod_id_fk)\
.add_columns(
Stock.stock_id,
Count.count_id,
Count.count_count,
Products.prod_barcode,
Products.prod_name
)\
.filter(Stock.stock_id==stock_id).all()
return render_template(
'tables/counts.html',
counts= product_counts,
stock_id = stock_id
)
@bp.route(
'/count/create/<stock_id>/<barcode>',
methods=['POST', 'GET']
)
@login_required
def count_create(stock_id, barcode):
prod_search = Products.query.filter_by(
prod_barcode=barcode
).first()
if request.method == 'POST':
new_count = Count(
count_id=str(uuid4()),
count_count=request.form['prod_count'],
prod_id_fk=prod_search.prod_id,
stock_id_fk=session['stock_id']
)
db.session.add(new_count)
db.session.commit()
return redirect(
url_for(
'views.stock_view',
stock_id=stock_id
)
)
return render_template(
'forms/count_create.html',
product= prod_search
)
@bp.route(
'/products/search',
methods=['POST', 'GET']
)
@login_required
def product_search():
if request.method=='POST':
prod_search = Products.query.filter_by(
prod_barcode=request.form['prod_barcode']
).first()
if prod_search == None:
return redirect(
url_for(
'views.product_add',
barcode = request.form['prod_barcode']
)
)
else:
return redirect(
url_for(
'views.count_create',
stock_id=session['stock_id'],
barcode=prod_search.prod_barcode
)
)
return render_template(
'forms/barcode_search.html'
)
@bp.route(
'/products/add/<barcode>',
methods=['POST', 'GET']
)
def product_add(barcode):
if request.method=='POST':
new_product = Products(
prod_code = request.form['prod_code'],
prod_barcode = barcode,
prod_name = request.form['prod_name'],
prod_desc = 'New Product'
)
db.session.add(new_product)
db.session.commit()
return redirect(
url_for(
'views.count_create',
stock_id=session['stock_id'],
barcode=barcode
)
)
return render_template(
'forms/product_create.html'
)
@bp.route(
'/count/delete/<count_id>',
methods=['POST', 'GET']
)
@login_required
def count_delete(count_id):
count = Stock.query\
.join(Count, Count.stock_id_fk==Stock.stock_id)\
.join(Products, Products.prod_id==Count.prod_id_fk)\
.add_columns(
Stock.stock_id,
Count.count_id,
Count.count_count,
Products.prod_barcode,
Products.prod_name
)\
.filter(Count.count_id==count_id).first()
if request.method == 'POST':
if request.form['submit'] == 'delete':
count = Count.query.filter_by(count_id=count_id).first()
db.session.delete(count)
db.session.commit()
return redirect(
url_for(
'views.stock_view',
stock_id=session['stock_id']
)
)
return render_template(
'forms/delete_confirm.html'
)
@bp.route(
'/count/edit/<count_id>',
methods=['POST', 'GET']
)
@login_required
def count_edit(count_id):
count = Count.query\
.join(Products, Products.prod_id==Count.prod_id_fk)\
.add_columns(
Products.prod_barcode,
Products.prod_code,
Products.prod_name,
Count.count_count
)\
.filter(Count.count_id==count_id).first()
if request.method=='POST':
updated_count = Count.query\
.filter_by(count_id=count_id)\
.update(dict(count_count=request.form['prod_count']))
db.session.commit()
return redirect(
url_for(
'views.stock_view',
stock_id=session['stock_id']
)
)
return render_template(
'forms/count_edit.html',
count=count
)

64
tests/conftest.py

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
import os
import tempfile
import pytest
from flaskr import create_app
from flaskr.db import get_db, init_db
# read in SQL for populating test data
with open(os.path.join(os.path.dirname(__file__), 'data.sql'), 'rb') as f:
_data_sql = f.read().decode('utf8')
@pytest.fixture
def app():
"""Create and configure a new app instance for each test."""
# create a temporary file to isolate the database for each test
db_fd, db_path = tempfile.mkstemp()
# create the app with common test config
app = create_app({
'TESTING': True,
'DATABASE': db_path,
})
# create the database and load test data
with app.app_context():
init_db()
get_db().executescript(_data_sql)
yield app
# close and remove the temporary database
os.close(db_fd)
os.unlink(db_path)
@pytest.fixture
def client(app):
"""A test client for the app."""
return app.test_client()
@pytest.fixture
def runner(app):
"""A test runner for the app's Click commands."""
return app.test_cli_runner()
class AuthActions(object):
def __init__(self, client):
self._client = client
def login(self, username='test', password='test'):
return self._client.post(
'/auth/login',
data={'username': username, 'password': password}
)
def logout(self):
return self._client.get('/auth/logout')
@pytest.fixture
def auth(client):
return AuthActions(client)

66
tests/test_auth.py

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
import pytest
from flask import g, session
from flaskr.db import get_db
def test_register(client, app):
# test that viewing the page renders without template errors
assert client.get('/auth/register').status_code == 200
# test that successful registration redirects to the login page
response = client.post(
'/auth/register', data={'username': 'a', 'password': 'a'}
)
assert 'http://localhost/auth/login' == response.headers['Location']
# test that the user was inserted into the database
with app.app_context():
assert get_db().execute(
"select * from user where username = 'a'",
).fetchone() is not None
@pytest.mark.parametrize(('username', 'password', 'message'), (
('', '', b'Username is required.'),
('a', '', b'Password is required.'),
('test', 'test', b'already registered'),
))
def test_register_validate_input(client, username, password, message):
response = client.post(
'/auth/register',
data={'username': username, 'password': password}
)
assert message in response.data
def test_login(client, auth):
# test that viewing the page renders without template errors
assert client.get('/auth/login').status_code == 200
# test that successful login redirects to the index page
response = auth.login()
assert response.headers['Location'] == 'http://localhost/'
# login request set the user_id in the session
# check that the user is loaded from the session
with client:
client.get('/')
assert session['user_id'] == 1
assert g.user['username'] == 'test'
@pytest.mark.parametrize(('username', 'password', 'message'), (
('a', 'test', b'Incorrect username.'),
('test', 'a', b'Incorrect password.'),
))
def test_login_validate_input(auth, username, password, message):
response = auth.login(username, password)
assert message in response.data
def test_logout(client, auth):
auth.login()
with client:
auth.logout()
assert 'user_id' not in session

92
tests/test_blog.py

@ -0,0 +1,92 @@ @@ -0,0 +1,92 @@
import pytest
from flaskr.db import get_db
def test_index(client, auth):
response = client.get('/')
assert b"Log In" in response.data
assert b"Register" in response.data
auth.login()
response = client.get('/')
assert b'test title' in response.data
assert b'by test on 2018-01-01' in response.data
assert b'test\nbody' in response.data
assert b'href="/1/update"' in response.data
@pytest.mark.parametrize('path', (
'/create',
'/1/update',
'/1/delete',
))
def test_login_required(client, path):
response = client.post(path)
assert response.headers['Location'] == 'http://localhost/auth/login'
def test_author_required(app, client, auth):
# change the post author to another user
with app.app_context():
db = get_db()
db.execute('UPDATE post SET author_id = 2 WHERE id = 1')
db.commit()
auth.login()
# current user can't modify other user's post
assert client.post('/1/update').status_code == 403
assert client.post('/1/delete').status_code == 403
# current user doesn't see edit link
assert b'href="/1/update"' not in client.get('/').data
@pytest.mark.parametrize('path', (
'/2/update',
'/2/delete',
))
def test_exists_required(client, auth, path):
auth.login()
assert client.post(path).status_code == 404
def test_create(client, auth, app):
auth.login()
assert client.get('/create').status_code == 200
client.post('/create', data={'title': 'created', 'body': ''})
with app.app_context():
db = get_db()
count = db.execute('SELECT COUNT(id) FROM post').fetchone()[0]
assert count == 2
def test_update(client, auth, app):
auth.login()
assert client.get('/1/update').status_code == 200
client.post('/1/update', data={'title': 'updated', 'body': ''})
with app.app_context():
db = get_db()
post = db.execute('SELECT * FROM post WHERE id = 1').fetchone()
assert post['title'] == 'updated'
@pytest.mark.parametrize('path', (
'/create',
'/1/update',
))
def test_create_update_validate(client, auth, path):
auth.login()
response = client.post(path, data={'title': '', 'body': ''})
assert b'Title is required.' in response.data
def test_delete(client, auth, app):
auth.login()
response = client.post('/1/delete')
assert response.headers['Location'] == 'http://localhost/'
with app.app_context():
db = get_db()
post = db.execute('SELECT * FROM post WHERE id = 1').fetchone()
assert post is None

28
tests/test_db.py

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
import sqlite3
import pytest
from flaskr.db import get_db
def test_get_close_db(app):
with app.app_context():
db = get_db()
assert db is get_db()
with pytest.raises(sqlite3.ProgrammingError) as e:
db.execute('SELECT 1')
assert 'closed' in str(e)
def test_init_db_command(runner, monkeypatch):
class Recorder(object):
called = False
def fake_init_db():
Recorder.called = True
monkeypatch.setattr('flaskr.db.init_db', fake_init_db)
result = runner.invoke(args=['init-db'])
assert 'Initialized' in result.output