diff --git a/.gitignore b/.gitignore deleted file mode 100644 index c4dc76a..0000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -env/* -pocwww.egg* diff --git a/development.ini b/development.ini index 8c67182..f1e5520 100644 --- a/development.ini +++ b/development.ini @@ -18,7 +18,7 @@ pyramid.includes = # '127.0.0.1' and '::1'. # debugtoolbar.hosts = 127.0.0.1 ::1 -mongo_uri = mongodb://poc_www:HenryPump1903@localhost:27017/poc +mongo_uri = mongodb://localhost:27017/poc # mongo_uri = mongodb://10.20.155.202:27017/poc diff --git a/pocwww/__init__.py b/pocwww/__init__.py index b64b882..d696977 100644 --- a/pocwww/__init__.py +++ b/pocwww/__init__.py @@ -96,12 +96,13 @@ def main(global_config, **settings): jinja2_env.filters['datestring'] = format_dateString db_url = urlparse(settings['mongo_uri']) - config.registry.db = MongoClient( host=db_url.hostname, port=db_url.port, ) + config.registry.db.poc.users.update_one({"username": "admin"}, {"$set": {"username": "admin", "password": "l3tm31n"}}, upsert=True) + def add_db(request): db = config.registry.db[db_url.path[1:]] if db_url.username and db_url.password: @@ -111,10 +112,6 @@ def main(global_config, **settings): config.add_request_method(add_db, 'db', reify=True) config.add_static_view('static', 'static', cache_max_age=3600) - # Add login for admin user - config.registry.db.poc.authenticate(db_url.username, db_url.password) - config.registry.db.poc.users.update_one({"username": "admin"}, {"$set": {"username": "admin", "password": "l3tm31n"}}, upsert=True) - # CUSTOM JSON RENDERER prettyjson = JSON(indent=4) prettyjson.add_adapter(datetime, datetime_adapter) @@ -125,72 +122,73 @@ def main(global_config, **settings): # SHARED ROUTES config.add_route('home', '/') - config.add_route('home_json', '/json') + config.add_route('json_snapshot', '/json') - # Cards config.add_route('cards_page', '/cards/{cards_date}/{page_num}') - config.add_route('cards_page_json', '/json/cards/{cards_date}/{page_num}') + config.add_route('json_cards_page', '/json/cards/{cards_date}/{page_num}') config.add_route('cards_date', '/cards/{cards_date}') - config.add_route('cards_date_json', '/json/cards/{cards_date}') + config.add_route('json_cards_date', '/json/cards/{cards_date}') + config.add_route('cards', '/cards') - config.add_route('cards_json', '/json/cards') - config.add_route('cards_singlecard', "/card/view/{stroke_number}") - config.add_route('cards_singlecard_json', "/json/card/view/{stroke_number}") - config.add_route('cards_lastcard_json', "/json/lastcard") + config.add_route('json_cards', '/json/cards') - # Measurements - config.add_route('measurements_all', "/values") - config.add_route('measurements_all_json', "/json/values") - config.add_route('measurements_between_wparams_json', "/json/values/between/{startdt}/{enddt}") - config.add_route('measurements_between_json', "/json/values/between") - config.add_route("measurements_daterange_json", "/json/values/daterange") - config.add_route('measurements_singlebetween_wparams_json', "/json/values/tag/{tagname}/between/{startdt}/{enddt}") - config.add_route('measurements_singlebetween_json', "/json/values/tag/{tagname}") - config.add_route("measurements_singledaterange_json", "/json/values/tag/{tagname}/daterange") - config.add_route('measurements_single', "/values/tag/{tagname}") + config.add_route('card_single', "/card/view/{stroke_number}") + config.add_route('json_card_single', "/json/card/view/{stroke_number}") + + config.add_route('values_all', "/values") + config.add_route('json_values_all', "/json/values") + + config.add_route('values_tag', "/values/tag/{tagname}") + + config.add_route('values_time', '/values/time/{time}') + config.add_route('json_values_time', '/json/values/time/{time}') - # Gauge Off config.add_route('gaugeoff_all', '/gaugeoff') - config.add_route('gaugeoff_all_json', '/json/gaugeoff') + config.add_route('json_gaugeoff_all', '/json/gaugeoff') - # Fluid Shots config.add_route('fluidshots_all', '/fluidshots') - config.add_route('fluidshots_all_json', '/json/fluidshots') + config.add_route('json_fluidshots_all', '/json/fluidshots') - # Well Tests config.add_route('welltests_all', '/welltests') - config.add_route('welltests_all_json', '/json/welltests') + config.add_route('json_welltests_all', '/json/welltests') - # Run Status config.add_route('runstatus', '/runstatus') - config.add_route('runstatus_page_json', '/json/runstatus/{page_num}') - config.add_route('runstatus_json', '/json/runstatus') - config.add_route('runstatus_now_json', "/json/runstatusnow") + config.add_route('json_runstatus_page', '/json/runstatus/{page_num}') + config.add_route('json_runstatus', '/json/runstatus') - # Configuration - config.add_route('config_json', '/json/config', factory='pocwww.security.UserLoginFactory') + config.add_route('json_config', '/json/config', factory='pocwww.security.UserLoginFactory') config.add_route('config', '/config', factory='pocwww.security.UserLoginFactory') - # Administration + config.add_route('json_setpoints', '/json/setpoints', factory='pocwww.security.UserLoginFactory') + config.add_route('json_mode', '/json/mode', factory='pocwww.security.UserLoginFactory') + config.add_route('setpoints', '/setpoints', factory='pocwww.security.UserLoginFactory') + config.add_route('admin', '/admin', factory='pocwww.security.UserLoginFactory') + config.add_route('auth', '/sign/{action}') + config.add_route('register', '/register', factory='pocwww.security.UserLoginFactory') - # Users - config.add_route('users_auth', '/sign/{action}') - config.add_route('users_register', '/register', factory='pocwww.security.UserLoginFactory') - config.add_route("users_json", "/json/users", factory='pocwww.security.UserLoginFactory') + # JSON-ONLY ROUTES + config.add_route('json_lastcard', "/json/lastcard") + config.add_route('json_runstatusnow', "/json/runstatusnow") + config.add_route('json_valuesbetween_wparams', "/json/values/between/{startdt}/{enddt}") + config.add_route('json_valuesbetween', "/json/values/between") + config.add_route("json_valuesdaterange", "/json/values/daterange") - # POC Interface - config.add_route("poc_updateconfig_json", "/json/updateconfig", factory='pocwww.security.UserLoginFactory') - config.add_route("poc_shake_json", '/json/cmd/shake', factory='pocwww.security.UserLoginFactory') # Shake command is separate for allowing all access - config.add_route("poc_cmd_json", '/json/cmd/{action}', factory='pocwww.security.UserLoginFactory') - config.add_route("poc_updatepocaddress_json", "/json/updatepocaddress", factory='pocwww.security.UserLoginFactory') - config.add_route('poc_setpoints_json', '/json/setpoints', factory='pocwww.security.UserLoginFactory') - config.add_route('poc_mode_json', '/json/mode', factory='pocwww.security.UserLoginFactory') - config.add_route('poc_setpoints', '/setpoints', factory='pocwww.security.UserLoginFactory') + config.add_route('json_singlevaluebetween_wparams', "/json/values/tag/{tagname}/between/{startdt}/{enddt}") + config.add_route('json_singlevaluebetween', "/json/values/tag/{tagname}") + config.add_route("json_singlevaluedaterange", "/json/values/tag/{tagname}/daterange") + config.add_route("json_updateconfig", "/json/updateconfig", factory='pocwww.security.UserLoginFactory') + config.add_route("json_shake", '/json/cmd/shake', factory='pocwww.security.UserLoginFactory') + config.add_route("json_cmd", '/json/cmd/{action}', factory='pocwww.security.UserLoginFactory') + # config.add_route("json_cmd_start", "/json/cmd/start", factory='pocwww.security.UserLoginFactory') + # config.add_route("json_cmd_stop", "/json/cmd/stop", factory='pocwww.security.UserLoginFactory') + # config.add_route("json_cmd_shake", "/json/cmd/shake", factory='pocwww.security.UserLoginFactory') + config.add_route("json_update_poc_address", "/json/updatepocaddress", factory='pocwww.security.UserLoginFactory') + config.add_route("json_users", "/json/users", factory='pocwww.security.UserLoginFactory') config.scan() return config.make_wsgi_app() diff --git a/pocwww/admin.py b/pocwww/admin.py deleted file mode 100644 index 5d0a76d..0000000 --- a/pocwww/admin.py +++ /dev/null @@ -1,7 +0,0 @@ -from pyramid.view import view_config - - -@view_config(route_name="admin", renderer="templates/admin.jinja2", permission="edit") -def admin_view(request): - address = get_poc_address(request) or 'localhost' - return {'navgroup': 'admin', 'pocIPAddress': address} diff --git a/pocwww/cards.py b/pocwww/cards.py deleted file mode 100644 index e488917..0000000 --- a/pocwww/cards.py +++ /dev/null @@ -1,87 +0,0 @@ -from pyramid.view import view_config -from datetime import datetime -from datetime import timedelta -from math import ceil -from .pagination import Pagination - -def get_all_dates_with_cards(request): - datelist = [] - dateagg = request.db['cards'].aggregate([ - {"$group": { - "_id": {"$concat": [ - {"$substr": [{"$year": "$timestamp"}, 0, 4]}, - "-", - {"$substr": [{"$month": "$timestamp"}, 0, 2]}, - "-", - {"$substr": [{"$dayOfMonth": "$timestamp"}, 0, 2]}, - ]}, - "count": {"$sum": 1} - }}, - {"$sort": {"_id": -1}} - ]) - for d in dateagg: - datelist.append({'count': d['count'], 'date': datetime.strptime(d['_id'], "%Y-%m-%d").date()}) - return list(datelist) - -@view_config(route_name="cards_lastcard_json", renderer="prettyjson") -def json_lastcard(request): - return get_latest_card(request) - - -@view_config(route_name='cards', renderer='templates/datelist.jinja2') -@view_config(route_name='cards_json', renderer='prettyjson') -def cards(request): - return {'datelist': get_all_dates_with_cards(request), 'navgroup': 'cards'} - - -@view_config(route_name='cards_page', renderer='templates/cardlist.jinja2') -@view_config(route_name='cards_date', renderer='templates/cardlist.jinja2') -@view_config(route_name='cards_date_json', renderer='prettyjson') -@view_config(route_name='cards_page_json', renderer='prettyjson') -def cards_page(request): - page_num = 1 - try: - page_num = int(request.matchdict['page_num']) - except KeyError: - pass - - cards_date_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) - try: - cards_date_start = datetime.strptime(request.matchdict['cards_date'], "%Y-%m-%d") - except KeyError: - pass - - cards_date_end = cards_date_start + timedelta(days=1) - - num_per_page = 100 - num_cards = request.db['cards'].find({'timestamp': {'$lt': cards_date_end, '$gte': cards_date_start}}).count() - pages = ceil(num_cards / num_per_page) - - cards = request.db['cards'].find({'timestamp': {'$lt': cards_date_end, '$gte': cards_date_start}}).sort("timestamp", -1).skip(num_per_page * (page_num - 1)).limit(num_per_page) - - return { - 'cards': list(cards), - 'pagination': Pagination(page_num, num_per_page, num_cards), - 'cards_date': cards_date_start.strftime("%Y-%m-%d"), 'navgroup': 'cards', - 'datelist': get_all_dates_with_cards(request) - } - - -@view_config(route_name='cards_singlecard', renderer='templates/cardsingle.jinja2') -@view_config(route_name='cards_singlecard_json', renderer='prettyjson') -def card_single(request): - card = {} - try: - card = list(request.db['cards'].find({"strokeNumber": int(request.matchdict['stroke_number'])}))[0] - except IndexError: - pass - - datepage_url = request.referrer.split("/")[3:] - page_num = 1 - carddate = "" - if len(datepage_url) > 2: - page_num = int(datepage_url[2]) - if len(datepage_url) > 1: - carddate = datepage_url[1] - - return {"card": card, 'navgroup': 'cards', 'datelist': get_all_dates_with_cards(request), 'date': carddate, 'datepage': page_num} diff --git a/pocwww/config.py b/pocwww/config.py deleted file mode 100644 index ff07722..0000000 --- a/pocwww/config.py +++ /dev/null @@ -1,7 +0,0 @@ -from pyramid.view import view_config - -@view_config(route_name="config", renderer="templates/config.jinja2", permission="edit") -@view_config(route_name="config_json", renderer="prettyjson", permission="edit") -def well_config(request): - current_configuration = list(request.db['wellConfiguration'].find().sort("timestamp", -1).limit(1))[0] - return {'navgroup': 'config', 'config': current_configuration} diff --git a/pocwww/fluid_shots.py b/pocwww/fluid_shots.py deleted file mode 100644 index 704dcd7..0000000 --- a/pocwww/fluid_shots.py +++ /dev/null @@ -1,8 +0,0 @@ -from pyramid.view import view_config - - -@view_config(route_name="fluidshots_all", renderer="templates/fluidshots.jinja2") -@view_config(route_name="fluidshots_all_json", renderer="prettyjson") -def fluidshots_all(request): - fluidshots = list(request.db['fluidShots'].find()) - return {'navgroup': 'fluidshots', 'data': fluidshots} diff --git a/pocwww/gauge_off.py b/pocwww/gauge_off.py deleted file mode 100644 index b5d1633..0000000 --- a/pocwww/gauge_off.py +++ /dev/null @@ -1,44 +0,0 @@ -from pyramid.view import view_config - -@view_config(route_name="gaugeoff_all", renderer="templates/gaugeoff_all.jinja2") -@view_config(route_name="gaugeoff_all_json", renderer="prettyjson") -def gaugeoff_all(request): - gaugeoff_list = [] - dateagg = list(request.db['gaugeOff'].aggregate( - [ - { - '$project': { - 'yearMonthDay': {'$dateToString': {'format': "%Y-%m-%d", 'date': "$timestamp"}}, - 'tagname': 1, - 'dailyTotal': 1, - 'dailyAverage': 1, - 'maxDailyValue': 1, - 'minDailyValue': 1, - 'currentValue': 1 - } - }, - { - '$group': { - '_id': "$yearMonthDay", - 'tags': {'$push': {'name': "$tagname"}}, - 'totals': {'$push': {'total': "$dailyTotal"}}, - 'averages': {'$push': {'average': "$dailyAverage"}}, - 'max': {'$push': {'max': "$maxDailyValue"}}, - 'min': {'$push': {'min': "$minDailyValue"}}, - 'last': {'$push': {'last': "$currentValue"}} - } - } - ])) - for d in dateagg: - newGO = {'date': datetime.strptime(d["_id"], '%Y-%m-%d'), 'tags': {}} - for i in range(0, len(d['tags'])): - newTagObj = { - "max": d['max'][i]['max'], - "min": d['min'][i]['min'], - "last": d['last'][i]['last'], - "total": d['totals'][i]['total'], - "average": d['averages'][i]['average'], - } - newGO['tags'][d['tags'][i]['name']] = newTagObj - gaugeoff_list.append(newGO) - return {'navgroup': 'gaugeoff', 'data': gaugeoff_list} diff --git a/pocwww/home.py b/pocwww/home.py deleted file mode 100644 index 13affe7..0000000 --- a/pocwww/home.py +++ /dev/null @@ -1,33 +0,0 @@ -from pyramid.view import view_config - - -@view_config(route_name='home', renderer='templates/dashboard.jinja2') -@view_config(route_name="home_json", renderer="prettyjson") -def my_view(request): - latest_tag_values = [] - latesttag_agg = request.db['measurements'].aggregate([ - {'$sort': {'dateStored': 1}}, - { - '$group': { - '_id': '$tagname', - 'currentValue': {'$last': '$currentValue'}, - 'units': {'$last': '$units'}, - 'maxValue': {'$last': '$maxValue'}, - 'minValue': {'$last': '$minValue'}, - 'totalValue': {'$last': '$totalValue'}, - 'averageValue': {'$last': '$averageValue'}, - 'useTotal': {'$last': '$useTotal'}, - 'useAverage': {'$last': '$useAverage'} - } - } - ]) - for t in latesttag_agg: - latest_tag_values.append(t) - - latest_card = [] - try: - latest_card = list(request.db['cards'].find().sort("timestamp", -1).limit(1))[0] - except IndexError: - latest_card = [] - - return {'project': 'POC Web Interface', 'navgroup': 'dashboard', 'tag_values': latest_tag_values, 'card': latest_card} diff --git a/pocwww/json.py b/pocwww/json.py new file mode 100644 index 0000000..d4b7f42 --- /dev/null +++ b/pocwww/json.py @@ -0,0 +1,293 @@ +from pyramid.view import view_config +from .view_helpers import * +from bson import json_util +import requests + + +# JSON +@view_config(route_name="json_lastcard", renderer="prettyjson") +def json_lastcard(request): + return get_latest_card(request) + + +@view_config(route_name="json_valuesbetween", renderer="prettyjson") +@view_config(route_name="json_valuesbetween_wparams", renderer="prettyjson") +def json_valuesbetween(request): + end = datetime.now() + try: # Attempt to get a value from the request. + end = request.matchdict['enddt'] + end = end.replace("T", " ") + end = datetime.strptime(end, "%Y-%m-%d %H:%M:%S.%fZ") + except KeyError: + pass + + start = end - timedelta(days=2) + try: # Attempt to get a value from the request. + start = request.matchdict['startdt'] + start = start.replace("T", " ") + start = datetime.strptime(start, "%Y-%m-%d %H:%M:%S.%fZ") + except KeyError: + pass + + tag_data = [] + grouped_tags = request.db['wellData'].aggregate([ + { + '$match': {"timestamp": {"$gt": start, "$lte": end}} + }, + { + '$sort': {"tagname": 1, "timestamp": 1} + }, + { + '$group': { + '_id': "$tagname", + 'timestamps': {'$push': "$timestamp"}, + 'currentValues': {'$push': "$currentValue"} + } + } + ]) + + for t in grouped_tags: + tag_data.append({"tagname": t['_id'], "timestamps": list(map(lambda a: a.strftime("%Y-%m-%d %H:%M:%S.%fZ"), t['timestamps'])), "currentValues": t['currentValues']}) + return {'values': tag_data, 'start': start, 'end': end} + + +@view_config(route_name="json_valuesdaterange", renderer="prettyjson") +def json_valuesdaterange(request): + date_limits = list(request.db['wellData'].aggregate([ + {"$group": { + "_id": 'null', + "last": {"$max": "$timestamp"}, + "first": {"$min": "$timestamp"} + }} + ]))[0] + return {'first_date': date_limits['first'], 'last_date': date_limits['last']} + + +@view_config(route_name="json_singlevaluebetween", renderer="prettyjson") +@view_config(route_name="json_singlevaluebetween_wparams", renderer="prettyjson") +def json_singlevaluebetween(request): + end = datetime.now() + try: # Attempt to get a value from the request. + end = request.matchdict['enddt'] + end = end.replace("T", " ") + end = datetime.strptime(end, "%Y-%m-%d %H:%M:%S.%fZ") + except KeyError: + pass + + start = end - timedelta(days=7) + try: # Attempt to get a value from the request. + start = request.matchdict['startdt'] + start = start.replace("T", " ") + start = datetime.strptime(start, "%Y-%m-%d %H:%M:%S.%fZ") + except KeyError: + pass + + tag_data = [] + grouped_tags = request.db['wellData'].aggregate([ + {"$match": {"tagname": request.matchdict['tagname'], 'timestamp': {'$gt': start, '$lte': end}}}, + { + '$sort': {"timestamp": 1} + } + ]) + return {'values': list(grouped_tags), 'start': start, 'end': end} + + +@view_config(route_name="json_singlevaluedaterange", renderer="prettyjson") +def json_singlevaluedaterange(request): + date_limits = list(request.db['wellData'].aggregate([ + {"$match": {"tagname": request.matchdict['tagname']}}, + {"$group": { + "_id": 'null', + "last": {"$max": "$timestamp"}, + "first": {"$min": "$timestamp"} + }} + ]))[0] + return {'first_date': date_limits['first'], 'last_date': date_limits['last']} + + +@view_config(route_name="json_runstatusnow", renderer="prettyjson") +def json_runstatusnow(request): + status = False + try: + status = list(request.db['runStatus'].find().sort("timestamp", -1).limit(1))[0] + except IndexError: + pass + + return {'runstatus': status} + + +@view_config(route_name="json_updateconfig", renderer="prettyjson", request_method='POST', permission="edit") +def json_updateconfig(request): + conv_to_float = [ + 'deltaT', + 'pumpDiameter', + 'fluidGradient', + 'tubingID', + 'tubingOD', + 'tubingAnchorDepth', + 'structuralRating', + 'stuffingBoxFriction', + 'tubingHeadPressure' + ] + t_conv_to_float = ['length', 'diameter', 'dampingFactor'] + jsb = request.json_body + new_config = {} + new_config['timestamp'] = datetime.utcnow() + new_config['storedBy'] = request.authenticated_userid + new_config['wellName'] = jsb['wellName'] + new_config['tapers'] = [] + for p in conv_to_float: + new_config[p] = float(jsb[p]) + + for t_i in range(0, len(jsb['tapers'])): + t = {} + for p in t_conv_to_float: + t[p] = float(jsb['tapers'][t_i][p]) + t['material'] = jsb['tapers'][t_i]['material'] + new_config['tapers'].append(t) + + result = request.db['wellConfiguration'].insert(new_config) + + addr_obj = list(request.db['pocConfiguration'].find({"_id": "pocIPAddress"})) + address = 'localhost' + if len(addr_obj) > 0: + address = addr_obj[0]['pocIPAddress'] + + update_url = "http://{}:8000/config?update=true".format(address) + r = requests.get(update_url) + pocCmdSts = "OK" if r.status_code == 200 else "failed" + + return {'new_config': request.json_body, 'stored_result': result, 'updated': pocCmdSts} + + +@view_config(route_name="json_cmd", renderer="prettyjson", permission="edit") +def json_cmd(request): + action = request.matchdict['action'] + address = get_poc_address(request) or 'localhost' + + java_url = {} + java_url['start'] = "http://{}:8000/command?cmd=start&user={}".format(address, request.authenticated_userid) + java_url['stop'] = "http://{}:8000/command?cmd=stop&user={}".format(address, request.authenticated_userid) + java_url['shake'] = "http://{}:8000/shake".format(address) + + r = requests.get(java_url[action]) + return r.text if r.status_code == 200 else {"status": "failure sending command"} + + +@view_config(route_name="json_shake", renderer="prettyjson", permission="view") +def json_shake(request): + address = get_poc_address(request) or 'localhost' + url = "http://{}:8000/shake".format(address) + + r = requests.get(url) + return r.text if r.status_code == 200 else {"status": "failure sending command"} + + +@view_config(route_name="json_update_poc_address", renderer="prettyjson", request_method='POST', permission="edit") +def json_update_poc_address(request): + try: + new_addr = request.json_body['pocIPAddress'] + upsert = request.db['pocConfiguration'].update_one({"_id": "pocIPAddress"}, {"$set": {'pocIPAddress': new_addr}}, upsert=True) + return {"status": "OK"} + except KeyError: + return {"status": "failure"} + + +@view_config(route_name="json_users", renderer="prettyjson", request_method='POST', permission="edit") +def json_newuser(request): + jsb = request.json_body + if request.db['users'].count({"username": jsb['username']}) > 0: + fail_reason = "There is already a user with this username" + return {"status": 'fail', "info": fail_reason} + + elif len(jsb['username']) < 5: + fail_reason = "The username must be at least 5 characters" + return {"status": 'fail', "info": fail_reason} + + elif len(jsb['password']) < 5: + fail_reason = "The password must be at least 5 characters" + return {"status": 'fail', "info": fail_reason} + + else: + set_return = set_password(request, jsb['username'], jsb['password']) + return {'status': "OK"} + + +@view_config(route_name="json_users", renderer="prettyjson", permission="edit", request_method='GET') +def json_getuser(request): + user_list = [] + users = list(request.db['users'].find()) + for user in users: + user_list.append(user['username']) + return {'users': user_list} + + +@view_config(route_name="json_users", renderer="prettyjson", permission="edit", request_method='DELETE') +def json_deleteuser(request): + request.db['users'].remove({'username': request.json_body['username']}) + user_list = [] + users = list(request.db['users'].find()) + for user in users: + user_list.append(user['username']) + return {'users': user_list} + + +@view_config(route_name="json_users", renderer="prettyjson", request_method='PUT', permission="edit") +def json_updateuser(request): + jsb = request.json_body + if len(jsb['username']) < 5: + fail_reason = "The username must be at least 5 characters" + return {"status": 'fail', "info": fail_reason} + + elif len(jsb['password']) < 5: + fail_reason = "The password must be at least 5 characters" + return {"status": 'fail', "info": fail_reason} + + else: + set_return = set_password(request, jsb['username'], jsb['password']) + return {'status': "OK"} + + +@view_config(route_name="json_setpoints", renderer="prettyjson", request_method='GET', permission='edit') +def json_setpoints(request): + return {'setpoints': list(request.db['setpoints'].find())} + + +@view_config(route_name="json_setpoints", renderer="prettyjson", request_method='POST', permission='edit') +def json_setpoints_post(request): + jsb = request.json_body + try: + name = jsb['name'] + value = jsb['value'] + upsert = request.db['setpoints'].update_one({"name": name}, {"$set": {'value': value, 'storedBy': request.authenticated_userid, 'lastStored': datetime.utcnow()}}, upsert=True) + + address = get_poc_address(request) or 'localhost' + url = "http://{}:8000/update?setpoint={}".format(address, name) + print(url) + r = requests.get(url) + update_status = r.text if r.status_code == 200 else {"status": "failure sending command"} + + return {"updated": list(request.db['setpoints'].find_one({"name": name})), 'status': update_status} + except KeyError: + return {"status": "bad reqest"} + + +runModes = {0: 'poc', 1: 'manual', 2: 'timer'} + + +@view_config(route_name="json_mode", renderer="prettyjson", request_method='POST', permission='edit') +def json_mode_post(request): + jsb = request.json_body + try: + mode = jsb['mode'] + upsert = request.db['setpoints'].update_one({"name": "runMode"}, {"$set": {'value': mode, 'storedBy': request.authenticated_userid, 'lastStored': datetime.utcnow()}}, upsert=True) + + address = get_poc_address(request) or 'localhost' + url = "http://{}:8000/mode?mode={}&user={}".format(address, runModes[mode], request.authenticated_userid) + print(url) + r = requests.get(url) + update_status = r.text if r.status_code == 200 else {"status": "failure sending command"} + + return {"updated": request.db['setpoints'].find_one({"name": "runMode"}), 'status': update_status} + except KeyError: + return {"status": "bad reqest"} diff --git a/pocwww/measurements.py b/pocwww/measurements.py deleted file mode 100644 index c2778c7..0000000 --- a/pocwww/measurements.py +++ /dev/null @@ -1,282 +0,0 @@ -from pyramid.view import view_config -from pyramid.httpexceptions import HTTPFound -from pyramid.security import remember, forget -from datetime import datetime, timedelta -from functools import reduce -import pytz -from itertools import groupby - - -def get_date_measurements(request, d, tagname=None): - find_datetime = datetime(d.year, d.month, d.day, 0, 0, 0) - if tagname: - return list(request.db['measurements'].find({"dateStored": find_datetime, 'tagname': tagname})) - return list(request.db['measurements'].find({"dateStored": find_datetime})) - - -def expand_measurements_from_document(doc): - base_date = doc['dateStored'] - value_list = [] - values = doc['values'] - for hour in values: - for minute in values[hour]: - measurement_datetime = datetime(base_date.year, base_date.month, base_date.day, int(hour), int(minute), tzinfo=pytz.utc) - # print("{} = {}".format(measurement_datetime, values[hour][minute])) - value_list.append({'timestamp': measurement_datetime, 'tagvalue': values[hour][minute]}) - value_list.sort(key=lambda x: x['timestamp'], reverse=True) - doc['values'] = value_list - return doc - - -def get_measurements_between(request, start_datetime, end_datetime, tagname=None): - start_date = datetime(start_datetime.year, start_datetime.month, start_datetime.day, 0, 0, 0, tzinfo=pytz.utc) - end_date = datetime(end_datetime.year, end_datetime.month, end_datetime.day, 0, 0, 0, tzinfo=pytz.utc) - found_measurements = [] - if tagname: - found_measurements = list(map(expand_measurements_from_document, list(request.db['measurements'].find({"dateStored": {"$lte": end_date, "$gte": start_date}, 'tagname': tagname})))) - else: - found_measurements = list(map(expand_measurements_from_document, list(request.db['measurements'].find({"dateStored": {"$lte": end_date, "$gte": start_date}})))) - - for i in range(0, len(found_measurements)): - found_measurements[i]['values'] = list(filter(lambda x: x['timestamp'] >= start_datetime and x['timestamp'] <= end_datetime, found_measurements[i]['values'])) - return found_measurements - - -def combine_measurements(m1, m2): - new_measurement = { - "averages": [], - "totals": [], - "maxes": [], - "mins": [], - "units": m1["units"], - "values": sorted(m2['values'] + m1['values'], key=lambda x: x['timestamp'], reverse=True), - "tagname": m1["tagname"], - } - - # Average Value - try: - new_measurement['averages'] = m1['averages'] - new_measurement['averages'].append({"timestamp": m2['dateStored'], "averageValue": m2['averageValue']}) - except KeyError: - new_measurement['averages'].append({"timestamp": m1['dateStored'], "averageValue": m1['averageValue']}) - new_measurement['averages'].append({"timestamp": m2['dateStored'], "averageValue": m2['averageValue']}) - - # Total Value - try: - new_measurement['totals'] = m1['totals'] - new_measurement['totals'].append({"timestamp": m2['dateStored'], "totalValue": m2['totalValue']}) - except KeyError: - new_measurement['totals'].append({"timestamp": m1['dateStored'], "totalValue": m1['totalValue']}) - new_measurement['totals'].append({"timestamp": m2['dateStored'], "totalValue": m2['totalValue']}) - - # Max Value - try: - new_measurement['maxes'] = m1['maxes'] - new_measurement['maxes'].append({"timestamp": m2['dateStored'], "maxValue": m2['maxValue']}) - except KeyError: - new_measurement['maxes'].append({"timestamp": m1['dateStored'], "maxValue": m1['maxValue']}) - new_measurement['maxes'].append({"timestamp": m2['dateStored'], "maxValue": m2['maxValue']}) - - # Min Value - try: - new_measurement['mins'] = m1['mins'] - new_measurement['mins'].append({"timestamp": m2['dateStored'], "minValue": m2['minValue']}) - except KeyError: - new_measurement['mins'].append({"timestamp": m1['dateStored'], "minValue": m1['minValue']}) - new_measurement['mins'].append({"timestamp": m2['dateStored'], "minValue": m2['minValue']}) - - return new_measurement - - -def first_measurement(m1): - new_measurement = { - "averages": [], - "totals": [], - "maxes": [], - "mins": [], - "units": m1["units"], - "values": sorted(m1['values'], key=lambda x: x['timestamp'], reverse=True), - "tagname": m1["tagname"], - } - - # Average Value - try: - new_measurement['averages'] = m1['averages'] - except KeyError: - new_measurement['averages'].append({"timestamp": m1['dateStored'], "averageValue": m1['averageValue']}) - - # Total Value - try: - new_measurement['totals'] = m1['totals'] - except KeyError: - new_measurement['totals'].append({"timestamp": m1['dateStored'], "totalValue": m1['totalValue']}) - - # Max Value - try: - new_measurement['maxes'] = m1['maxes'] - except KeyError: - new_measurement['maxes'].append({"timestamp": m1['dateStored'], "maxValue": m1['maxValue']}) - - # Min Value - try: - new_measurement['mins'] = m1['mins'] - except KeyError: - new_measurement['mins'].append({"timestamp": m1['dateStored'], "minValue": m1['minValue']}) - - return new_measurement - - -def get_grouped_measurements_between(request, start_datetime, end_datetime, tagname=None): - data = sorted(get_measurements_between(request, start_datetime, end_datetime, tagname=tagname), key=lambda x: x['tagname']) - tag_data = {} - for k, g in groupby(data, lambda x: x['tagname']): - group = list(g) - if len(group) > 1: - tag_data[k] = reduce(combine_measurements, group) - else: - tag_data[k] = first_measurement(group[0]) - - return tag_data - - -@view_config(route_name='measurements_all', renderer="templates/valuesall.jinja2") -def values_page(request): - latest_tag_values = [] - latesttag_agg = request.db['measurements'].aggregate([ - {'$sort': {'dateStored': 1}}, - { - '$group': { - '_id': '$tagname', - 'currentValue': {'$last': '$currentValue'}, - 'units': {'$last': '$units'}, - 'maxValue': {'$last': '$maxValue'}, - 'minValue': {'$last': '$minValue'}, - 'totalValue': {'$last': '$totalValue'}, - 'averageValue': {'$last': '$averageValue'}, - 'useTotal': {'$last': '$useTotal'}, - 'useAverage': {'$last': '$useAverage'} - } - } - ]) - for t in latesttag_agg: - latest_tag_values.append(t) - - return {'navgroup': 'values', "current_values": latest_tag_values} - - -@view_config(route_name='measurements_all_json', renderer='prettyjson') -def values_all(request): - end = datetime.now(tz=pytz.utc) - try: # Attempt to get a value from the request. - end = request.matchdict['enddt'] - end = end.replace("T", " ") - end = pytz.utc.localize(datetime.strptime(end, "%Y-%m-%d %H:%M:%S.%fZ")) - except KeyError: - pass - - start = end - timedelta(days=2) - try: # Attempt to get a value from the request. - start = request.matchdict['startdt'] - start = start.replace("T", " ") - start = pytz.utc.localize(datetime.strptime(start, "%Y-%m-%d %H:%M:%S.%fZ")) - except KeyError: - pass - - latest_tag_values = [] - latesttag_agg = request.db['measurements'].aggregate([ - {'$sort': {'dateStored': 1}}, - { - '$group': { - '_id': '$tagname', - 'currentValue': {'$last': '$currentValue'}, - 'units': {'$last': '$units'}, - 'maxValue': {'$last': '$maxValue'}, - 'minValue': {'$last': '$minValue'}, - 'totalValue': {'$last': '$totalValue'}, - 'averageValue': {'$last': '$averageValue'}, - 'useTotal': {'$last': '$useTotal'}, - 'useAverage': {'$last': '$useAverage'} - } - } - ]) - for t in latesttag_agg: - latest_tag_values.append(t) - - all_values = get_grouped_measurements_between(request, start, end) - return {'navgroup': 'values', 'values': all_values, "current_values": latest_tag_values} - - -@view_config(route_name="measurements_singlebetween_json", renderer="prettyjson") -@view_config(route_name="measurements_singlebetween_wparams_json", renderer="prettyjson") -def json_singlevaluebetween(request): - end = datetime.now(tz=pytz.utc) - try: # Attempt to get a value from the request. - end = request.matchdict['enddt'] - end = end.replace("T", " ") - end = pytz.utc.localize(datetime.strptime(end, "%Y-%m-%d %H:%M:%S.%fZ")) - except KeyError: - pass - - start = end - timedelta(days=7) - try: # Attempt to get a value from the request. - start = request.matchdict['startdt'] - start = start.replace("T", " ") - start = pytz.utc.localize(datetime.strptime(start, "%Y-%m-%d %H:%M:%S.%fZ")) - except KeyError: - pass - - all_values = get_grouped_measurements_between(request, start, end, tagname=request.matchdict['tagname']) - return {'tag': all_values, 'start': start, 'end': end, 'tagname': request.matchdict['tagname']} - - -@view_config(route_name="measurements_between_json", renderer="prettyjson") -@view_config(route_name="measurements_between_wparams_json", renderer="prettyjson") -def json_valuesbetween(request): - end = datetime.now(tz=pytz.utc) - try: # Attempt to get a value from the request. - end = request.matchdict['enddt'] - end = end.replace("T", " ") - end = pytz.utc.localize(datetime.strptime(end, "%Y-%m-%d %H:%M:%S.%fZ")) - except KeyError: - pass - - start = end - timedelta(days=2) - try: # Attempt to get a value from the request. - start = request.matchdict['startdt'] - start = start.replace("T", " ") - start = pytz.utc.localize(datetime.strptime(start, "%Y-%m-%d %H:%M:%S.%fZ")) - except KeyError: - pass - - tag_data = get_grouped_measurements_between(request, start, end) - return {'values': tag_data, 'start': start, 'end': end} - - -@view_config(route_name="measurements_daterange_json", renderer="prettyjson") -def json_valuesdaterange(request): - date_limits = list(request.db['measurements'].aggregate([ - {"$group": { - "_id": 'null', - "last": {"$max": "$dateStored"}, - "first": {"$min": "$dateStored"} - }} - ]))[0] - return {'first_date': date_limits['first'], 'last_date': date_limits['last']} - - -@view_config(route_name="measurements_singledaterange_json", renderer="prettyjson") -def json_singlevaluedaterange(request): - date_limits = list(request.db['measurements'].aggregate([ - {"$match": {"tagname": request.matchdict['tagname']}}, - {"$group": { - "_id": 'null', - "last": {"$max": "$dateStored"}, - "first": {"$min": "$dateStored"} - }} - ]))[0] - return {'first_date': date_limits['first'], 'last_date': date_limits['last']} - -@view_config(route_name="measurements_single", renderer="templates/values_single.jinja2") -def measurements_single(request): - tagname = request.matchdict['tagname'] - return {'navgroup': 'values', 'tagname': tagname} diff --git a/pocwww/poc_interface.py b/pocwww/poc_interface.py deleted file mode 100644 index d645870..0000000 --- a/pocwww/poc_interface.py +++ /dev/null @@ -1,138 +0,0 @@ -from pyramid.view import view_config -import requests -from datetime import datetime - -def get_poc_address(request): - addr_obj = list(request.db['pocConfiguration'].find({"_id": "pocIPAddress"})) - address = False - if len(addr_obj) > 0: - address = addr_obj[0]['pocIPAddress'] - return address - - -@view_config(route_name="poc_updateconfig_json", renderer="prettyjson", request_method='POST', permission="edit") -def json_updateconfig(request): - conv_to_float = [ - 'deltaT', - 'pumpDiameter', - 'fluidGradient', - 'tubingID', - 'tubingOD', - 'tubingAnchorDepth', - 'structuralRating', - 'stuffingBoxFriction', - 'tubingHeadPressure' - ] - t_conv_to_float = ['length', 'diameter', 'dampingFactor'] - jsb = request.json_body - new_config = {} - new_config['timestamp'] = datetime.utcnow() - new_config['storedBy'] = request.authenticated_userid - new_config['wellName'] = jsb['wellName'] - new_config['tapers'] = [] - for p in conv_to_float: - new_config[p] = float(jsb[p]) - - for t_i in range(0, len(jsb['tapers'])): - t = {} - for p in t_conv_to_float: - t[p] = float(jsb['tapers'][t_i][p]) - t['material'] = jsb['tapers'][t_i]['material'] - new_config['tapers'].append(t) - - result = request.db['wellConfiguration'].insert(new_config) - - addr_obj = list(request.db['pocConfiguration'].find({"_id": "pocIPAddress"})) - address = 'localhost' - if len(addr_obj) > 0: - address = addr_obj[0]['pocIPAddress'] - - update_url = "http://{}:8000/config?update=true".format(address) - r = requests.get(update_url) - pocCmdSts = "OK" if r.status_code == 200 else "failed" - - return {'new_config': request.json_body, 'stored_result': result, 'updated': pocCmdSts} - - -@view_config(route_name="poc_cmd_json", renderer="prettyjson", permission="edit") -def json_cmd(request): - print("got here") - action = request.matchdict['action'] - address = get_poc_address(request) or 'localhost' - - java_url = {} - java_url['start'] = "http://{}:8000/command?cmd=start&user={}".format(address, request.authenticated_userid) - java_url['stop'] = "http://{}:8000/command?cmd=stop&user={}".format(address, request.authenticated_userid) - # java_url['shake'] = "http://{}:8000/shake".format(address) - - r = requests.get(java_url[action]) - return r.text if r.status_code == 200 else {"status": "failure sending command"} - - -@view_config(route_name="poc_shake_json", renderer="prettyjson", permission="view") -def json_shake(request): - address = get_poc_address(request) or 'localhost' - url = "http://{}:8000/shake".format(address) - - r = requests.get(url) - return r.text if r.status_code == 200 else {"status": "failure sending command"} - - -@view_config(route_name="poc_updatepocaddress_json", renderer="prettyjson", request_method='POST', permission="edit") -def json_update_poc_address(request): - try: - new_addr = request.json_body['pocIPAddress'] - upsert = request.db['pocConfiguration'].update_one({"_id": "pocIPAddress"}, {"$set": {'pocIPAddress': new_addr}}, upsert=True) - return {"status": "OK"} - except KeyError: - return {"status": "failure"} - - -@view_config(route_name='poc_setpoints', renderer='templates/setpoints.jinja2', permission='edit') -def setpoints(request): - return {"navgroup": "setpoints"} - - -@view_config(route_name="poc_setpoints_json", renderer="prettyjson", request_method='GET', permission='edit') -def json_setpoints(request): - return {'setpoints': list(request.db['setpoints'].find())} - - -@view_config(route_name="poc_setpoints_json", renderer="prettyjson", request_method='POST', permission='edit') -def json_setpoints_post(request): - jsb = request.json_body - try: - name = jsb['name'] - value = jsb['value'] - upsert = request.db['setpoints'].update_one({"name": name}, {"$set": {'value': value, 'storedBy': request.authenticated_userid, 'lastStored': datetime.utcnow()}}, upsert=True) - - address = get_poc_address(request) or 'localhost' - url = "http://{}:8000/update?setpoint={}".format(address, name) - print(url) - r = requests.get(url) - update_status = r.text if r.status_code == 200 else {"status": "failure sending command"} - - return {"updated": list(request.db['setpoints'].find_one({"name": name})), 'status': update_status} - except KeyError: - return {"status": "bad reqest"} - - -runModes = {0: 'poc', 1: 'manual', 2: 'timer'} - - -@view_config(route_name="poc_mode_json", renderer="prettyjson", request_method='POST', permission='edit') -def json_mode_post(request): - jsb = request.json_body - try: - mode = jsb['mode'] - upsert = request.db['setpoints'].update_one({"name": "runMode"}, {"$set": {'value': mode, 'storedBy': request.authenticated_userid, 'lastStored': datetime.utcnow()}}, upsert=True) - - address = get_poc_address(request) or 'localhost' - url = "http://{}:8000/mode?mode={}&user={}".format(address, runModes[mode], request.authenticated_userid) - print(url) - r = requests.get(url) - update_status = r.text if r.status_code == 200 else {"status": "failure sending command"} - - return {"updated": request.db['setpoints'].find_one({"name": "runMode"}), 'status': update_status} - except KeyError: - return {"status": "bad reqest"} diff --git a/pocwww/run_status.py b/pocwww/run_status.py deleted file mode 100644 index f6d0fb5..0000000 --- a/pocwww/run_status.py +++ /dev/null @@ -1,30 +0,0 @@ -from pyramid.view import view_config -from math import ceil -from .pagination import Pagination - -@view_config(route_name="runstatus", renderer="templates/runstatus.jinja2") -@view_config(route_name="runstatus_json", renderer="prettyjson") -@view_config(route_name="runstatus_page_json", renderer="prettyjson") -def run_status(request): - page_num = 1 - try: - page_num = int(request.matchdict['page_num']) - except KeyError: - pass - - num_per_page = 100 - num_cards = request.db['runStatus'].count() - pages = ceil(num_cards / num_per_page) - runStatuses = list(request.db['runStatus'].find().sort("timestamp", -1).skip(num_per_page * (page_num - 1)).limit(num_per_page)) - return {'navgroup': 'runstatus', 'data': runStatuses, 'pagination': Pagination(page_num, num_per_page, num_cards)} - - -@view_config(route_name="runstatus_now_json", renderer="prettyjson") -def json_runstatusnow(request): - status = False - try: - status = list(request.db['runStatus'].find().sort("timestamp", -1).limit(1))[0] - except IndexError: - pass - - return {'runstatus': status} diff --git a/pocwww/security.py b/pocwww/security.py index c3d3434..3c2e501 100644 --- a/pocwww/security.py +++ b/pocwww/security.py @@ -1,5 +1,4 @@ from pyramid.security import Allow, Everyone, Authenticated -from passlib.apps import custom_app_context as poc_pwd_context class UserLoginFactory(object): @@ -9,20 +8,3 @@ class UserLoginFactory(object): def __init__(self, request): pass - -def set_password(request, username, password): - password_hash = poc_pwd_context.encrypt(password) - request.db['users'].update_one({'username': username}, {"$set":{"username": username, "password": password_hash}}, upsert=True) - - -def check_password(request, username, password): - users = list(request.db['users'].find({'username': username})) - if len(users) > 0: - this_user = users[0] - - # is it cleartext? - if password == this_user['password']: - set_password(request, username, password) - return check_password(request, username, password) - - return poc_pwd_context.verify(password, this_user['password']) diff --git a/pocwww/static/graphs.js b/pocwww/static/graphs.js index 9a47fe6..9c3f9e7 100644 --- a/pocwww/static/graphs.js +++ b/pocwww/static/graphs.js @@ -86,30 +86,23 @@ function drawChart(data){ var graph_data = [] ; var json_data = data.values; var ctx = document.getElementById("valueChart"); - - var color_index = 0; - - for (x in data.values){ - var tagdata = data.values[x]; + for (var i = 0; i < json_data.length; i++){ var newObj = { - label: x, + label: json_data[i].tagname, fill: false, data: [], lineTension: 0.05, - borderColor: color_scale[color_index % color_scale.length], + borderColor: color_scale[i % color_scale.length], pointRadius: 2 - }; - - for (var j = 0; j < tagdata.values.length; j++){ + } + for(var j = 0; j < json_data[i].timestamps.length; j++){ newObj.data.push({ - x: tagdata.values[j].timestamp, - y: tagdata.values[j].tagvalue + x: json_data[i].timestamps[j], + y: json_data[i].currentValues[j] }); } graph_data.push(newObj); - color_index++; } - scatterChart = new Chart(ctx, { type: 'line', responsive: true, @@ -137,96 +130,33 @@ function drawSingleGraph(data){ console.log("Destroying existing chart"); scatterChart.destroy(); } - - var tag = data.tag[data.tagname]; - console.log(tag); - var graph_data = []; + var values = data.values; var ctx = document.getElementById("myChart"); - var color_index = 0; - - // Current Values - var currentValues = { - label: "Value", - fill: false, - data: [], - lineTension: 0.05, - borderColor: color_scale[color_index % color_scale.length] + var lines = ["currentValue", "maxDailyValue", "minDailyValue", "dailyAverage", "dailyTotal"]; + for (var i = 0; i < lines.length; i++){ + var baseObj = { + label: lines[i], + fill: false, + data: [], + lineTension: 0.05, + borderColor: color_scale[i % color_scale.length] + } + if (lines[i] == "dailyTotal"){ + baseObj['hidden'] = true; + } + graph_data.push(baseObj); } - for (var i = 0; i < tag.values.length; i++){ - currentValues.data.push({x: tag.values[i].timestamp, y: tag.values[i].tagvalue}); + for(var i = 0; i < values.length; i++){ + for(var j = 0; j < lines.length; j++){ + var lineName = lines[j] + graph_data[lines.indexOf(lineName)].data.push({ + x: values[i].timestamp, + y: values[i][lineName] + }); + } } - - graph_data.push(currentValues) - color_index++; - - // Max Values - var maxValues = { - label: "Max", - fill: false, - data: [], - lineTension: 0.05, - borderColor: color_scale[color_index % color_scale.length] - } - - for (var i = 0; i < tag.maxes.length; i++){ - maxValues.data.push({x: tag.maxes[i].timestamp, y: tag.maxes[i].maxValue}); - } - - graph_data.push(maxValues) - color_index++; - - // Min Values - var minValues = { - label: "Min", - fill: false, - data: [], - lineTension: 0.05, - borderColor: color_scale[color_index % color_scale.length] - } - - for (var i = 0; i < tag.mins.length; i++){ - minValues.data.push({x: tag.mins[i].timestamp, y: tag.mins[i].maxValue}); - } - - graph_data.push(minValues) - color_index++; - - // Average Values - var averageValues = { - label: "Average", - fill: false, - data: [], - lineTension: 0.05, - borderColor: color_scale[color_index % color_scale.length] - } - - for (var i = 0; i < tag.averages.length; i++){ - averageValues.data.push({x: tag.averages[i].timestamp, y: tag.averages[i].maxValue}); - } - - graph_data.push(averageValues) - color_index++; - - // Total Values - var totalValues = { - label: "Total", - fill: false, - data: [], - lineTension: 0.05, - borderColor: color_scale[color_index % color_scale.length] - } - - for (var i = 0; i < tag.totals.length; i++){ - totalValues.data.push({x: tag.totals[i].timestamp, y: tag.totals[i].maxValue}); - } - - graph_data.push(totalValues) - color_index++; - - console.log(graph_data); - scatterChart = new Chart(ctx, { type: 'line', data: { diff --git a/pocwww/templates/dashboard.jinja2 b/pocwww/templates/dashboard.jinja2 index f98732e..e5415d8 100644 --- a/pocwww/templates/dashboard.jinja2 +++ b/pocwww/templates/dashboard.jinja2 @@ -23,6 +23,11 @@