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, IntegrityError |
12
|
|
|
from sqlalchemy.orm.exc import NoResultFound |
13
|
|
|
|
14
|
|
|
from . import views_data |
15
|
|
|
|
16
|
|
|
from .models import * |
17
|
|
|
from .models.model import * |
18
|
|
|
from .models import user as __user |
19
|
|
|
from .models.user import User |
20
|
|
|
from .models.item import Item |
21
|
|
|
from .models.box import Box |
22
|
|
|
from .models.box_item import BoxItem |
23
|
|
|
from .models.transaction import Transaction, Deposit, CashDeposit, CCDeposit, BTCDeposit, Purchase |
24
|
|
|
from .models.transaction import Inventory, InventoryLineItem |
25
|
|
|
from .models.transaction import PurchaseLineItem, SubTransaction, SubSubTransaction |
26
|
|
|
from .models.account import Account, VirtualAccount, CashAccount |
27
|
|
|
from .models.event import Event |
28
|
|
|
from .models import event as __event |
29
|
|
|
from .models.vendor import Vendor |
30
|
|
|
from .models.item_vendor import ItemVendor |
31
|
|
|
from .models.box_vendor import BoxVendor |
32
|
|
|
from .models.request import Request |
33
|
|
|
from .models.announcement import Announcement |
34
|
|
|
from .models.btcdeposit import BtcPendingDeposit |
35
|
|
|
from .models.receipt import Receipt |
36
|
|
|
from .models.pool import Pool |
37
|
|
|
from .models.pool_user import PoolUser |
38
|
|
|
|
39
|
|
|
from .utility import post_stripe_payment |
40
|
|
|
|
41
|
|
|
from pyramid.security import Allow, Everyone, remember, forget |
42
|
|
|
|
43
|
|
|
import chezbetty.datalayer as datalayer |
44
|
|
|
from .btc import Bitcoin, BTCException |
45
|
|
|
|
46
|
|
|
import uuid |
47
|
|
|
import math |
48
|
|
|
import pytz |
49
|
|
|
import traceback |
50
|
|
|
import arrow |
51
|
|
|
|
52
|
|
|
### |
53
|
|
|
### User Admin |
54
|
|
|
### |
55
|
|
|
|
56
|
|
|
|
57
|
|
|
### Common helper code |
58
|
|
|
|
59
|
|
|
def transaction_history_queries(request, user_or_pool): |
60
|
|
|
# Web library native date format: 2015/06/19 19:00 |
61
|
|
|
# DO NOT CHANGE: The datetimepicker has a bug and tries to parse the |
62
|
|
|
# prepopulated value before reading the format string, |
63
|
|
|
# which means you have to use its native format string |
64
|
|
|
TS_FORMAT = 'YYYY/MM/DD HH:mm' |
65
|
|
|
if 'history-end' not in request.GET: |
66
|
|
|
request.GET['history-end'] =\ |
67
|
|
|
arrow.now()\ |
68
|
|
|
.replace(hours=+1)\ |
69
|
|
|
.floor('hour')\ |
70
|
|
|
.format(TS_FORMAT) |
71
|
|
|
if 'history-start' not in request.GET: |
72
|
|
|
request.GET['history-start'] =\ |
73
|
|
|
arrow.get(request.GET['history-end'], TS_FORMAT)\ |
74
|
|
|
.replace(months=-1)\ |
75
|
|
|
.format(TS_FORMAT) |
76
|
|
|
|
77
|
|
|
start = arrow.get(request.GET['history-start'], TS_FORMAT) |
78
|
|
|
end = arrow.get(request.GET['history-end'], TS_FORMAT) |
79
|
|
|
start = start.replace(tzinfo='US/Eastern') |
80
|
|
|
end = end .replace(tzinfo='US/Eastern') |
81
|
|
|
start = start.to('utc') |
82
|
|
|
end = end .to('utc') |
83
|
|
|
|
84
|
|
|
query = user_or_pool.get_transactions_query() |
85
|
|
|
query = query\ |
86
|
|
|
.filter(event.Event.timestamp > start.datetime)\ |
87
|
|
|
.filter(event.Event.timestamp < end.datetime) |
88
|
|
|
|
89
|
|
|
for t in ('purchase', 'adjustment'): |
90
|
|
|
if 'history-filter-'+t in request.GET: |
91
|
|
|
query = query.filter(event.Event.type!=t) |
92
|
|
|
for t in ('cashdeposit', 'ccdeposit', 'btcdeposit'): |
93
|
|
|
if 'history-filter-'+t in request.GET: |
94
|
|
|
query = query.filter(Transaction.type!=t) |
95
|
|
|
transactions = query.all() |
96
|
|
|
|
97
|
|
|
withdrawls = query.filter(event.Event.type=='purchase').all() |
98
|
|
|
deposits = query.filter(event.Event.type=='deposit').all() |
99
|
|
|
adjustments = query.filter(event.Event.type=='adjustment').all() |
100
|
|
|
|
101
|
|
|
withdrawls = sum(w.amount for w in withdrawls) |
102
|
|
|
deposits = sum(d.amount for d in deposits) |
103
|
|
|
adjustments = sum(a.amount for a in adjustments) if len(adjustments) else None |
104
|
|
|
|
105
|
|
|
return {'transactions': transactions, |
106
|
|
|
'withdrawls': withdrawls, |
107
|
|
|
'deposits': deposits, |
108
|
|
|
'adjustments': adjustments, |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
|
112
|
|
|
|
113
|
|
|
@view_config(route_name='user_ajax_bool', |
114
|
|
|
permission='user') |
115
|
|
|
def user_ajax_bool(request): |
116
|
|
|
obj_str = request.matchdict['object'] |
117
|
|
|
obj_id = int(request.matchdict['id']) |
118
|
|
|
obj_field = request.matchdict['field'] |
119
|
|
|
obj_state = request.matchdict['state'].lower() == 'true' |
120
|
|
|
|
121
|
|
|
if obj_str == 'pool': |
122
|
|
|
obj = Pool.from_id(obj_id) |
123
|
|
|
if obj.owner != request.user.id: |
124
|
|
|
request.response.status = 502 |
125
|
|
|
return request.response |
126
|
|
|
elif obj_str == 'pool_user': |
127
|
|
|
obj = PoolUser.from_id(obj_id) |
128
|
|
|
if obj.pool.owner != request.user.id: |
129
|
|
|
request.response.status = 502 |
130
|
|
|
return request.response |
131
|
|
|
else: |
132
|
|
|
# Return an error, object type not recognized |
133
|
|
|
request.response.status = 502 |
134
|
|
|
return request.response |
135
|
|
|
|
136
|
|
|
setattr(obj, obj_field, obj_state) |
137
|
|
|
DBSession.flush() |
138
|
|
|
|
139
|
|
|
return request.response |
140
|
|
|
|
141
|
|
|
@view_config(route_name='user_index', |
142
|
|
|
renderer='templates/user/index.jinja2', |
143
|
|
|
permission='user') |
144
|
|
|
def user_index(request): |
145
|
|
|
r = transaction_history_queries(request, request.user) |
146
|
|
|
r['user'] = request.user |
147
|
|
|
r['my_pools'] = Pool.all_by_owner(request.user) |
148
|
|
|
|
149
|
|
|
return r |
150
|
|
|
|
151
|
|
|
@view_config(route_name='user_index_slash', |
152
|
|
|
renderer='templates/user/index.jinja2', |
153
|
|
|
permission='user') |
154
|
|
|
def user_index_slash(request): |
155
|
|
|
return HTTPFound(location=request.route_url('user_index')) |
156
|
|
|
|
157
|
|
|
@view_config(route_name='user_deposit_cc', |
158
|
|
|
renderer='templates/user/deposit_cc.jinja2', |
159
|
|
|
permission='user') |
160
|
|
|
def user_deposit_cc(request): |
161
|
|
|
pools = Pool.all_accessable(request.user, True) |
162
|
|
|
pool = None |
163
|
|
|
if 'acct' in request.GET: |
164
|
|
|
account = request.GET['acct'] |
165
|
|
|
if account != 'user': |
166
|
|
|
pool = Pool.from_id(account.split('-')[1]) |
167
|
|
|
else: |
168
|
|
|
account = 'user' |
169
|
|
|
return {'user': request.user, |
170
|
|
|
'account': account, |
171
|
|
|
'pool': pool, |
172
|
|
|
'pools': pools, |
173
|
|
|
'stripe_pk': request.registry.settings['stripe.publishable_key'], |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
@view_config(route_name='user_deposit_cc_custom', |
177
|
|
|
renderer='templates/user/deposit_cc_custom.jinja2', |
178
|
|
|
permission='user') |
179
|
|
|
def user_deposit_cc_custom(request): |
180
|
|
|
account = request.GET['betty_to_account'] |
181
|
|
|
if account != 'user': |
182
|
|
|
pool = Pool.from_id(account.split('-')[1]) |
183
|
|
|
else: |
184
|
|
|
pool = None |
185
|
|
|
return {'user': request.user, |
186
|
|
|
'stripe_pk': request.registry.settings['stripe.publishable_key'], |
187
|
|
|
'amount': round(Decimal(request.GET['deposit-amount']), 2), |
188
|
|
|
'account': account, |
189
|
|
|
'pool': pool, |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
@view_config(route_name='user_deposit_cc_submit', |
193
|
|
|
request_method='POST', |
194
|
|
|
permission='user') |
195
|
|
|
def user_deposit_cc_submit(request): |
196
|
|
|
token = request.POST['stripeToken'] |
197
|
|
|
amount = Decimal(request.POST['betty_amount']) |
198
|
|
|
total_cents = int(request.POST['betty_total_cents']) |
199
|
|
|
to_account = request.POST['betty_to_account'] |
200
|
|
|
|
201
|
|
|
try: |
202
|
|
|
if to_account != 'user': |
203
|
|
|
pool = Pool.from_id(to_account.split('-')[1]) |
204
|
|
|
if pool.enabled == False: |
205
|
|
|
print("to_account:", to_account) |
206
|
|
|
raise NotImplementedError |
207
|
|
|
if pool.owner != request.user.id: |
208
|
|
|
if pool not in map(lambda pu: getattr(pu, 'pool'), request.user.pools): |
209
|
|
|
print("to_account:", to_account) |
210
|
|
|
raise NotImplementedError |
211
|
|
|
except Exception as e: |
212
|
|
|
traceback.print_exc() |
213
|
|
|
request.session.flash('Unexpected error processing transaction. Card NOT charged.', 'error') |
214
|
|
|
return HTTPFound(location=request.route_url('user_index')) |
215
|
|
|
|
216
|
|
|
post_stripe_payment( |
217
|
|
|
datalayer, |
218
|
|
|
request, |
219
|
|
|
token, |
220
|
|
|
amount, |
221
|
|
|
total_cents, |
222
|
|
|
request.user, |
223
|
|
|
request.user if to_account == 'user' else pool, |
224
|
|
|
) |
225
|
|
|
|
226
|
|
|
return HTTPFound(location=request.route_url('user_index')) |
227
|
|
|
|
228
|
|
|
@view_config(route_name='user_pools', |
229
|
|
|
renderer='templates/user/pools.jinja2', |
230
|
|
|
permission='user') |
231
|
|
|
def user_pools(request): |
232
|
|
|
return {'user': request.user, |
233
|
|
|
'my_pools': Pool.all_by_owner(request.user)} |
234
|
|
|
|
235
|
|
|
|
236
|
|
|
@view_config(route_name='user_pools_new_submit', |
237
|
|
|
request_method='POST', |
238
|
|
|
permission='user') |
239
|
|
|
def user_pools_new_submit(request): |
240
|
|
|
try: |
241
|
|
|
pool_name = request.POST['pool-name'].strip() |
242
|
|
|
if len(pool_name) > 255: |
243
|
|
|
pool_name = pool_name[0:255] |
244
|
|
|
if len(pool_name) < 5: |
245
|
|
|
request.session.flash('Pool names must be at least 5 letters long', 'error') |
246
|
|
|
return HTTPFound(location=request.route_url('user_pools')) |
247
|
|
|
|
248
|
|
|
pool = Pool(request.user, pool_name) |
249
|
|
|
DBSession.add(pool) |
250
|
|
|
DBSession.flush() |
251
|
|
|
|
252
|
|
|
request.session.flash('Pool created.', 'succcess') |
253
|
|
|
return HTTPFound(location=request.route_url('user_pool', pool_id=pool.id)) |
254
|
|
|
|
255
|
|
|
except Exception as e: |
256
|
|
|
if request.debug: raise(e) |
257
|
|
|
request.session.flash('Error creating pool.', 'error') |
258
|
|
|
return HTTPFound(location=request.route_url('user_pools')) |
259
|
|
|
|
260
|
|
|
|
261
|
|
View Code Duplication |
@view_config(route_name='user_pool', |
|
|
|
|
262
|
|
|
renderer='templates/user/pool.jinja2', |
263
|
|
|
permission='user') |
264
|
|
|
def user_pool(request): |
265
|
|
|
try: |
266
|
|
|
pool = Pool.from_id(request.matchdict['pool_id']) |
267
|
|
|
if pool.owner != request.user.id: |
268
|
|
|
request.session.flash('You do not have permission to view that pool.', 'error') |
269
|
|
|
return HTTPFound(location=request.route_url('user_pools')) |
270
|
|
|
|
271
|
|
|
r = transaction_history_queries(request, pool) |
272
|
|
|
r['user'] = request.user |
273
|
|
|
r['pool'] = pool |
274
|
|
|
|
275
|
|
|
return r |
276
|
|
|
except Exception as e: |
277
|
|
|
if request.debug: raise(e) |
278
|
|
|
request.session.flash('Could not load pool.', 'error') |
279
|
|
|
return HTTPFound(location=request.route_url('user_pools')) |
280
|
|
|
|
281
|
|
|
|
282
|
|
|
@view_config(route_name='user_pool_addmember_submit', |
283
|
|
|
request_method='POST', |
284
|
|
|
permission='user') |
285
|
|
|
def user_pool_addmember_submit(request): |
286
|
|
|
try: |
287
|
|
|
pool = Pool.from_id(request.POST['pool-id']) |
288
|
|
|
if pool.owner != request.user.id: |
289
|
|
|
request.session.flash('You do not have permission to view that pool.', 'error') |
290
|
|
|
return HTTPFound(location=request.route_url('user_pools')) |
291
|
|
|
|
292
|
|
|
# Look up the user that is being added to the pool |
293
|
|
|
user = User.from_uniqname(request.POST['uniqname'].strip(), True) |
294
|
|
|
if user == None: |
295
|
|
|
request.session.flash('Could not find that user.', 'error') |
296
|
|
|
return HTTPFound(location=request.route_url('user_pool', pool_id=pool.id)) |
297
|
|
|
|
298
|
|
|
# Can't add yourself |
299
|
|
|
if user.id == pool.owner: |
300
|
|
|
request.session.flash('You cannot add yourself to a pool. By owning the pool you are automatically a part of it.', 'error') |
301
|
|
|
return HTTPFound(location=request.route_url('user_pool', pool_id=pool.id)) |
302
|
|
|
|
303
|
|
|
# Make sure the user isn't already in the pool |
304
|
|
|
for u in pool.users: |
305
|
|
|
if u.user_id == user.id: |
306
|
|
|
request.session.flash('User is already in pool.', 'error') |
307
|
|
|
return HTTPFound(location=request.route_url('user_pool', pool_id=pool.id)) |
308
|
|
|
|
309
|
|
|
# Add the user to the pool |
310
|
|
|
pooluser = PoolUser(pool, user) |
311
|
|
|
DBSession.add(pooluser) |
312
|
|
|
DBSession.flush() |
313
|
|
|
|
314
|
|
|
request.session.flash('{} added to the pool.'.format(user.name), 'succcess') |
315
|
|
|
return HTTPFound(location=request.route_url('user_pool', pool_id=pool.id)) |
316
|
|
|
|
317
|
|
|
except Exception as e: |
318
|
|
|
if request.debug: raise(e) |
319
|
|
|
request.session.flash('Error adding user to pool.', 'error') |
320
|
|
|
return HTTPFound(location=request.route_url('user_pools')) |
321
|
|
|
|
322
|
|
|
|
323
|
|
|
@view_config(route_name='user_pool_changename_submit', |
324
|
|
|
request_method='POST', |
325
|
|
|
permission='user') |
326
|
|
|
def user_pool_changename_submit(request): |
327
|
|
|
try: |
328
|
|
|
pool = Pool.from_id(request.POST['pool-id']) |
329
|
|
|
if pool.owner != request.user.id: |
330
|
|
|
request.session.flash('You do not have permission to view that pool.', 'error') |
331
|
|
|
return HTTPFound(location=request.route_url('user_pools')) |
332
|
|
|
|
333
|
|
|
pool_name = request.POST['newname'].strip() |
334
|
|
|
if len(pool_name) > 255: |
335
|
|
|
pool_name = pool_name[0:255] |
336
|
|
|
if len(pool_name) < 5: |
337
|
|
|
request.session.flash('Pool names must be at least 5 letters long', 'error') |
338
|
|
|
return HTTPFound(location=request.route_url('user_pool', pool_id=int(pool.id))) |
339
|
|
|
|
340
|
|
|
pool.name = pool_name |
341
|
|
|
|
342
|
|
|
request.session.flash('Pool created.', 'succcess') |
343
|
|
|
return HTTPFound(location=request.route_url('user_pool', pool_id=pool.id)) |
344
|
|
|
except Exception as e: |
345
|
|
|
if request.debug: raise(e) |
346
|
|
|
request.session.flash('Error changing pool name.', 'error') |
347
|
|
|
return HTTPFound(location=request.route_url('user_pools')) |
348
|
|
|
|
349
|
|
|
|
350
|
|
|
@view_config(route_name='user_password_edit', |
351
|
|
|
renderer='templates/user/password_edit.jinja2', |
352
|
|
|
permission='user') |
353
|
|
|
def user_password_edit(request): |
354
|
|
|
return {} |
355
|
|
|
|
356
|
|
|
|
357
|
|
View Code Duplication |
@view_config(route_name='user_password_edit_submit', |
|
|
|
|
358
|
|
|
request_method='POST', |
359
|
|
|
permission='user') |
360
|
|
|
def user_password_edit_submit(request): |
361
|
|
|
pwd0 = request.POST['edit-password-0'] |
362
|
|
|
pwd1 = request.POST['edit-password-1'] |
363
|
|
|
if pwd0 != pwd1: |
364
|
|
|
request.session.flash('Error: Passwords do not match', 'error') |
365
|
|
|
return HTTPFound(location=request.route_url('user_password_edit')) |
366
|
|
|
request.user.password = pwd0 |
367
|
|
|
request.session.flash('Password changed successfully.', 'success') |
368
|
|
|
return HTTPFound(location=request.route_url('user_index')) |
369
|
|
|
# check that changing password for actually logged in user |
370
|
|
|
|
371
|
|
|
|
372
|
|
|
|