Initial commit
This commit is contained in:
commit
7825abf11c
3
config.py
Normal file
3
config.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
DEBUG = False
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
SQLALCHEMY_DATABASE_URI = 'sqlite:///../discworld.db'
|
BIN
discworld.db
Normal file
BIN
discworld.db
Normal file
Binary file not shown.
9
discworld/__init__.py
Normal file
9
discworld/__init__.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from flask import Flask
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_object('config')
|
||||||
|
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
from . import views
|
95
discworld/models.py
Normal file
95
discworld/models.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
db,
|
||||||
|
shop
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ShopProduct(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key = True)
|
||||||
|
name = db.Column(db.String(100), unique=True, nullable=False)
|
||||||
|
total_listed = db.Column(db.Integer)
|
||||||
|
total_sold = db.Column(db.Integer)
|
||||||
|
entries = db.relationship('ShopEntry', backref='product', lazy=True,
|
||||||
|
order_by="desc(ShopEntry.date)")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<ShopProduct "{}">'.format(self.name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_entry(self):
|
||||||
|
return ShopEntry.query.filter_by(
|
||||||
|
product_id=self.id).order_by(ShopEntry.date.desc()).first()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_stocked(self):
|
||||||
|
entries = ShopEntry.query.filter_by(product_id=self.id).order_by(
|
||||||
|
ShopEntry.date.asc())
|
||||||
|
|
||||||
|
last_stock = 0
|
||||||
|
stocked_count = 0
|
||||||
|
for entry in entries:
|
||||||
|
diff = last_stock - entry.stock
|
||||||
|
if diff < 0:
|
||||||
|
stocked_count += abs(diff)
|
||||||
|
last_stock = entry.stock
|
||||||
|
return stocked_count
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_sold(self):
|
||||||
|
entries = ShopEntry.query.filter_by(product_id=self.id).order_by(
|
||||||
|
ShopEntry.date.asc())
|
||||||
|
|
||||||
|
last_stock = 0
|
||||||
|
sold_count = 0
|
||||||
|
for entry in entries:
|
||||||
|
diff = last_stock - entry.stock
|
||||||
|
if diff > 0:
|
||||||
|
sold_count += diff
|
||||||
|
last_stock = entry.stock
|
||||||
|
return sold_count
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_earned(self):
|
||||||
|
entries = ShopEntry.query.filter_by(product_id=self.id).order_by(
|
||||||
|
ShopEntry.date.asc())
|
||||||
|
|
||||||
|
last_stock = 0
|
||||||
|
earned_count = 0.00
|
||||||
|
for entry in entries:
|
||||||
|
diff = last_stock - entry.stock
|
||||||
|
if diff > 0:
|
||||||
|
earned_count += (entry.price * diff)
|
||||||
|
last_stock = entry.stock
|
||||||
|
return earned_count
|
||||||
|
|
||||||
|
|
||||||
|
class ShopEntry(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
stock = db.Column(db.Integer, unique=False, nullable=False)
|
||||||
|
raw_price = db.Column(db.String(20), unique=False, nullable=False)
|
||||||
|
raw_stock = db.Column(db.String(2), unique=False, nullable=False)
|
||||||
|
date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||||
|
product_id = db.Column(db.Integer, db.ForeignKey('shop_product.id'),
|
||||||
|
nullable=False)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<ShopEntry "{}" {}>'.format(
|
||||||
|
self.product.name,
|
||||||
|
self.id
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stock(self):
|
||||||
|
stock_map = {
|
||||||
|
'zero': 0, 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5,
|
||||||
|
'six': 6, 'seven': 7, 'eight': 8, 'nine': 9, 'ten': 10,
|
||||||
|
'eleven': 11, 'twelve': 12, 'thirteen': 13, 'fourteen': 14
|
||||||
|
}
|
||||||
|
return stock_map[self.raw_stock]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def price(self):
|
||||||
|
brass = shop.convert_lancre_to_brass(self.raw_price)
|
||||||
|
return shop.convert_brass_to_am(brass)
|
97
discworld/shop.py
Normal file
97
discworld/shop.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
db,
|
||||||
|
models
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_lancre_to_brass(raw_price):
|
||||||
|
coins = {
|
||||||
|
'penny': 0,
|
||||||
|
'shilling': 0,
|
||||||
|
'crown': 0,
|
||||||
|
'sovereign': 0,
|
||||||
|
'hedgehog': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
def noz(num):
|
||||||
|
if num and num is not '-':
|
||||||
|
return int(num)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Figure out which special notation we have
|
||||||
|
if "LC" in raw_price:
|
||||||
|
pattern = r'^LC (\d+)\|(\d+|-)\|(\d+|-)$'
|
||||||
|
match = re.match(pattern, raw_price)
|
||||||
|
groups = match.groups()
|
||||||
|
|
||||||
|
coins['penny'] = noz(groups[2])
|
||||||
|
coins['shilling'] = noz(groups[1])
|
||||||
|
coins['crown'] = noz(groups[0])
|
||||||
|
elif "LSov" in raw_price:
|
||||||
|
pattern = r'^LSov (\d+)\|(\d+|-)\|(\d+|-)\|(\d+|-)$'
|
||||||
|
match = re.match(pattern, raw_price)
|
||||||
|
groups = match.groups()
|
||||||
|
|
||||||
|
coins['penny'] = noz(groups[3])
|
||||||
|
coins['shilling'] = noz(groups[2])
|
||||||
|
coins['crown'] = noz(groups[1])
|
||||||
|
coins['sovereign'] = noz(groups[0])
|
||||||
|
elif "LH" in raw_price:
|
||||||
|
pattern = r'^LH (\d+)\|(\d+|-)\|(\d+|-)\|(\d+|-)\|(\d+|-)$'
|
||||||
|
match = re.match(pattern, raw_price)
|
||||||
|
groups = match.groups()
|
||||||
|
|
||||||
|
coins['penny'] = noz(groups[4])
|
||||||
|
coins['shilling'] = noz(groups[3])
|
||||||
|
coins['crown'] = noz(groups[2])
|
||||||
|
coins['sovereign'] = noz(groups[1])
|
||||||
|
coins['hedgehog'] = noz(groups[0])
|
||||||
|
|
||||||
|
# Convert to brass
|
||||||
|
brass_coins = (
|
||||||
|
(coins['hedgehog'] * 248832) +
|
||||||
|
(coins['sovereign'] * 20736) +
|
||||||
|
(coins['crown'] * 1728) +
|
||||||
|
(coins['shilling'] * 144) +
|
||||||
|
(coins['penny'] * 12)
|
||||||
|
)
|
||||||
|
return brass_coins
|
||||||
|
|
||||||
|
def convert_brass_to_am(brass_price):
|
||||||
|
# 12 brass coins to am pennies
|
||||||
|
pennies = brass_price / 4
|
||||||
|
return (pennies/100)
|
||||||
|
|
||||||
|
def parse_shop_output(data):
|
||||||
|
pattern = r'^\s{3}?\w{2}\)\sAn*\s([\w\s-]+) for (L\w{1,3} [\d\-|]+);\s(\w+)\sleft\.$'
|
||||||
|
matches = [m.groups() for m in re.finditer(pattern, data, re.MULTILINE)]
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Iterate over each product line in the data
|
||||||
|
seen_products = []
|
||||||
|
for m in matches:
|
||||||
|
product = models.ShopProduct.query.filter_by(name=m[0]).first()
|
||||||
|
if not product:
|
||||||
|
# If we didn't find a product, create it
|
||||||
|
product = models.ShopProduct(name=m[0])
|
||||||
|
db.session.add(product)
|
||||||
|
# Add a ShopEntry for this row only if stock has changed
|
||||||
|
if not product.latest_entry or product.latest_entry.raw_stock != m[2]:
|
||||||
|
entry = models.ShopEntry(raw_price=m[1], raw_stock=m[2], product=product)
|
||||||
|
db.session.add(entry)
|
||||||
|
seen_products.append(product)
|
||||||
|
|
||||||
|
# Check all products against seen, record sellouts
|
||||||
|
products = models.ShopProduct.query.all()
|
||||||
|
for product in products:
|
||||||
|
if product not in seen_products and product.latest_entry.stock != 0:
|
||||||
|
entry = models.ShopEntry(
|
||||||
|
raw_price=product.latest_entry.raw_price,
|
||||||
|
raw_stock='zero', product=product
|
||||||
|
)
|
||||||
|
db.session.add(entry)
|
||||||
|
db.session.commit()
|
BIN
discworld/static/favicon.ico
Normal file
BIN
discworld/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 785 B |
76
discworld/templates/layout.html
Normal file
76
discworld/templates/layout.html
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Ramtops Remedies and Reagents</title>
|
||||||
|
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<!-- UIkit CSS -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.35/css/uikit.min.css" />
|
||||||
|
|
||||||
|
<!-- UIkit JS -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.35/js/uikit.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.35/js/uikit-icons.min.js"></script>
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="uk-container uk-container-large">
|
||||||
|
<!-- Title -->
|
||||||
|
<h3 class="uk-text-center uk-heading-line uk-margin-small-top">
|
||||||
|
<a href="/" class="">Ramtops Remedies and Reagents</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Top Bar -->
|
||||||
|
<div class="" uk-grid>
|
||||||
|
<div class="uk-width-1-3"> {% block left_top_bar %} {% endblock %} </div>
|
||||||
|
<div class="uk-width-1-3 uk-text-center"> {% block center_top_bar %} {% endblock %} </div>
|
||||||
|
<div class="uk-width-1-3">
|
||||||
|
<a href="/shop/data" class="uk-button uk-button-primary uk-button-small uk-align-right uk-border-rounded">
|
||||||
|
Report Stock
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page Content -->
|
||||||
|
{% block content %} {% endblock %}
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="uk-text-center uk-margin-large-top uk-margin-small-bottom">
|
||||||
|
© 2017 by <a href="http://binaryatrocity.name">Ruhsbaar</a>
|
||||||
|
|
|
||||||
|
Created for Ramtops Remedies and Reagents
|
||||||
|
|
|
||||||
|
<a href="http://discworld.starturtle.net">DiscworldMUD</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
{% block pagescripts %} {% endblock %}
|
||||||
|
<script>
|
||||||
|
function change_date_strings() {
|
||||||
|
var times = document.querySelectorAll('table > tbody > tr > td:last-child');
|
||||||
|
for(var time of times) {
|
||||||
|
var local = new Date(time.textContent + 'Z');
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
weekday: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
timeZoneName: 'short'
|
||||||
|
};
|
||||||
|
time.innerText = local.toLocaleString('en-US', options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On Page Ready */
|
||||||
|
document.addEventListener("DOMContentLoaded", function(event) {
|
||||||
|
change_date_strings();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</html>
|
205
discworld/templates/shop_dashboard.html
Normal file
205
discworld/templates/shop_dashboard.html
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block left_top_bar %}
|
||||||
|
<button class="uk-button uk-button-default uk-button-small uk-border-rounded"
|
||||||
|
uk-toggle="target: #charts-container">Show/Hide Charts</button>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block center_top_bar %}
|
||||||
|
Total Funds Earned:
|
||||||
|
<span id="total_funds_earned" class="uk-text-primary"></span>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="charts-container" uk-grid hidden>
|
||||||
|
<div class="uk-width-1-2">
|
||||||
|
<canvas id="sales-chart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="uk-width-1-2">
|
||||||
|
<canvas id="stock-chart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table id="products" class="uk-table uk-table-small uk-table-striped uk-table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="uk-table-expand">Product Name</th>
|
||||||
|
<th>Current Stock</th>
|
||||||
|
<th>Total Stock</th>
|
||||||
|
<th>Total Sold</th>
|
||||||
|
<th>Total Earned</th>
|
||||||
|
<th>Latest Price</th>
|
||||||
|
<th>Last Update</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for product in products %}
|
||||||
|
{% if product.latest_entry != None %}
|
||||||
|
<tr>
|
||||||
|
<td class="uk-table-link">
|
||||||
|
<a href="/shop/product/{{ product.id }}">
|
||||||
|
{{ product.name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ product.latest_entry.stock }}</td>
|
||||||
|
<td>{{ product.total_stocked }}</td>
|
||||||
|
<td>{{ product.total_sold }}</td>
|
||||||
|
<td>A${{ product.total_earned }}</td>
|
||||||
|
<td>
|
||||||
|
<span title="{{ product.latest_entry.raw_price}}">
|
||||||
|
A${{ product.latest_entry.price }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="uk-text-nowrap">{{ product.latest_entry.date.isoformat() }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block pagescripts %}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.js"></script>
|
||||||
|
<script>
|
||||||
|
function highlight_empty_stock() {
|
||||||
|
var rows = document.querySelectorAll('table#products > tbody > tr > td:nth-child(2)');
|
||||||
|
for(var row of rows) {
|
||||||
|
if(parseInt(row.textContent) == 0) {
|
||||||
|
row.parentElement.classList.add('uk-text-danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculate_total_earnings() {
|
||||||
|
var total_earnings = 0.00;
|
||||||
|
var earnings = document.querySelectorAll('table#products > tbody > tr > td:nth-child(5)');
|
||||||
|
for(var earning of earnings) {
|
||||||
|
total_earnings += parseFloat(earning.textContent.substring(2))
|
||||||
|
}
|
||||||
|
document.getElementById('total_funds_earned').textContent = "A$" + total_earnings.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chart.js */
|
||||||
|
var item_labels = [
|
||||||
|
{% for product in products %}
|
||||||
|
"{{ product.name }}",
|
||||||
|
{% endfor %}
|
||||||
|
];
|
||||||
|
var sale_data = [
|
||||||
|
{% for product in products %}
|
||||||
|
{{ product.total_sold }},
|
||||||
|
{% endfor %}
|
||||||
|
];
|
||||||
|
var earnings_data = [
|
||||||
|
{% for product in products %}
|
||||||
|
{{ product.total_earned }},
|
||||||
|
{% endfor %}
|
||||||
|
];
|
||||||
|
var total_stock_data = [
|
||||||
|
{% for product in products %}
|
||||||
|
{{ product.total_stocked }},
|
||||||
|
{% endfor %}
|
||||||
|
];
|
||||||
|
var current_stock_data = [
|
||||||
|
{% for product in products %}
|
||||||
|
{{ product.latest_entry.stock }},
|
||||||
|
{% endfor %}
|
||||||
|
];
|
||||||
|
|
||||||
|
function init_sales_chart(labels, sold_data, earned_data) {
|
||||||
|
var ctx = document.getElementById("sales-chart").getContext('2d');
|
||||||
|
var chart = new Chart(ctx, {
|
||||||
|
type: 'horizontalBar',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Sold',
|
||||||
|
backgroundColor: 'rgb(93, 173, 226)',
|
||||||
|
borderColor: 'rgb(89, 131, 227)',
|
||||||
|
data: sold_data,
|
||||||
|
xAxisID: 'quantity'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Earned',
|
||||||
|
backgroundColor: 'rgb(173, 93, 226)',
|
||||||
|
borderColor: 'rgb(131, 89, 227)',
|
||||||
|
data: earned_data,
|
||||||
|
xAxisID: 'currency'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
elements: { rectangle: { borderWidth: 2, } },
|
||||||
|
responsive: true,
|
||||||
|
legend: { position: 'bottom', },
|
||||||
|
title: { display: true, text: 'Sales & Earnings'},
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
id: 'quantity',
|
||||||
|
type: 'linear',
|
||||||
|
position: 'top'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'currency',
|
||||||
|
type: 'linear',
|
||||||
|
position: 'bottom'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function init_stock_chart(labels, total_data, current_data) {
|
||||||
|
var ctx = document.getElementById("stock-chart").getContext('2d');
|
||||||
|
var chart = new Chart(ctx, {
|
||||||
|
type: 'horizontalBar',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Total',
|
||||||
|
backgroundColor: 'rgb(54, 84, 126)',
|
||||||
|
data: total_data,
|
||||||
|
xAxisID: 'quantity'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Current',
|
||||||
|
backgroundColor: 'rgb(73, 133, 226)',
|
||||||
|
data: current_data,
|
||||||
|
xAxisID: 'big_quantity'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
legend: { position: 'bottom', },
|
||||||
|
title: { display: true, text: 'Total/Current Stock'},
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
id: 'quantity',
|
||||||
|
type: 'linear',
|
||||||
|
position: 'top'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'big_quantity',
|
||||||
|
type: 'linear',
|
||||||
|
position: 'bottom'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On Page Ready */
|
||||||
|
document.addEventListener("DOMContentLoaded", function(event) {
|
||||||
|
highlight_empty_stock();
|
||||||
|
calculate_total_earnings();
|
||||||
|
init_sales_chart(item_labels, sale_data, earnings_data);
|
||||||
|
init_stock_chart(item_labels, total_stock_data, current_stock_data);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
17
discworld/templates/shop_dataentry.html
Normal file
17
discworld/templates/shop_dataentry.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<form id="mission-form" action="{{ url_for('shop_dataentry') }}" method="post"
|
||||||
|
class="uk-margin-large-left">
|
||||||
|
<label for="mission-data">Enter shops "Mission Item" output:</label> <br/>
|
||||||
|
<textarea class="uk-textarea uk-form-small uk-form-width-large uk-margin-medium-bottom"
|
||||||
|
autofocus=true rows=15 name="mission-data"></textarea>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" name="password"
|
||||||
|
class="uk-input uk-form-small uk-form-width-small uk-margin-medium-left"/>
|
||||||
|
<br/>
|
||||||
|
<input type="submit" value="Parse Entries"
|
||||||
|
class="uk-button uk-button-primary uk-margin-small-top uk-border-rounded"/>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
119
discworld/templates/shop_product_entries.html
Normal file
119
discworld/templates/shop_product_entries.html
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block left_top_bar %}
|
||||||
|
<button class="uk-button uk-button-default uk-button-small uk-border-rounded"
|
||||||
|
uk-toggle="target: #charts-container">Show/Hide Charts</button>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block center_top_bar %}
|
||||||
|
<h4 class="uk-text-center">{{ product.name }}</h4>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="charts-container" uk-grid hidden>
|
||||||
|
<div class="uk-width-1-1">
|
||||||
|
<canvas id="time-chart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table id="products" class="uk-table uk-table-small uk-table-striped uk-table-hover uk-margin-large-left">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Stock</th>
|
||||||
|
<th>Price (Raw)</th>
|
||||||
|
<th>Price (Cvt)</th>
|
||||||
|
<th>Update</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for entry in product.entries %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ entry.stock }}</td>
|
||||||
|
<td>{{ entry.raw_price }}</td>
|
||||||
|
<td>A${{ entry.price }}</td>
|
||||||
|
<td>{{ entry.date.isoformat() }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
<caption class="uk-margin-small-bottom">{{ product.entries | length }} total entries.</caption>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block pagescripts %}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.3/moment.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.js"></script>
|
||||||
|
<script>
|
||||||
|
/* Chart.js */
|
||||||
|
var item_labels = [
|
||||||
|
"{{ product.name }}",
|
||||||
|
];
|
||||||
|
var time_data = [
|
||||||
|
{% for entry in product.entries %}
|
||||||
|
{x: "{{ entry.date.isoformat() }}", y: {{ entry.stock }}},
|
||||||
|
{% endfor %}
|
||||||
|
];
|
||||||
|
|
||||||
|
function init_time_chart(labels, timedata) {
|
||||||
|
var ctx = document.getElementById("time-chart").getContext('2d');
|
||||||
|
var chart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Entries',
|
||||||
|
backgroundColor: 'rgb(93, 173, 226)',
|
||||||
|
borderColor: 'rgb(89, 131, 227)',
|
||||||
|
fill: false,
|
||||||
|
data: timedata,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
title: { display: true, text: 'Entries'},
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
type: 'time',
|
||||||
|
distribution: 'series',
|
||||||
|
display: true,
|
||||||
|
scaleLabel: {
|
||||||
|
display: true,
|
||||||
|
labelString: 'Date'
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
unit: 'day',
|
||||||
|
unitStepSize: 1,
|
||||||
|
displayFormats: {
|
||||||
|
day: 'MMM D YYYY h:mm a'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
major: {
|
||||||
|
fontStyle: "bold",
|
||||||
|
fontColor: "#FF0000"
|
||||||
|
},
|
||||||
|
source: 'data'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
display: true,
|
||||||
|
scaleLabel: {
|
||||||
|
display: true,
|
||||||
|
labelString: 'Stock'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On Page Ready */
|
||||||
|
document.addEventListener("DOMContentLoaded", function(event) {
|
||||||
|
init_time_chart(item_labels, time_data);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
36
discworld/views.py
Normal file
36
discworld/views.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
request,
|
||||||
|
url_for,
|
||||||
|
redirect,
|
||||||
|
render_template
|
||||||
|
)
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
db,
|
||||||
|
app,
|
||||||
|
models,
|
||||||
|
shop
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def shop_dashboard():
|
||||||
|
return render_template('shop_dashboard.html',
|
||||||
|
products=models.ShopProduct.query.all())
|
||||||
|
|
||||||
|
@app.route('/shop/data', methods=['POST','GET'])
|
||||||
|
def shop_dataentry():
|
||||||
|
if request.method == 'POST':
|
||||||
|
if request.form.get('password') == 'r3m3di3s' and request.form.get('mission-data'):
|
||||||
|
data = request.form['mission-data']
|
||||||
|
clean_data = data.replace('\r\n', '\n')
|
||||||
|
shop.parse_shop_output(clean_data)
|
||||||
|
return redirect(url_for('shop_dashboard'))
|
||||||
|
return render_template('shop_dataentry.html')
|
||||||
|
|
||||||
|
@app.route('/shop/product/<int:product_id>')
|
||||||
|
def shop_product_entries(product_id):
|
||||||
|
product = models.ShopProduct.query.filter_by(id=product_id).first_or_404()
|
||||||
|
return render_template('shop_product_entries.html', product=product)
|
11
uwsgi.ini
Normal file
11
uwsgi.ini
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[uwsgi]
|
||||||
|
module = wsgi:application
|
||||||
|
|
||||||
|
master = true
|
||||||
|
processes = 4
|
||||||
|
|
||||||
|
socket = discworld.sock
|
||||||
|
chmod-scoket = 660
|
||||||
|
vacuum = true
|
||||||
|
|
||||||
|
die-on-term = true
|
Loading…
x
Reference in New Issue
Block a user