Browse Source

Initial commit

master
Brandon Cornejo 6 years ago
commit
7825abf11c
  1. 3
      config.py
  2. BIN
      discworld.db
  3. 9
      discworld/__init__.py
  4. 95
      discworld/models.py
  5. 97
      discworld/shop.py
  6. BIN
      discworld/static/favicon.ico
  7. 76
      discworld/templates/layout.html
  8. 205
      discworld/templates/shop_dashboard.html
  9. 17
      discworld/templates/shop_dataentry.html
  10. 119
      discworld/templates/shop_product_entries.html
  11. 36
      discworld/views.py
  12. 11
      uwsgi.ini
  13. 4
      wsgi.py

3
config.py

@ -0,0 +1,3 @@
DEBUG = False
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_DATABASE_URI = 'sqlite:///../discworld.db'

BIN
discworld.db

9
discworld/__init__.py

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

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

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

After

Width: 16  |  Height: 16  |  Size: 785 B

76
discworld/templates/layout.html

@ -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">
&copy; 2017 by <a href="http://binaryatrocity.name">Ruhsbaar</a>
&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;
Created for Ramtops Remedies and Reagents
&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;
<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

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

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

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

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

@ -0,0 +1,11 @@
[uwsgi]
module = wsgi:application
master = true
processes = 4
socket = discworld.sock
chmod-scoket = 660
vacuum = true
die-on-term = true

4
wsgi.py

@ -0,0 +1,4 @@
from discworld import app as application
if __name__ == "__main__":
application.run()
Loading…
Cancel
Save