|
1
|
|
|
from pyramid.events import subscriber |
|
2
|
|
|
from pyramid.events import BeforeRender |
|
3
|
|
|
from pyramid.httpexceptions import HTTPFound |
|
4
|
|
|
from pyramid.renderers import render |
|
5
|
|
|
from pyramid.renderers import render_to_response |
|
6
|
|
|
from pyramid.response import Response |
|
7
|
|
|
from pyramid.response import FileResponse |
|
8
|
|
|
from pyramid.view import view_config, forbidden_view_config |
|
9
|
|
|
|
|
10
|
|
|
from sqlalchemy.sql import func |
|
11
|
|
|
from sqlalchemy.exc import DBAPIError |
|
12
|
|
|
from sqlalchemy.orm.exc import NoResultFound |
|
13
|
|
|
|
|
14
|
|
|
from .models import * |
|
15
|
|
|
from .models.model import * |
|
16
|
|
|
from .models import user as __user |
|
17
|
|
|
from .models.user import User |
|
18
|
|
|
from .models.item import Item |
|
19
|
|
|
from .models.box import Box |
|
20
|
|
|
from .models.box_item import BoxItem |
|
21
|
|
|
from .models.transaction import Transaction, Deposit, CashDeposit, BTCDeposit |
|
22
|
|
|
from .models.transaction import PurchaseLineItem, SubTransaction |
|
23
|
|
|
from .models.account import Account, VirtualAccount, CashAccount |
|
24
|
|
|
from .models.event import Event |
|
25
|
|
|
from .models import event as __event |
|
26
|
|
|
from .models.vendor import Vendor |
|
27
|
|
|
from .models.item_vendor import ItemVendor |
|
28
|
|
|
from .models.request import Request |
|
29
|
|
|
from .models.announcement import Announcement |
|
30
|
|
|
from .models.btcdeposit import BtcPendingDeposit |
|
31
|
|
|
from .models.receipt import Receipt |
|
32
|
|
|
|
|
33
|
|
|
from pyramid.security import Allow, Everyone, remember, forget |
|
34
|
|
|
|
|
35
|
|
|
import chezbetty.datalayer as datalayer |
|
36
|
|
|
from .btc import Bitcoin, BTCException |
|
37
|
|
|
|
|
38
|
|
|
# Used for generating barcodes |
|
39
|
|
|
from reportlab.graphics.barcode import code39 |
|
40
|
|
|
from reportlab.graphics.barcode import code93 |
|
41
|
|
|
from reportlab.lib.pagesizes import letter |
|
42
|
|
|
from reportlab.lib.units import mm, inch |
|
43
|
|
|
from reportlab.pdfgen import canvas |
|
44
|
|
|
|
|
45
|
|
|
from . import utility |
|
46
|
|
|
import arrow |
|
47
|
|
|
|
|
48
|
|
|
class InvalidMetric(Exception): |
|
49
|
|
|
pass |
|
50
|
|
|
|
|
51
|
|
|
# fix_timezone |
|
52
|
|
|
def ftz(i): |
|
53
|
|
|
return i |
|
54
|
|
|
#if type(i) is datetime.date: |
|
55
|
|
|
# i = datetime.datetime(i.year, i.month, i.day) |
|
56
|
|
|
#return pytz.timezone('America/Detroit').localize(i).astimezone(tz=pytz.timezone('UTC')) |
|
57
|
|
|
|
|
58
|
|
|
|
|
59
|
|
|
def get_start(days): |
|
60
|
|
|
if days: |
|
61
|
|
|
# "now" is really midnight tonight, so we really want tomorrows date. |
|
62
|
|
|
# This makes the comparisons and math work so 1 day would mean today |
|
63
|
|
|
now = arrow.utcnow() + datetime.timedelta(days=1) |
|
64
|
|
|
delta = datetime.timedelta(days=days) |
|
65
|
|
|
return now - delta |
|
66
|
|
|
else: |
|
67
|
|
|
# Hard code in when Betty started |
|
68
|
|
|
return arrow.get(datetime.date(year=2014, month=7, day=8)) |
|
69
|
|
|
|
|
70
|
|
|
def get_end(): |
|
71
|
|
|
return arrow.utcnow() + datetime.timedelta(days=1) |
|
72
|
|
|
|
|
73
|
|
|
|
|
74
|
|
|
def create_x_y_from_group(group, start, end, period, process_output=lambda x: x, default=0): |
|
75
|
|
|
x = [] |
|
76
|
|
|
y = [] |
|
77
|
|
|
|
|
78
|
|
|
if period == 'year': |
|
79
|
|
|
dt = datetime.timedelta(days=365) |
|
80
|
|
|
fmt_str = '{}' |
|
81
|
|
|
elif period == 'month': |
|
82
|
|
|
dt = datetime.timedelta(days=30) |
|
83
|
|
|
fmt_str = '{}-{:02}' |
|
84
|
|
|
elif period == 'day': |
|
85
|
|
|
dt = datetime.timedelta(days=1) |
|
86
|
|
|
fmt_str = '{}-{:02}-{:02}' |
|
87
|
|
|
|
|
88
|
|
|
# Apparently this is a copy operation |
|
89
|
|
|
if start == datetime.date.min: |
|
90
|
|
|
ptr = group[0][0] |
|
91
|
|
|
else: |
|
92
|
|
|
ptr = start |
|
93
|
|
|
|
|
94
|
|
|
for d,total in group: |
|
95
|
|
|
# Fill in days with no data |
|
96
|
|
|
while ptr < arrow.get(datetime.date(d.year, d.month, d.day)): |
|
97
|
|
|
x.append(fmt_str.format(ptr.year, ptr.month, ptr.day)) |
|
98
|
|
|
y.append(default) |
|
99
|
|
|
ptr += dt |
|
100
|
|
|
|
|
101
|
|
|
x.append(fmt_str.format(d.year, d.month, d.day)) |
|
102
|
|
|
y.append(process_output(total)) |
|
103
|
|
|
|
|
104
|
|
|
ptr += dt |
|
105
|
|
|
|
|
106
|
|
|
# Fill in the end |
|
107
|
|
|
while ptr < end: |
|
108
|
|
|
x.append(fmt_str.format(ptr.year, ptr.month, ptr.day)) |
|
109
|
|
|
y.append(default) |
|
110
|
|
|
ptr += dt |
|
111
|
|
|
return x,y |
|
112
|
|
|
|
|
113
|
|
|
def datetime_to_timestamps (data, process_output=lambda x: x): |
|
114
|
|
|
out = [] |
|
115
|
|
|
for d in data: |
|
116
|
|
|
t = arrow.get( |
|
117
|
|
|
datetime.datetime( |
|
118
|
|
|
year=d[0].year, |
|
119
|
|
|
month=d[0].month, |
|
120
|
|
|
day=d[0].day, |
|
121
|
|
|
hour=12, |
|
122
|
|
|
) |
|
123
|
|
|
).timestamp * 1000 |
|
124
|
|
|
#t = round(datetime.datetime(year=d[0].year, month=d[0].month, day=d[0].day, hour=12)\ |
|
125
|
|
|
# .replace(tzinfo=datetime.timezone.utc).timestamp()*1000) |
|
126
|
|
|
# t = round(datetime.datetime.combine(d[0], datetime.datetime.min.time())\ |
|
127
|
|
|
# .replace(tzinfo=datetime.timezone.utc).timestamp()*1000) |
|
128
|
|
|
out.append((t, process_output(d[1]))) |
|
129
|
|
|
return out |
|
130
|
|
|
|
|
131
|
|
|
|
|
132
|
|
|
# Get x,y for some data metric |
|
133
|
|
|
# |
|
134
|
|
|
# start: datetime.datetime that all data must be at or after |
|
135
|
|
|
# end: datetime.datetime that all data must be before |
|
136
|
|
|
# metric: 'items', 'sales', or 'deposits' |
|
137
|
|
|
# period: 'day', 'month', or 'year' |
|
138
|
|
View Code Duplication |
def admin_data_period_range(start, end, metric, period): |
|
|
|
|
|
|
139
|
|
|
if metric == 'items': |
|
140
|
|
|
data = PurchaseLineItem.quantity_by_period(period, start=ftz(start), end=ftz(end)) |
|
141
|
|
|
return zip(create_x_y_from_group(data, start, end, period)) |
|
142
|
|
|
elif metric == 'sales': |
|
143
|
|
|
data = PurchaseLineItem.virtual_revenue_by_period(period, start=ftz(start), end=ftz(end)) |
|
144
|
|
|
return zip(create_x_y_from_group(data, start, end, period, float, 0.0)) |
|
145
|
|
|
elif metric == 'deposits': |
|
146
|
|
|
data = Deposit.deposits_by_period('day', start=ftz(start), end=ftz(end)) |
|
147
|
|
|
return zip(create_x_y_from_group(data, start, end, period, float, 0.0)) |
|
148
|
|
|
elif metric == 'deposits_cash': |
|
149
|
|
|
data = CashDeposit.deposits_by_period('day', start=ftz(start), end=ftz(end)) |
|
150
|
|
|
return zip(create_x_y_from_group(data, start, end, period, float, 0.0)) |
|
151
|
|
|
elif metric == 'deposits_btc': |
|
152
|
|
|
data = BTCDeposit.deposits_by_period('day', start=ftz(start), end=ftz(end)) |
|
153
|
|
|
return zip(create_x_y_from_group(data, start, end, period, float, 0.0)) |
|
154
|
|
|
else: |
|
155
|
|
|
raise(InvalidMetric(metric)) |
|
156
|
|
|
|
|
157
|
|
|
|
|
158
|
|
|
def admin_data_period(num_days, metric, period): |
|
159
|
|
|
return admin_data_period_range(get_start(num_days), get_end(), metric, period) |
|
160
|
|
|
|
|
161
|
|
View Code Duplication |
def admin_data_highcharts_period(metric, period): |
|
|
|
|
|
|
162
|
|
|
start = get_start(0) |
|
163
|
|
|
end = get_end() |
|
164
|
|
|
if metric == 'items': |
|
165
|
|
|
data = PurchaseLineItem.quantity_by_period(period, start=ftz(start), end=ftz(end)) |
|
166
|
|
|
return datetime_to_timestamps(data) |
|
167
|
|
|
elif metric == 'sales': |
|
168
|
|
|
data = PurchaseLineItem.virtual_revenue_by_period(period, start=ftz(start), end=ftz(end)) |
|
169
|
|
|
return datetime_to_timestamps(data, float) |
|
170
|
|
|
elif metric == 'deposits': |
|
171
|
|
|
data = Deposit.deposits_by_period('day', start=ftz(start), end=ftz(end)) |
|
172
|
|
|
return datetime_to_timestamps(data, float) |
|
173
|
|
|
elif metric == 'deposits_cash': |
|
174
|
|
|
data = CashDeposit.deposits_by_period('day', start=ftz(start), end=ftz(end)) |
|
175
|
|
|
return datetime_to_timestamps(data, float) |
|
176
|
|
|
elif metric == 'deposits_btc': |
|
177
|
|
|
data = BTCDeposit.deposits_by_period('day', start=ftz(start), end=ftz(end)) |
|
178
|
|
|
return datetime_to_timestamps(data, float) |
|
179
|
|
|
else: |
|
180
|
|
|
raise(InvalidMetric(metric)) |
|
181
|
|
|
|
|
182
|
|
|
|
|
183
|
|
|
### |
|
184
|
|
|
### "Each" functions. So "monday", "tuesday", etc. instead of 2014-07-21 |
|
185
|
|
|
### |
|
186
|
|
|
|
|
187
|
|
|
month_each_mapping = [(i, datetime.date(2000,i,1).strftime('%B')) for i in range(1,13)] |
|
188
|
|
|
|
|
189
|
|
|
day_each_mapping = [(i, '{:02}'.format(i)) for i in range(0,31)] |
|
190
|
|
|
|
|
191
|
|
|
weekday_each_mapping = [(6, 'Sunday'), (0, 'Monday'), (1, 'Tuesday'), |
|
192
|
|
|
(2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), |
|
193
|
|
|
(5, 'Saturday')] |
|
194
|
|
|
|
|
195
|
|
|
hour_each_mapping = [(i, '{0:02}:00-{0:02}:59'.format(i)) for i in range(0,24)] |
|
196
|
|
|
|
|
197
|
|
|
|
|
198
|
|
|
def create_x_y_from_group_each(group, mapping, start, end, process_output=lambda x: x, default=0): |
|
199
|
|
|
x = [] |
|
200
|
|
|
y = [] |
|
201
|
|
|
|
|
202
|
|
|
for d in mapping: |
|
203
|
|
|
# Put the x axis label in the x array |
|
204
|
|
|
x.append(d[1]) |
|
205
|
|
|
|
|
206
|
|
|
if d[0] in group: |
|
207
|
|
|
# We have a reading for this particular time unit |
|
208
|
|
|
y.append(process_output(group[d[0]])) |
|
209
|
|
|
else: |
|
210
|
|
|
y.append(default) |
|
211
|
|
|
|
|
212
|
|
|
return x,y |
|
213
|
|
|
|
|
214
|
|
|
|
|
215
|
|
|
# Get data about each something. So each weekday, or each hour |
|
216
|
|
|
# |
|
217
|
|
|
# metric: 'items', 'sales', or 'deposits' |
|
218
|
|
|
# each: 'day_each' or 'hour_each' |
|
219
|
|
|
def admin_data_each_range(start, end, metric, each): |
|
220
|
|
|
if each == 'month_each': |
|
221
|
|
|
mapping = month_each_mapping |
|
222
|
|
|
elif each == 'day_each': |
|
223
|
|
|
mapping = day_each_mapping |
|
224
|
|
|
elif each == 'weekday_each': |
|
225
|
|
|
mapping = weekday_each_mapping |
|
226
|
|
|
elif each == 'hour_each': |
|
227
|
|
|
mapping = hour_each_mapping |
|
228
|
|
|
|
|
229
|
|
|
if metric == 'items': |
|
230
|
|
|
data = PurchaseLineItem.quantity_by_period(each, start=ftz(start), end=ftz(end)) |
|
231
|
|
|
return zip(create_x_y_from_group_each(data, mapping, start, end)) |
|
232
|
|
|
elif metric == 'sales': |
|
233
|
|
|
data = PurchaseLineItem.virtual_revenue_by_period(each, start=ftz(start), end=ftz(end)) |
|
234
|
|
|
return zip(create_x_y_from_group_each(data, mapping, start, end, float, 0.0)) |
|
235
|
|
|
elif metric == 'deposits': |
|
236
|
|
|
data = Deposit.deposits_by_period(each, start=ftz(start), end=ftz(end)) |
|
237
|
|
|
return zip(create_x_y_from_group_each(data, mapping, start, end, float, 0.0)) |
|
238
|
|
|
else: |
|
239
|
|
|
raise(InvalidMetric(metric)) |
|
240
|
|
|
|
|
241
|
|
|
|
|
242
|
|
|
def admin_data_each(num_days, metric, each): |
|
243
|
|
|
return admin_data_each_range(get_start(num_days), get_end(), metric, each) |
|
244
|
|
|
|
|
245
|
|
|
|
|
246
|
|
|
|
|
247
|
|
|
def create_json(request, metric, period): |
|
248
|
|
|
try: |
|
249
|
|
|
if 'days' in request.GET: |
|
250
|
|
|
num_days = int(request.GET['days']) |
|
251
|
|
|
else: |
|
252
|
|
|
num_days = 0 |
|
253
|
|
|
if 'each' in period: |
|
254
|
|
|
x,y = admin_data_each(num_days, metric, period) |
|
255
|
|
|
else: |
|
256
|
|
|
x,y = admin_data_period(num_days, metric, period) |
|
257
|
|
|
return {'x': x, |
|
258
|
|
|
'y': y, |
|
259
|
|
|
'num_days': num_days or 'all'} |
|
260
|
|
|
except ValueError: |
|
261
|
|
|
return {'status': 'error'} |
|
262
|
|
|
except utility.InvalidGroupPeriod as e: |
|
263
|
|
|
return {'status': 'error', |
|
264
|
|
|
'message': 'Invalid period for grouping data: {}'.format(e)} |
|
265
|
|
|
except InvalidMetric as e: |
|
266
|
|
|
return {'status': 'error', |
|
267
|
|
|
'message': 'Invalid metric for requesting data: {}'.format(e)} |
|
268
|
|
|
except Exception as e: |
|
269
|
|
|
if request.debug: raise(e) |
|
270
|
|
|
return {'status': 'error'} |
|
271
|
|
|
|
|
272
|
|
|
|
|
273
|
|
|
def create_highcharts_json(request, metric, period): |
|
274
|
|
|
try: |
|
275
|
|
|
return admin_data_highcharts_period(metric, period) |
|
276
|
|
|
except ValueError: |
|
277
|
|
|
return {'status': 'error'} |
|
278
|
|
|
except utility.InvalidGroupPeriod as e: |
|
279
|
|
|
return {'status': 'error', |
|
280
|
|
|
'message': 'Invalid period for grouping data: {}'.format(e)} |
|
281
|
|
|
except InvalidMetric as e: |
|
282
|
|
|
return {'status': 'error', |
|
283
|
|
|
'message': 'Invalid metric for requesting data: {}'.format(e)} |
|
284
|
|
|
except Exception as e: |
|
285
|
|
|
if request.debug: raise(e) |
|
286
|
|
|
return {'status': 'error'} |
|
287
|
|
|
|
|
288
|
|
|
def create_dict_to_date(metric, period): |
|
289
|
|
|
now = datetime.date.today() |
|
290
|
|
|
|
|
291
|
|
|
if period == 'month': |
|
292
|
|
|
start = arrow.get(datetime.date(now.year, now.month, 1)) |
|
293
|
|
|
elif period == 'year': |
|
294
|
|
|
start = arrow.get(datetime.date(now.year, 1, 1)) |
|
295
|
|
|
|
|
296
|
|
|
xs,ys = admin_data_period_range(start, get_end(), metric, period) |
|
297
|
|
|
|
|
298
|
|
|
return {'xs': xs, |
|
299
|
|
|
'ys': ys} |
|
300
|
|
|
|
|
301
|
|
|
|
|
302
|
|
|
def create_dict(metric, period, num_days): |
|
303
|
|
|
if 'each' in period: |
|
304
|
|
|
xs,ys = admin_data_each(num_days, metric, period) |
|
305
|
|
|
else: |
|
306
|
|
|
xs,ys = admin_data_period(num_days, metric, period) |
|
307
|
|
|
|
|
308
|
|
|
return {'xs': xs, |
|
309
|
|
|
'ys': ys, |
|
310
|
|
|
'avg': [sum(y)/len(y) for y in ys], |
|
311
|
|
|
'avg_hack': [[sum(y)/len(y)]*len(y) for y in ys], |
|
312
|
|
|
'num_days': num_days or 'all'} |
|
313
|
|
|
|
|
314
|
|
|
|
|
315
|
|
|
# Get a list of timestamps and the number of a particular item that was sold |
|
316
|
|
|
# at that time. |
|
317
|
|
|
def create_item_sales_json(request, item_id): |
|
318
|
|
|
sales = PurchaseLineItem.item_sale_quantities(item_id) |
|
319
|
|
|
|
|
320
|
|
|
individual = [] |
|
321
|
|
|
totals = [] |
|
322
|
|
|
total = 0 |
|
323
|
|
|
for s in sales: |
|
324
|
|
|
tstamp = s[1].timestamp.timestamp*1000 |
|
325
|
|
|
individual.append((tstamp, s[0].quantity)) |
|
326
|
|
|
total += s[0].quantity |
|
327
|
|
|
totals.append((tstamp, total)) |
|
328
|
|
|
|
|
329
|
|
|
return {'individual': individual, |
|
330
|
|
|
'sum': totals} |
|
331
|
|
|
|
|
332
|
|
|
####### |
|
333
|
|
|
### Calculate the speed of sale for all items |
|
334
|
|
|
|
|
335
|
|
|
# We are going to do this over all time and over the last 30 days |
|
336
|
|
|
|
|
337
|
|
|
# Returns a dict of {item_num -> {number of days -> sale speed}} |
|
338
|
|
|
def item_sale_speed(num_days, only_item_id=None): |
|
339
|
|
|
# TODO: If we're only looking for one item (only_item_id), this can probably |
|
340
|
|
|
# be made more efficient |
|
341
|
|
|
|
|
342
|
|
|
# First we need to figure out when each item was in stock and when it wasn't. |
|
343
|
|
|
# I don't know what the best way to do this is. I think the easiest way is |
|
344
|
|
|
# to look at the in_stock column in the item_history table and figure it |
|
345
|
|
|
# out from there. |
|
346
|
|
|
|
|
347
|
|
|
# Start by getting all item change events for the last thirty days |
|
348
|
|
|
data = {} |
|
349
|
|
|
|
|
350
|
|
|
data_onsale = {} |
|
351
|
|
|
|
|
352
|
|
|
start = get_start(num_days) |
|
353
|
|
|
start_datetime = arrow.get(datetime.datetime(start.year, start.month, start.day)) |
|
354
|
|
|
|
|
355
|
|
|
start_padding = get_start(num_days*3) |
|
356
|
|
|
start_str = start_padding.strftime('%Y-%m-%d 0:0') |
|
357
|
|
|
# This gets a little hairy b/c we circumvent sqlalchemy here. This means |
|
358
|
|
|
# that timestamps aren't automatically converted into arrow objects, so we |
|
359
|
|
|
# have to do it ourselves everywhere we access them |
|
360
|
|
|
items = DBSession.execute("SELECT * FROM items_history\ |
|
361
|
|
|
WHERE item_changed_at>'{}'\ |
|
362
|
|
|
ORDER BY item_changed_at ASC".format(start_str)) |
|
363
|
|
|
|
|
364
|
|
|
# Calculate the number of days in the interval the item was in stock |
|
365
|
|
|
for item in items: |
|
366
|
|
|
status = item.in_stock>0 |
|
367
|
|
|
|
|
368
|
|
|
item_changed_at = arrow.get(item.item_changed_at) |
|
369
|
|
|
|
|
370
|
|
|
if item.id not in data_onsale: |
|
371
|
|
|
data_onsale[item.id] = {'days_on_sale': 0, |
|
372
|
|
|
'date_in_stock': None, |
|
373
|
|
|
'num_sold': 0} |
|
374
|
|
|
|
|
375
|
|
|
if item_changed_at < start_datetime: |
|
376
|
|
|
# We need to figure out if the item started in stock at the |
|
377
|
|
|
# beginning of the time period. |
|
378
|
|
|
if status == True: |
|
379
|
|
|
data_onsale[item.id]['date_in_stock'] = start_datetime |
|
380
|
|
|
else: |
|
381
|
|
|
data_onsale[item.id]['date_in_stock'] = None |
|
382
|
|
|
|
|
383
|
|
|
elif (status == True) and (data_onsale[item.id]['date_in_stock'] == None): |
|
384
|
|
|
# item is in stock now and wasn't before |
|
385
|
|
|
data_onsale[item.id]['date_in_stock'] = item_changed_at |
|
386
|
|
|
|
|
387
|
|
|
elif (status == False) and (data_onsale[item.id]['date_in_stock'] != None): |
|
388
|
|
|
# Item is now out of stock |
|
389
|
|
|
|
|
390
|
|
|
# calculate time difference |
|
391
|
|
|
tdelta = item_changed_at - data_onsale[item.id]['date_in_stock'] |
|
392
|
|
|
data_onsale[item.id]['days_on_sale'] += tdelta.days |
|
393
|
|
|
#print('{}: {}'.format(item.id, tdelta)) |
|
394
|
|
|
|
|
395
|
|
|
data_onsale[item.id]['date_in_stock'] = None |
|
396
|
|
|
|
|
397
|
|
|
for item_id,item_data in data_onsale.items(): |
|
398
|
|
|
if item_data['date_in_stock'] != None: |
|
399
|
|
|
tdelta = arrow.now() - item_data['date_in_stock'] |
|
400
|
|
|
item_data['days_on_sale'] += tdelta.days |
|
401
|
|
|
#print('{}: {}'.format(item_id, tdelta.days)) |
|
402
|
|
|
|
|
403
|
|
|
|
|
404
|
|
|
# Calculate the number of items sold during the period |
|
405
|
|
|
purchases = DBSession.query(PurchaseLineItem)\ |
|
406
|
|
|
.join(Transaction)\ |
|
407
|
|
|
.join(Event)\ |
|
408
|
|
|
.filter(Event.deleted==False)\ |
|
409
|
|
|
.filter(Event.timestamp>start) |
|
410
|
|
|
for purchase in purchases: |
|
411
|
|
|
item_id = purchase.item_id |
|
412
|
|
|
quantity = purchase.quantity |
|
413
|
|
|
|
|
414
|
|
|
# Not sure this check should be necessary, but just make sure |
|
415
|
|
|
if item_id not in data_onsale: |
|
416
|
|
|
data_onsale[item_id] = {'days_on_sale': 0, |
|
417
|
|
|
'date_in_stock': None, |
|
418
|
|
|
'num_sold': 0} |
|
419
|
|
|
|
|
420
|
|
|
data_onsale[item_id]['num_sold'] += quantity |
|
421
|
|
|
|
|
422
|
|
|
|
|
423
|
|
|
# Calculate rate, finally |
|
424
|
|
|
for itemid,item_data in data_onsale.items(): |
|
425
|
|
|
if item_data['days_on_sale'] == 0: |
|
426
|
|
|
data[itemid] = 0 |
|
427
|
|
|
continue |
|
428
|
|
|
data[itemid] = item_data['num_sold'] / item_data['days_on_sale'] |
|
429
|
|
|
|
|
430
|
|
|
if only_item_id: |
|
431
|
|
|
if only_item_id in data: |
|
432
|
|
|
return data[only_item_id] |
|
433
|
|
|
else: |
|
434
|
|
|
return 0 |
|
435
|
|
|
else: |
|
436
|
|
|
return data |
|
437
|
|
|
|
|
438
|
|
|
|
|
439
|
|
|
####### |
|
440
|
|
|
### Calculate a histogram of user balances |
|
441
|
|
|
# |
|
442
|
|
|
# This has a special feature where it counts 0.00 as its own special bin |
|
443
|
|
|
def user_balance_histogram (): |
|
444
|
|
|
bin_size = 5 # $5 |
|
445
|
|
|
bins = {} |
|
446
|
|
|
|
|
447
|
|
|
def to_bin (x): |
|
448
|
|
|
if x == Decimal(0): |
|
449
|
|
|
return 0 |
|
450
|
|
|
start = int(bin_size * round(float(x)/bin_size)) |
|
451
|
|
|
if start == 0: |
|
452
|
|
|
start = 0.01 |
|
453
|
|
|
return start |
|
454
|
|
|
|
|
455
|
|
|
users = User.get_normal_users() |
|
456
|
|
|
for user in users: |
|
457
|
|
|
balance_bin = to_bin(user.balance) |
|
458
|
|
|
if balance_bin not in bins: |
|
459
|
|
|
bins[balance_bin] = 1 |
|
460
|
|
|
else: |
|
461
|
|
|
bins[balance_bin] += 1 |
|
462
|
|
|
|
|
463
|
|
|
out = {} |
|
464
|
|
|
|
|
465
|
|
|
out['raw'] = bins |
|
466
|
|
|
|
|
467
|
|
|
last = None |
|
468
|
|
|
x = [] |
|
469
|
|
|
y = [] |
|
470
|
|
|
for bin_start, count in sorted(bins.items()): |
|
471
|
|
|
zero = False |
|
472
|
|
|
|
|
473
|
|
|
# Handle near 0 special |
|
474
|
|
|
if bin_start == 0: |
|
475
|
|
|
zero = True |
|
476
|
|
|
|
|
477
|
|
|
if bin_start == 0.01: |
|
478
|
|
|
bin_start = 0 |
|
479
|
|
|
|
|
480
|
|
|
# Fill in missing bins, if needed |
|
481
|
|
|
if last != None and bin_start-last > bin_size: |
|
482
|
|
|
for i in range(last+bin_size, bin_start, bin_size): |
|
483
|
|
|
b = '{} to {}'.format(i, i+bin_size) |
|
484
|
|
|
x.append(b) |
|
485
|
|
|
y.append(0) |
|
486
|
|
|
|
|
487
|
|
|
if zero: |
|
488
|
|
|
b = '0' |
|
489
|
|
|
else: |
|
490
|
|
|
b = '{} to {}'.format(bin_start, bin_start+bin_size) |
|
491
|
|
|
x.append(b) |
|
492
|
|
|
y.append(count) |
|
493
|
|
|
|
|
494
|
|
|
last = bin_start |
|
495
|
|
|
|
|
496
|
|
|
out['x'] = x |
|
497
|
|
|
out['y'] = y |
|
498
|
|
|
|
|
499
|
|
|
return out |
|
500
|
|
|
|
|
501
|
|
|
|
|
502
|
|
|
####### |
|
503
|
|
|
### Calculate a histogram of user days since last purchase |
|
504
|
|
|
# |
|
505
|
|
|
# This has a special feature where it counts 0.00 as its own special bin |
|
506
|
|
|
def user_dayssincepurchase_histogram (): |
|
507
|
|
|
bin_size = 10 # days |
|
508
|
|
|
bins = {} |
|
509
|
|
|
|
|
510
|
|
|
def to_bin (x): |
|
511
|
|
|
if x == None: |
|
512
|
|
|
return None |
|
513
|
|
|
return int(bin_size * round(float(x)/bin_size)) |
|
514
|
|
|
|
|
515
|
|
|
users = User.get_normal_users() |
|
516
|
|
|
for user in users: |
|
517
|
|
|
the_bin = to_bin(user.days_since_last_purchase) |
|
518
|
|
|
if the_bin != None: |
|
519
|
|
|
if the_bin not in bins: |
|
520
|
|
|
bins[the_bin] = 1 |
|
521
|
|
|
else: |
|
522
|
|
|
bins[the_bin] += 1 |
|
523
|
|
|
|
|
524
|
|
|
out = {} |
|
525
|
|
|
|
|
526
|
|
|
out['raw'] = bins |
|
527
|
|
|
|
|
528
|
|
|
last = None |
|
529
|
|
|
x = [] |
|
530
|
|
|
y = [] |
|
531
|
|
|
for bin_start, count in sorted(bins.items()): |
|
532
|
|
|
# Fill in missing bins, if needed |
|
533
|
|
|
if last != None and bin_start-last > bin_size: |
|
534
|
|
|
for i in range(last+bin_size, bin_start, bin_size): |
|
535
|
|
|
b = '{} to {}'.format(i, i+bin_size) |
|
536
|
|
|
x.append(b) |
|
537
|
|
|
y.append(0) |
|
538
|
|
|
|
|
539
|
|
|
b = '{} to {}'.format(bin_start, bin_start+bin_size) |
|
540
|
|
|
x.append(b) |
|
541
|
|
|
y.append(count) |
|
542
|
|
|
|
|
543
|
|
|
last = bin_start |
|
544
|
|
|
|
|
545
|
|
|
out['x'] = x |
|
546
|
|
|
out['y'] = y |
|
547
|
|
|
|
|
548
|
|
|
return out |
|
549
|
|
|
|
|
550
|
|
|
|
|
551
|
|
|
####### |
|
552
|
|
|
### Calculate a histogram of number of purchases by each user |
|
553
|
|
|
# |
|
554
|
|
|
# |
|
555
|
|
|
def user_numberofpurchases_histogram (): |
|
556
|
|
|
bins = {} |
|
557
|
|
|
|
|
558
|
|
|
users = User.get_normal_users() |
|
559
|
|
|
for user in users: |
|
560
|
|
|
number_purchases = user.number_of_purchases |
|
561
|
|
|
if number_purchases > 200: |
|
562
|
|
|
number_purchases = 200 |
|
563
|
|
|
|
|
564
|
|
|
if number_purchases not in bins: |
|
565
|
|
|
bins[number_purchases] = 1 |
|
566
|
|
|
else: |
|
567
|
|
|
bins[number_purchases] += 1 |
|
568
|
|
|
|
|
569
|
|
|
out = {} |
|
570
|
|
|
|
|
571
|
|
|
last = None |
|
572
|
|
|
x = [] |
|
573
|
|
|
y = [] |
|
574
|
|
|
for bin_start, count in sorted(bins.items()): |
|
575
|
|
|
# Fill in missing bins, if needed |
|
576
|
|
|
if last != None and bin_start-last > 1: |
|
577
|
|
|
for i in range(last, bin_start): |
|
578
|
|
|
b = '{}'.format(i) |
|
579
|
|
|
x.append(b) |
|
580
|
|
|
y.append(0) |
|
581
|
|
|
|
|
582
|
|
|
b = '{}'.format(bin_start) |
|
583
|
|
|
x.append(b) |
|
584
|
|
|
y.append(count) |
|
585
|
|
|
|
|
586
|
|
|
last = bin_start |
|
587
|
|
|
|
|
588
|
|
|
out['x'] = x |
|
589
|
|
|
out['y'] = y |
|
590
|
|
|
|
|
591
|
|
|
return out |
|
592
|
|
|
|
|
593
|
|
|
|
|
594
|
|
|
@view_config(route_name='admin_data_items_json', |
|
595
|
|
|
renderer='json', |
|
596
|
|
|
permission='manage') |
|
597
|
|
|
def admin_data_items_json(request): |
|
598
|
|
|
return create_json(request, 'items', request.matchdict['period']) |
|
599
|
|
|
|
|
600
|
|
|
|
|
601
|
|
|
@view_config(route_name='admin_data_sales_json', |
|
602
|
|
|
renderer='json', |
|
603
|
|
|
permission='manage') |
|
604
|
|
|
def admin_data_sales_json(request): |
|
605
|
|
|
return create_json(request, 'sales', request.matchdict['period']) |
|
606
|
|
|
|
|
607
|
|
|
|
|
608
|
|
|
@view_config(route_name='admin_data_json_highcharts', |
|
609
|
|
|
renderer='json', |
|
610
|
|
|
permission='manage') |
|
611
|
|
|
def admin_data_json_highcharts(request): |
|
612
|
|
|
return create_highcharts_json(request, request.matchdict['metric'], request.matchdict['period']) |
|
613
|
|
|
|
|
614
|
|
|
|
|
615
|
|
|
@view_config(route_name='admin_data_deposits_json', |
|
616
|
|
|
renderer='json', |
|
617
|
|
|
permission='manage') |
|
618
|
|
|
def admin_data_deposits_json(request): |
|
619
|
|
|
return create_json(request, 'deposits', request.matchdict['period']) |
|
620
|
|
|
|
|
621
|
|
|
|
|
622
|
|
|
@view_config(route_name='admin_data_items_each_json', |
|
623
|
|
|
renderer='json', |
|
624
|
|
|
permission='manage') |
|
625
|
|
|
def admin_data_items_each_json(request): |
|
626
|
|
|
return create_json(request, 'items', request.matchdict['period']+'_each') |
|
627
|
|
|
|
|
628
|
|
|
|
|
629
|
|
|
@view_config(route_name='admin_data_sales_each_json', |
|
630
|
|
|
renderer='json', |
|
631
|
|
|
permission='manage') |
|
632
|
|
|
def admin_data_sales_each_json(request): |
|
633
|
|
|
return create_json(request, 'sales', request.matchdict['period']+'_each') |
|
634
|
|
|
|
|
635
|
|
|
|
|
636
|
|
|
@view_config(route_name='admin_data_deposits_each_json', |
|
637
|
|
|
renderer='json', |
|
638
|
|
|
permission='manage') |
|
639
|
|
|
def admin_data_deposits_each_json(request): |
|
640
|
|
|
return create_json(request, 'deposits', request.matchdict['period']+'_each') |
|
641
|
|
|
|
|
642
|
|
|
|
|
643
|
|
|
# All of the sale dates and quantities of a particular item |
|
644
|
|
|
@view_config(route_name='admin_data_item_sales_json', |
|
645
|
|
|
renderer='json', |
|
646
|
|
|
permission='manage') |
|
647
|
|
|
def admin_data_item_sales_json(request): |
|
648
|
|
|
return create_item_sales_json(request, request.matchdict['item_id']) |
|
649
|
|
|
|
|
650
|
|
|
|
|
651
|
|
|
# Timestamps and the number of total users |
|
652
|
|
|
@view_config(route_name='admin_data_users_totals_json', |
|
653
|
|
|
renderer='json', |
|
654
|
|
|
permission='manage') |
|
655
|
|
|
def admin_data_users_totals_json(request): |
|
656
|
|
|
return User.get_user_count_cumulative() |
|
657
|
|
|
|
|
658
|
|
|
|
|
659
|
|
|
# Timestamps and user debt, bank balance, debt/# users in debt |
|
660
|
|
|
@view_config(route_name='admin_data_users_balance_totals_json', |
|
661
|
|
|
renderer='json', |
|
662
|
|
|
permission='manage') |
|
663
|
|
|
def admin_data_users_balance_totals_json(request): |
|
664
|
|
|
return Transaction.get_balance_total_daily() |
|
665
|
|
|
|
|
666
|
|
|
|
|
667
|
|
|
# Timestamps and balance for a specific user over time |
|
668
|
|
|
@view_config(route_name='admin_data_user_balance_json', |
|
669
|
|
|
renderer='json', |
|
670
|
|
|
permission='manage') |
|
671
|
|
|
def admin_data_user_balance_json(request): |
|
672
|
|
|
user = User.from_id(request.matchdict['user_id']) |
|
673
|
|
|
return Transaction.get_balances_over_time_for_user(user) |
|
674
|
|
|
|
|
675
|
|
|
|
|
676
|
|
|
# # Timestamps and user debt, "bank balance", debt/user |
|
677
|
|
|
# @view_config(route_name='admin_data_users_balance_totals_percapita_json', |
|
678
|
|
|
# renderer='json', |
|
679
|
|
|
# permission='manage') |
|
680
|
|
|
# def admin_data_users_balance_totals_percapita_json(request): |
|
681
|
|
|
# debt = Transaction.get_balance_total_daily() |
|
682
|
|
|
# users = User.get_user_count_cumulative() |
|
683
|
|
|
|
|
684
|
|
|
# di = 0 |
|
685
|
|
|
# ui = 0 |
|
686
|
|
|
# next_user_time = users[ui][0] |
|
687
|
|
|
# user_count = users[ui][1] |
|
688
|
|
|
# out = [] |
|
689
|
|
|
|
|
690
|
|
|
# for rec in debt: |
|
691
|
|
|
# timestamp = rec[0] |
|
692
|
|
|
# debt = rec[1] |
|
693
|
|
|
# balance = rec[2] |
|
694
|
|
|
|
|
695
|
|
|
# # Look for the correct number of users |
|
696
|
|
|
# while timestamp > next_user_time: |
|
697
|
|
|
# ui += 1 |
|
698
|
|
|
# if ui >= len(users): |
|
699
|
|
|
# break |
|
700
|
|
|
# next_user_time = users[ui][0] |
|
701
|
|
|
# user_count = users[ui][1] |
|
702
|
|
|
|
|
703
|
|
|
# debt_per_capita = debt/user_count |
|
704
|
|
|
|
|
705
|
|
|
# out.append((timestamp, debt, balance, debt_per_capita)) |
|
706
|
|
|
|
|
707
|
|
|
# return out |
|
708
|
|
|
|
|
709
|
|
|
|
|
710
|
|
|
@view_config(route_name='admin_data_speed_items', |
|
711
|
|
|
renderer='json', |
|
712
|
|
|
permission='manage') |
|
713
|
|
|
def admin_data_speed_items(request): |
|
714
|
|
|
return item_sale_speed(30) |
|
715
|
|
|
|
|
716
|
|
|
|
|
717
|
|
|
@view_config(route_name='admin_data_histogram_balances', |
|
718
|
|
|
renderer='json', |
|
719
|
|
|
permission='manage') |
|
720
|
|
|
def admin_data_histogram_balances(request): |
|
721
|
|
|
return user_balance_histogram() |
|
722
|
|
|
|
|
723
|
|
|
|
|
724
|
|
|
@view_config(route_name='admin_data_histogram_dayssincepurchase', |
|
725
|
|
|
renderer='json', |
|
726
|
|
|
permission='manage') |
|
727
|
|
|
def admin_data_histogram_dayssincepurchase(request): |
|
728
|
|
|
return user_dayssincepurchase_histogram() |
|
729
|
|
|
|
|
730
|
|
|
|
|
731
|
|
|
@view_config(route_name='admin_data_histogram_numberofpurchases', |
|
732
|
|
|
renderer='json', |
|
733
|
|
|
permission='manage') |
|
734
|
|
|
def admin_data_histogram_numberofpurchases(request): |
|
735
|
|
|
return user_numberofpurchases_histogram() |
|
736
|
|
|
|
|
737
|
|
|
|