terminal()   F
last analyzed

Complexity

Conditions 19

Size

Total Lines 103

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 2 Features 1
Metric Value
cc 19
dl 0
loc 103
rs 2
c 5
b 2
f 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like terminal() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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.view import view_config, forbidden_view_config
8
9
from pyramid.i18n import TranslationStringFactory, get_localizer
10
_ = TranslationStringFactory('betty')
11
12
from sqlalchemy.sql import func
13
from sqlalchemy.exc import DBAPIError
14
from sqlalchemy.orm.exc import NoResultFound
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.transaction import Transaction, BTCDeposit, PurchaseLineItem
23
from .models.account import Account, VirtualAccount, CashAccount
24
from .models.event import Event
25
from .models.announcement import Announcement
26
from .models.btcdeposit import BtcPendingDeposit
27
from .models.pool import Pool
28
from .models.tag import Tag
29
from .models.ephemeron import Ephemeron
30
from .models.badscan import BadScan
31
32
from .utility import user_password_reset
33
from .utility import send_email
34
35
from pyramid.security import Allow, Everyone, remember, forget
36
37
import chezbetty.datalayer as datalayer
38
import transaction
39
40
import math
41
42
# Custom exception
43
class DepositException(Exception):
44
    pass
45
46
47
# Check for a valid user by UMID.
48
#
49
# Input: form encoded umid=11223344
50
#
51
# Note: will not create new user if this user does not exist.
52
@view_config(route_name='terminal_umid_check',
53
             request_method='POST',
54
             renderer='json',
55
             permission='service')
56
def terminal_umid_check(request):
57
    try:
58
        user = User.from_umid(request.POST['umid'])
59
        if user.role == 'serviceaccount':
60
            # n.b. don't expose a different error path here
61
            raise User.InvalidUserException
62
        return {}
63
    except:
64
        return {'error': get_localizer(request).translate(
65
                    _('mcard-keypad-error',
66
                      default='First time using Betty? You need to swipe your M-Card the first time you log in.'))}
67
68
69
## Main terminal page with purchase/cash deposit.
70
@view_config(route_name='terminal',
71
             renderer='templates/terminal/terminal.jinja2',
72
             permission='service')
73
def terminal(request):
74
    try:
75
        if len(request.matchdict['umid']) != 8:
76
            raise __user.InvalidUserException()
77
78
        with transaction.manager:
79
            user = User.from_umid(request.matchdict['umid'], create_if_never_seen=True)
80
        user = DBSession.merge(user)
81
        if not user.enabled:
82
            request.session.flash(get_localizer(request).translate(_(
83
                'user-not-enabled',
84
                default='User is not enabled. Please contact ${email}.',
85
                mapping={'email':request.registry.settings['chezbetty.email']},
86
                )), 'error')
87
            return HTTPFound(location=request.route_url('index'))
88
89
        # Handle re-instating archived user
90
        if user.archived:
91
            if user.archived_balance != 0:
92
                datalayer.adjust_user_balance(user,
93
                                              user.archived_balance,
94
                                              'Reinstated archived user.',
95
                                              request.user)
96
            user.balance = user.archived_balance
97
            user.archived = False
98
99
        # NOTE TODO (added on 2016/05/14): The "name" field in this temp
100
        # table needs to be terminal specific. That is, if there are multiple
101
        # terminals, items and money shouldn't be able to move between them.
102
103
        # If cash was added before a user was logged in, credit that now
104
        deposit = None
105
        in_flight_deposit = Ephemeron.from_name('deposit')
106
        if in_flight_deposit:
107
            amount = Decimal(in_flight_deposit.value)
108
            deposit = datalayer.deposit(user, user, amount)
109
            DBSession.delete(in_flight_deposit)
110
111
        # For Demo mode:
112
        items = DBSession.query(Item)\
113
                         .filter(Item.enabled == True)\
114
                         .order_by(Item.name)\
115
                         .limit(6).all()
116
117
        # Determine initial wall-of-shame fee (if applicable)
118
        purchase_fee_percent = Decimal(0)
119
        if user.balance <= Decimal('-5.0'):
120
            purchase_fee_percent = 5 + (math.floor((user.balance+5) / -5) * 5)
121
122
        # Figure out if any pools can be used to pay for this purchase
123
        purchase_pools = []
124
        for pool in Pool.all_by_owner(user, True):
125
            if pool.balance > (pool.credit_limit * -1):
126
                purchase_pools.append(pool)
127
128
        for pu in user.pools:
129
            if pu.pool.enabled and pu.pool.balance > (pu.pool.credit_limit * -1):
130
                purchase_pools.append(pu.pool)
131
132
        # Get the list of tags that have items without barcodes in them
133
        tags_with_nobarcode_items = Tag.get_tags_with_nobarcode_items();
134
135
        # Add any pre-scanned items
136
        cart_html = ''
137
        cart = Ephemeron.from_name('cart')
138
        if cart:
139
            barcodes = cart.value.split(',')
140
            barcode_dict = {}
141
            # Group barcodes if an item was scanned more than once
142
            for barcode in barcodes:
143
                if barcode in barcode_dict:
144
                    barcode_dict[barcode] += 1
145
                else:
146
                    barcode_dict[barcode] = 1
147
148
            for barcode, quantity in barcode_dict.items():
149
                item = get_item_from_barcode(barcode)
150
151
                if type(item) is Item:
152
                    cart_html += render('templates/terminal/purchase_item_row.jinja2', {'item': item,
153
                                                                                        'quantity': quantity})
154
155
            # Remove temporary cart now that we've shown it to the user
156
            DBSession.delete(cart)
157
158
        return {'user': user,
159
                'items': items,
160
                'purchase_pools': purchase_pools,
161
                'purchase_fee_percent': purchase_fee_percent,
162
                'tags_with_nobarcode_items': tags_with_nobarcode_items,
163
                'nobarcode_notag_items': Item.get_nobarcode_notag_items(),
164
                'existing_items': cart_html,
165
                'deposit': deposit}
166
167
    except __user.InvalidUserException as e:
168
        request.session.flash(get_localizer(request).translate(_(
169
            'mcard-error',
170
            default='Failed to read M-Card. Please try swiping again.',
171
            )), 'error')
172
        return HTTPFound(location=request.route_url('index'))
173
174
175
## Get all items without barcodes in a tag
176
@view_config(route_name='terminal_purchase_tag',
177
             renderer='json',
178
             permission='service')
179
def terminal_purchase_tag(request):
180
    try:
181
        tag_id = int(request.matchdict['tag_id'])
182
        tag = Tag.from_id(tag_id)
183
    except:
184
        if request.matchdict['tag_id'] == 'other':
185
            tag = {'name': 'other',
186
                   'nobarcode_items': Item.get_nobarcode_notag_items()}
187
        else:
188
            return {'error': 'Unable to parse TAG ID'}
189
190
    item_array = render('templates/terminal/purchase_nobarcode_items.jinja2',
191
                        {'tag': tag})
192
193
    return {'items_html': item_array}
194
195
196
## Add a cash deposit.
197
@view_config(route_name='terminal_deposit',
198
             request_method='POST',
199
             renderer='json',
200
             permission='service')
201
def terminal_deposit(request):
202
    try:
203
        if request.POST['umid'] == '':
204
            # User was not logged in when deposit was made. We store
205
            # this deposit temporarily and give it to the next user who
206
            # logs in.
207
            user = None
208
        else:
209
            user = User.from_umid(request.POST['umid'])
210
        amount = Decimal(request.POST['amount'])
211
        method = request.POST['method']
212
213
        # Can't deposit a negative amount
214
        if amount <= 0.0:
215
            raise DepositException('Deposit amount must be greater than $0.00')
216
217
        # Now check the deposit method. We trust anything that comes from the
218
        # bill acceptor, but double check a manual deposit
219
        if method == 'manual':
220
            # Check if the deposit amount is too great.
221
            if amount > 2.0:
222
                # Anything above $2 is blocked
223
                raise DepositException('Deposit amount of ${:,.2f} exceeds the limit'.format(amount))
224
225
        elif method == 'acceptor':
226
            # Any amount is OK
227
            pass
228
229
        else:
230
            raise DepositException('"{}" is an unknown deposit type'.format(method))
231
232
        # At this point the deposit can go through
233
        ret = {}
234
235
        if user:
236
            deposit = datalayer.deposit(user, user, amount, method != 'manual')
237
            ret['type'] = 'user'
238
            ret['amount'] = float(deposit['amount'])
239
            ret['event_id'] = deposit['event'].id
240
            ret['user_balance'] = float(user.balance)
241
242
        else:
243
            # No one was logged in. Need to save this temporarily
244
            # total_stored = datalayer.temporary_deposit(amount);
245
            # ret['type'] = 'temporary'
246
            # ret['new_amount'] = float(amount)
247
            # ret['total_amount'] = float(total_stored)
248
249
            print('GOT NON-LOGGED IN DEPOSIT')
250
            print('GOT NON-LOGGED IN DEPOSIT')
251
            print('GOT NON-LOGGED IN DEPOSIT')
252
            print('AMOUNT: {}'.format(amount))
253
            print('IGNORING THIS FOR NOW.')
254
            ret['error'] = 'Must be logged in to deposit'
255
256
        return ret
257
258
    except __user.InvalidUserException as e:
259
        request.session.flash('Invalid user error. Please try again.', 'error')
260
        return {'error': 'Error finding user.'}
261
262
    except ValueError as e:
263
        return {'error': 'Error understanding deposit amount.'}
264
265
    except DepositException as e:
266
        return {'error': str(e)}
267
268
    except Exception as e:
269
        return {'error': str(e)}
270
271
272
def get_item_from_barcode(barcode):
273
    try:
274
        item = Item.from_barcode(barcode)
275
    except:
276
        # Could not find the item. Check to see if the user scanned a box
277
        # instead. This could lead to two cases: a) the box only has 1 item in it
278
        # in which case we just add that item to the cart. This likely occurred
279
        # because the individual items do not have barcodes so we just use
280
        # the box. b) The box has multiple items in it in which case we throw
281
        # an error for now.
282
        try:
283
            box = Box.from_barcode(barcode)
284
            if box.subitem_number == 1:
285
                item = box.items[0].item
286
            else:
287
                return 'Cannot add that entire box to your order. Please scan an individual item.'
288
        except:
289
            badscan = BadScan(barcode)
290
            DBSession.add(badscan)
291
            DBSession.flush()
292
293
            return 'Could not find that item.'
294
295
    if not item.enabled:
296
        return 'That product is not currently for sale.'
297
298
    return item
299
300
301
## Add an item to a shopping cart.
302
@view_config(route_name='terminal_item_barcode',
303
             renderer='json',
304
             permission='service')
305
def terminal_item_barcode(request):
306
    item = get_item_from_barcode(request.matchdict['barcode'])
307
308
    if type(item) is str:
309
         return {'error': item}
310
311
    item_html = render('templates/terminal/purchase_item_row.jinja2', {'item': item})
312
    return {'id':item.id,
313
            'item_row_html': item_html}
314
315
316
## Add item to saved list for when a user logs in (when we can add it to
317
## the cart)
318
@view_config(route_name='terminal_saveitem_barcode',
319
             renderer='json',
320
             permission='service')
321
def terminal_saveitem_barcode(request):
322
    try:
323
        cart_barcodes = Ephemeron.add_list('cart', request.matchdict['barcode'])
324
        return {'barcodes': cart_barcodes}
325
    except Exception as e:
326
        if request.debug: raise(e)
327
        return {'error': 'Error.'}
328
329
330
## Add an item to a shopping cart.
331
@view_config(route_name='terminal_item_id',
332
             renderer='json',
333
             permission='service')
334
def terminal_item_id(request):
335
    try:
336
        item = Item.from_id(request.matchdict['item_id'])
337
    except:
338
        return {'error': 'Could not find that item.'}
339
340
    if not item.enabled:
341
        return {'error': 'That product is not currently for sale.'}
342
343
    item_html = render('templates/terminal/purchase_item_row.jinja2', {'item': item})
344
    return {'id': item.id,
345
            'item_row_html': item_html}
346
347
348
## Make a purchase from the terminal.
349
@view_config(route_name='terminal_purchase',
350
             request_method='POST',
351
             renderer='json',
352
             permission='service')
353
def terminal_purchase(request):
354
    try:
355
        user = User.from_umid(request.POST['umid'])
356
357
        ignored_keys = ['umid', 'pool_id']
358
359
        # Bundle all purchase items
360
        items = {}
361
        for item_id,quantity in request.POST.items():
362
            if item_id in ignored_keys:
363
                continue
364
            item = Item.from_id(int(item_id))
365
            items[item] = int(quantity)
366
367
        # What should pay for this?
368
        # Note: should do a bunch of checking to make sure all of this
369
        # is kosher. But given our locked down single terminal, we're just
370
        # going to skip all of that.
371
        if 'pool_id' in request.POST:
372
            pool = Pool.from_id(int(request.POST['pool_id']))
373
            purchase = datalayer.purchase(user, pool, items)
374
        else:
375
            purchase = datalayer.purchase(user, user, items)
376
377
        # Create a order complete view
378
        order = {'total': purchase.amount,
379
                 'discount': purchase.discount,
380
                 'items': []}
381
        for subtrans in purchase.subtransactions:
382
            item = {}
383
            item['name'] = subtrans.item.name
384
            item['quantity'] = subtrans.quantity
385
            item['price'] = subtrans.item.price
386
            item['total_price'] = subtrans.amount
387
            order['items'].append(item)
388
389
        if purchase.fr_account_virt_id == user.id:
390
            account_type = 'user'
391
            pool = None
392
        else:
393
            account_type = 'pool'
394
            pool = Pool.from_id(purchase.fr_account_virt_id)
395
396
        summary = render('templates/terminal/purchase_complete.jinja2',
397
            {'user': user,
398
             'event': purchase.event,
399
             'order': order,
400
             'transaction': purchase,
401
             'account_type': account_type,
402
             'pool': pool})
403
404
        # Return the committed transaction ID
405
        return {'order_table': summary,
406
                'user_balance': float(user.balance)}
407
408
    except __user.InvalidUserException as e:
409
        return {'error': get_localizer(request).translate(_('invalid-user-error',
410
                           default='Invalid user error. Please try again.'))
411
               }
412
413
    except ValueError as e:
414
        return {'error': 'Unable to parse Item ID or quantity'}
415
416
    except NoResultFound as e:
417
        # Could not find an item
418
        return {'error': 'Unable to identify an item.'}
419
420
421
## Delete a just completed purchase.
422
@view_config(route_name='terminal_purchase_delete',
423
             request_method='POST',
424
             renderer='json',
425
             permission='service')
426
def terminal_purchase_delete(request):
427
    try:
428
        user = User.from_umid(request.POST['umid'])
429
        old_event = Event.from_id(request.POST['old_event_id'])
430
431
        if old_event.type != 'purchase' or \
432
           old_event.transactions[0].type != 'purchase' or \
433
           (old_event.transactions[0].fr_account_virt_id != user.id and \
434
            old_event.user_id != user.id):
435
           # Something went wrong, can't undo this purchase
436
           raise DepositException('Cannot undo that purchase')
437
438
        # Now undo old deposit
439
        datalayer.undo_event(old_event, user)
440
441
        return {'user_balance': float(user.balance)}
442
443
    except __user.InvalidUserException as e:
444
        return {'error': 'Invalid user error. Please try again.'}
445
446
    except DepositException as e:
447
        return {'error': str(e)}
448
449
450