From 072974c8c579772ecaaed750f19947888217b9bc Mon Sep 17 00:00:00 2001 From: Patrick McDonagh Date: Thu, 9 Mar 2017 18:12:18 -0600 Subject: [PATCH] Initial Commit after moving from POC-Java repo --- .coveragerc | 3 + CHANGES.txt | 4 + MANIFEST.in | 2 + README.txt | 29 ++ development.ini | 63 +++ generate_cert.sh | 7 + poc.conf | 46 +++ pocwww/__init__.py | 194 +++++++++ pocwww/json.py | 293 ++++++++++++++ pocwww/pagination.py | 36 ++ pocwww/security.py | 10 + pocwww/static/graphs.js | 280 +++++++++++++ pocwww/static/liquidFillGauge.js | 268 +++++++++++++ pocwww/static/moment.min.js | 551 ++++++++++++++++++++++++++ pocwww/static/pyramid-16x16.png | Bin 0 -> 1319 bytes pocwww/static/pyramid.png | Bin 0 -> 12901 bytes pocwww/static/theme.css | 261 ++++++++++++ pocwww/templates/admin.jinja2 | 59 +++ pocwww/templates/cardlist.jinja2 | 40 ++ pocwww/templates/cardsingle.jinja2 | 24 ++ pocwww/templates/config.jinja2 | 174 ++++++++ pocwww/templates/dashboard.jinja2 | 92 +++++ pocwww/templates/datelist.jinja2 | 23 ++ pocwww/templates/fluidshots.jinja2 | 37 ++ pocwww/templates/gaugeoff_all.jinja2 | 43 ++ pocwww/templates/layout.jinja2 | 194 +++++++++ pocwww/templates/pagination.jinja2 | 41 ++ pocwww/templates/register.jinja2 | 187 +++++++++ pocwww/templates/runstatus.jinja2 | 39 ++ pocwww/templates/setpoints.jinja2 | 127 ++++++ pocwww/templates/values_single.jinja2 | 57 +++ pocwww/templates/valuesall.jinja2 | 94 +++++ pocwww/templates/welltests.jinja2 | 49 +++ pocwww/tests.py | 29 ++ pocwww/view_helpers.py | 108 +++++ pocwww/views.py | 172 ++++++++ production.ini | 66 +++ pytest.ini | 3 + setup.py | 55 +++ supervisor.conf | 27 ++ 40 files changed, 3787 insertions(+) create mode 100644 .coveragerc create mode 100644 CHANGES.txt create mode 100644 MANIFEST.in create mode 100644 README.txt create mode 100644 development.ini create mode 100644 generate_cert.sh create mode 100644 poc.conf create mode 100644 pocwww/__init__.py create mode 100644 pocwww/json.py create mode 100644 pocwww/pagination.py create mode 100644 pocwww/security.py create mode 100644 pocwww/static/graphs.js create mode 100644 pocwww/static/liquidFillGauge.js create mode 100644 pocwww/static/moment.min.js create mode 100644 pocwww/static/pyramid-16x16.png create mode 100644 pocwww/static/pyramid.png create mode 100644 pocwww/static/theme.css create mode 100644 pocwww/templates/admin.jinja2 create mode 100644 pocwww/templates/cardlist.jinja2 create mode 100644 pocwww/templates/cardsingle.jinja2 create mode 100644 pocwww/templates/config.jinja2 create mode 100644 pocwww/templates/dashboard.jinja2 create mode 100644 pocwww/templates/datelist.jinja2 create mode 100644 pocwww/templates/fluidshots.jinja2 create mode 100644 pocwww/templates/gaugeoff_all.jinja2 create mode 100644 pocwww/templates/layout.jinja2 create mode 100644 pocwww/templates/pagination.jinja2 create mode 100644 pocwww/templates/register.jinja2 create mode 100644 pocwww/templates/runstatus.jinja2 create mode 100644 pocwww/templates/setpoints.jinja2 create mode 100644 pocwww/templates/values_single.jinja2 create mode 100644 pocwww/templates/valuesall.jinja2 create mode 100644 pocwww/templates/welltests.jinja2 create mode 100644 pocwww/tests.py create mode 100644 pocwww/view_helpers.py create mode 100644 pocwww/views.py create mode 100644 production.ini create mode 100644 pytest.ini create mode 100644 setup.py create mode 100644 supervisor.conf diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..d292a1e --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[run] +source = pocwww +omit = pocwww/test* diff --git a/CHANGES.txt b/CHANGES.txt new file mode 100644 index 0000000..14b902f --- /dev/null +++ b/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..608cd89 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include pocwww *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2 diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..ff46e9e --- /dev/null +++ b/README.txt @@ -0,0 +1,29 @@ +POC Web Interface +=============================== + +Getting Started +--------------- + +- Change directory into your newly created project. + + cd POC Web Interface + +- Create a Python virtual environment. + + python3 -m venv env + +- Upgrade packaging tools. + + env/bin/pip install --upgrade pip setuptools + +- Install the project in editable mode with its testing requirements. + + env/bin/pip install -e ".[testing]" + +- Run your project's tests. + + env/bin/pytest + +- Run your project. + + env/bin/pserve development.ini diff --git a/development.ini b/development.ini new file mode 100644 index 0000000..f1e5520 --- /dev/null +++ b/development.ini @@ -0,0 +1,63 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:pocwww + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +mongo_uri = mongodb://localhost:27017/poc +# mongo_uri = mongodb://10.20.155.202:27017/poc + + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = *:6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, pocwww + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_pocwww] +level = DEBUG +handlers = +qualname = pocwww + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/generate_cert.sh b/generate_cert.sh new file mode 100644 index 0000000..88f285f --- /dev/null +++ b/generate_cert.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +mkdir -p /root/ssl +FQDN=$(hostname -f) +openssl req -newkey rsa:2048 -nodes -keyout poc.key -x509 -days 365 -out poc.crt -subj "/C=US/ST=Texas/L=Dallas/O=Henry Pump/CN=$FQDN" +mv poc.key /root/ssl/ +mv poc.crt /root/ssl/ diff --git a/poc.conf b/poc.conf new file mode 100644 index 0000000..e8b6a58 --- /dev/null +++ b/poc.conf @@ -0,0 +1,46 @@ +# poc.conf + +upstream pocwww-site { + server 127.0.0.1:5000; + server 127.0.0.1:5001; +} + +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + return 301 https://$host$request_uri; +} + +server { + # listen 80; + + # optional ssl configuration + + listen 443 ssl; + ssl_certificate /root/ssl/poc.crt; + ssl_certificate_key /root/ssl/poc.key; + + # end of optional ssl configuration + + server_name localhost; + + access_log /root/poc_access.log; + + location / { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + client_max_body_size 10m; + client_body_buffer_size 128k; + proxy_connect_timeout 60s; + proxy_send_timeout 90s; + proxy_read_timeout 90s; + proxy_buffering off; + proxy_temp_file_write_size 64k; + proxy_pass http://pocwww-site; + proxy_redirect off; + } +} diff --git a/pocwww/__init__.py b/pocwww/__init__.py new file mode 100644 index 0000000..d696977 --- /dev/null +++ b/pocwww/__init__.py @@ -0,0 +1,194 @@ +from pyramid.config import Configurator +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy +from datetime import datetime, date +from dateutil import tz +from pyramid.renderers import JSON +from bson.objectid import ObjectId +from .pagination import Pagination + + +try: + # for python 2 + from urlparse import urlparse +except ImportError: + # for python 3 + from urllib.parse import urlparse + +from pymongo import MongoClient + +from_zone = tz.tzutc() +to_zone = tz.tzlocal() + + +def format_datetime(inpDate, format='medium'): + inpDate = inpDate.replace(tzinfo=from_zone) + localDate = inpDate.astimezone(to_zone) + if format == 'long': + format = "%A, %B %d %Y at %I:%M:%S %p" + elif format == 'medium': + format = "%a, %b %d %Y %I:%M:%S %p" + elif format == 'short': + format = "%m/%d/%Y %I:%M:%S %p" + return localDate.strftime(format) + + +def format_dateString(inpDate, format='medium'): + iDate = datetime.strptime(inpDate, '%Y-%m-%d') + if format == 'long': + format = "%A, %B %d %Y" + elif format == 'medium': + format = "%a, %b %d %Y" + elif format == 'short': + format = "%m/%d/%Y" + return iDate.strftime(format) + + +def format_date(inpDate, format='medium'): + if format == 'long': + format = "%A, %B %d %Y" + elif format == 'medium': + format = "%a, %b %d %Y" + elif format == 'short': + format = "%m/%d/%Y" + return inpDate.strftime(format) + + +def roundDigits(inpValue, digits=2): + return round(inpValue, digits) + + +# JSON RENDERERS +def datetime_adapter(obj, request): + return obj.strftime("%Y-%m-%d %H:%M:%S.%fZ") + + +def objectId_adapter(obj, request): + return str(obj) + + +def date_adapter(obj, request): + return obj.strftime("%Y-%m-%d") + + +def pagination_adapter(obj, request): + p = { + 'page': obj.page, + 'per_page': obj.per_page, + 'total_count': obj.total_count, + } + return p + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + authentication_policy = AuthTktAuthenticationPolicy('H3nryP7mp') + authorization_policy = ACLAuthorizationPolicy() + config = Configurator(settings=settings, + authentication_policy=authentication_policy, + authorization_policy=authorization_policy) + config.include('pyramid_jinja2') + config.commit() # this is needed or you will get None back on the next line + jinja2_env = config.get_jinja2_environment() + jinja2_env.filters['datetime'] = format_datetime + jinja2_env.filters['date'] = format_date + 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: + db.authenticate(db_url.username, db_url.password) + return db + + config.add_request_method(add_db, 'db', reify=True) + config.add_static_view('static', 'static', cache_max_age=3600) + + # CUSTOM JSON RENDERER + prettyjson = JSON(indent=4) + prettyjson.add_adapter(datetime, datetime_adapter) + prettyjson.add_adapter(date, date_adapter) + prettyjson.add_adapter(ObjectId, objectId_adapter) + prettyjson.add_adapter(Pagination, pagination_adapter) + config.add_renderer('prettyjson', prettyjson) + + # SHARED ROUTES + config.add_route('home', '/') + config.add_route('json_snapshot', '/json') + + config.add_route('cards_page', '/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('json_cards_date', '/json/cards/{cards_date}') + + config.add_route('cards', '/cards') + config.add_route('json_cards', '/json/cards') + + 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}') + + config.add_route('gaugeoff_all', '/gaugeoff') + config.add_route('json_gaugeoff_all', '/json/gaugeoff') + + config.add_route('fluidshots_all', '/fluidshots') + config.add_route('json_fluidshots_all', '/json/fluidshots') + + config.add_route('welltests_all', '/welltests') + config.add_route('json_welltests_all', '/json/welltests') + + config.add_route('runstatus', '/runstatus') + config.add_route('json_runstatus_page', '/json/runstatus/{page_num}') + config.add_route('json_runstatus', '/json/runstatus') + + config.add_route('json_config', '/json/config', factory='pocwww.security.UserLoginFactory') + config.add_route('config', '/config', factory='pocwww.security.UserLoginFactory') + + 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') + + # 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") + + 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/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/pagination.py b/pocwww/pagination.py new file mode 100644 index 0000000..90a4aa2 --- /dev/null +++ b/pocwww/pagination.py @@ -0,0 +1,36 @@ +from math import ceil + + +class Pagination(object): + + def __init__(self, page, per_page, total_count): + self.page = page + self.per_page = per_page + self.total_count = total_count + self.prev_num = page - 1 + self.next_num = page + 1 + + @property + def pages(self): + return int(ceil(self.total_count / float(self.per_page))) + + @property + def has_prev(self): + return self.page > 1 + + @property + def has_next(self): + return self.page < self.pages + + def iter_pages(self, left_edge=2, left_current=2, + right_current=5, right_edge=2): + last = 0 + for num in range(1, self.pages + 1): + if num <= left_edge or \ + (num > self.page - left_current - 1 and + num < self.page + right_current) or \ + num > self.pages - right_edge: + if last + 1 != num: + yield None + yield num + last = num diff --git a/pocwww/security.py b/pocwww/security.py new file mode 100644 index 0000000..3c2e501 --- /dev/null +++ b/pocwww/security.py @@ -0,0 +1,10 @@ +from pyramid.security import Allow, Everyone, Authenticated + + +class UserLoginFactory(object): + __acl__ = [(Allow, Everyone, 'view'), + (Allow, Authenticated, 'control'), + (Allow, Authenticated, 'edit'), ] + + def __init__(self, request): + pass diff --git a/pocwww/static/graphs.js b/pocwww/static/graphs.js new file mode 100644 index 0000000..9c3f9e7 --- /dev/null +++ b/pocwww/static/graphs.js @@ -0,0 +1,280 @@ +var simple_color_scale = ["#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00", "#ffff33", "#a65628", "#f781bf"]; +var color_scale = ['#a6cee3','#1f78b4','#b2df8a','#33a02c','#fb9a99','#e31a1c','#fdbf6f','#ff7f00','#cab2d6','#6a3d9a','#ffff99','#b15928']; + +function drawCards(data){ + var graph_data = [] ; + var json_data = data.card; + var surf = document.getElementById("surfacecard"); + var down = document.getElementById("downholecard"); + + var surfaceCardData = [{ + label: 'Surface Card', + fill: false, + data: [], + lineTension: 0.05, + borderColor: color_scale[2], + pointRadius: 2 + }]; + + var downholeCardData = [{ + label: 'Downhole Card', + fill: false, + data: [], + lineTension: 0.05, + borderColor: color_scale[1], + pointRadius: 2 + }]; + + for (var i = 0; i < json_data['surface_position'].length; i++){ + surfaceCardData[0].data.push({ + x: json_data['surface_position'][i], + y: json_data['surface_load'][i] + }); + } + + for (var i = 0; i < json_data['downhole_position'].length; i++){ + downholeCardData[0].data.push({ + x: json_data['downhole_position'][i], + y: json_data['downhole_load'][i] + }); + } + + var surfaceChart = new Chart(surf, { + type: 'line', + responsive: true, + data: { + datasets: surfaceCardData + }, + options: { + scales: { + xAxes: [{ + type: 'linear', + position: 'bottom' + }] + }, + legend: { + display: false + } + } + }); + + var downholeChart = new Chart(down, { + type: 'line', + responsive: true, + data: { + datasets: downholeCardData + }, + options: { + scales: { + xAxes: [{ + type: 'linear', + position: 'bottom' + }] + }, + legend: { + display: false + } + } + }); +} + +function drawChart(data){ + if (typeof(scatterChart) != "undefined"){ + console.log("Destroying existing chart"); + scatterChart.destroy(); + } + var graph_data = [] ; + var json_data = data.values; + var ctx = document.getElementById("valueChart"); + for (var i = 0; i < json_data.length; i++){ + var newObj = { + label: json_data[i].tagname, + fill: false, + data: [], + lineTension: 0.05, + borderColor: color_scale[i % color_scale.length], + pointRadius: 2 + } + for(var j = 0; j < json_data[i].timestamps.length; j++){ + newObj.data.push({ + x: json_data[i].timestamps[j], + y: json_data[i].currentValues[j] + }); + } + graph_data.push(newObj); + } + scatterChart = new Chart(ctx, { + type: 'line', + responsive: true, + data: { + datasets: graph_data + }, + options: { + scales: { + xAxes: [{ + type: 'time', + position: 'bottom' + }] + }, + legend: { + labels: { + boxWidth: 20 + } + } + } + }); +} + +function drawSingleGraph(data){ + if (typeof(scatterChart) != "undefined"){ + console.log("Destroying existing chart"); + scatterChart.destroy(); + } + var graph_data = []; + var values = data.values; + var ctx = document.getElementById("myChart"); + 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 < 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] + }); + } + } + scatterChart = new Chart(ctx, { + type: 'line', + data: { + datasets: graph_data + }, + options: { + scales: { + xAxes: [{ + type: 'time', + position: 'bottom' + }] + } + } + }); +} + + +function drawAllSlider(data){ + + var date_min = new Date(Date.parse(data.first_date)); + var date_max = new Date(Date.parse(data.last_date)); + console.log(data); + + var min_val = date_min/1000; + var max_val = date_max/1000; + + var endD = new Date(); + var dummyDate = new Date(); + var startD = new Date(dummyDate.setDate(endD.getDate() - 2)); + + + var start_date = startD/1000; + $('#slider-start').html(startD.toString()); + if(start_date < min_val){ + start_date = min_val; + $('#slider-start').html(date_min.toString()); + } + + var end_date = endD/1000; + $('#slider-end').html(startD.toString()); + if(end_date > max_val){ + end_date = max_val; + $('#slider-end').html(date_max.toString()); + } + + $("#slider-range").slider({ + range: true, + min: min_val, + max: max_val, + step: 10, + values: [start_date, end_date], + slide: function (e, ui) { + var dt_cur_from = new Date(ui.values[0]*1000); //.format("yyyy-mm-dd hh:ii:ss"); + $('#slider-start').html(dt_cur_from.toString()); + + var dt_cur_to = new Date(ui.values[1]*1000); //.format("yyyy-mm-dd hh:ii:ss"); + $('#slider-end').html(dt_cur_to.toString()); + }, + stop: function(e, ui){ + var startD = new Date(ui.values[0]*1000); + var endD = new Date(ui.values[1]*1000); + $.ajax({ + dataType: 'json', + url:"/json/values/between/" + startD.toISOString() + "/" + endD.toISOString(), + success: drawChart + }); + } + }); +} + +function drawSingleSlider(data){ + + var date_min = new Date(Date.parse(data.first_date)); + var date_max = new Date(Date.parse(data.last_date)); + console.log(data); + var min_val = date_min/1000; + var max_val = date_max/1000; + + var endD = new Date(); + var dummyDate = new Date(); + var startD = new Date(dummyDate.setDate(endD.getDate() - 2)); + + + var start_date = startD/1000; + $('#slider-start').html(startD.toString()); + if(start_date < min_val){ + start_date = min_val; + $('#slider-start').html(date_min.toString()); + } + + var end_date = endD/1000; + $('#slider-end').html(startD.toString()); + if(end_date > max_val){ + end_date = max_val; + $('#slider-end').html(date_max.toString()); + } + + $("#slider-range").slider({ + range: true, + min: min_val, + max: max_val, + step: 10, + values: [start_date, end_date], + slide: function (e, ui) { + var dt_cur_from = new Date(ui.values[0]*1000); //.format("yyyy-mm-dd hh:ii:ss"); + $('#slider-start').html(dt_cur_from.toString()); + + var dt_cur_to = new Date(ui.values[1]*1000); //.format("yyyy-mm-dd hh:ii:ss"); + $('#slider-end').html(dt_cur_to.toString()); + }, + stop: function(e, ui){ + var startD = new Date(ui.values[0]*1000); + var endD = new Date(ui.values[1]*1000); + $.ajax({ + dataType: 'json', + url: "/json/values/tag/" + tagName + "/between/" + startD.toISOString() + "/" + endD.toISOString(), + success: drawSingleGraph + }); + } + }); +} diff --git a/pocwww/static/liquidFillGauge.js b/pocwww/static/liquidFillGauge.js new file mode 100644 index 0000000..7493713 --- /dev/null +++ b/pocwww/static/liquidFillGauge.js @@ -0,0 +1,268 @@ +/*! + * @license Open source under BSD 2-clause (http://choosealicense.com/licenses/bsd-2-clause/) + * Copyright (c) 2015, Curtis Bratton + * All rights reserved. + * + * Liquid Fill Gauge v1.1 + */ +function liquidFillGaugeDefaultSettings(){ + return { + minValue: 0, // The gauge minimum value. + maxValue: 100, // The gauge maximum value. + circleThickness: 0.05, // The outer circle thickness as a percentage of it's radius. + circleFillGap: 0.05, // The size of the gap between the outer circle and wave circle as a percentage of the outer circles radius. + circleColor: "#178BCA", // The color of the outer circle. + waveHeight: 0.05, // The wave height as a percentage of the radius of the wave circle. + waveCount: 1, // The number of full waves per width of the wave circle. + waveRiseTime: 1000, // The amount of time in milliseconds for the wave to rise from 0 to it's final height. + waveAnimateTime: 18000, // The amount of time in milliseconds for a full wave to enter the wave circle. + waveRise: true, // Control if the wave should rise from 0 to it's full height, or start at it's full height. + waveHeightScaling: true, // Controls wave size scaling at low and high fill percentages. When true, wave height reaches it's maximum at 50% fill, and minimum at 0% and 100% fill. This helps to prevent the wave from making the wave circle from appear totally full or empty when near it's minimum or maximum fill. + waveAnimate: true, // Controls if the wave scrolls or is static. + waveColor: "#178BCA", // The color of the fill wave. + waveOffset: 0, // The amount to initially offset the wave. 0 = no offset. 1 = offset of one full wave. + textVertPosition: .5, // The height at which to display the percentage text withing the wave circle. 0 = bottom, 1 = top. + textSize: 1, // The relative height of the text to display in the wave circle. 1 = 50% + valueCountUp: true, // If true, the displayed value counts up from 0 to it's final value upon loading. If false, the final value is displayed. + displayPercent: true, // If true, a % symbol is displayed after the value. + textColor: "#045681", // The color of the value text when the wave does not overlap it. + waveTextColor: "#A4DBf8" // The color of the value text when the wave overlaps it. + }; +} + +function loadLiquidFillGauge(elementId, value, config) { + if(config == null) config = liquidFillGaugeDefaultSettings(); + + var gauge = d3.select("#" + elementId); + var radius = Math.min(parseInt(gauge.style("width")), parseInt(gauge.style("height")))/2; + var locationX = parseInt(gauge.style("width"))/2 - radius; + var locationY = parseInt(gauge.style("height"))/2 - radius; + var fillPercent = Math.max(config.minValue, Math.min(config.maxValue, value))/config.maxValue; + + var waveHeightScale; + if(config.waveHeightScaling){ + waveHeightScale = d3.scale.linear() + .range([0,config.waveHeight,0]) + .domain([0,50,100]); + } else { + waveHeightScale = d3.scale.linear() + .range([config.waveHeight,config.waveHeight]) + .domain([0,100]); + } + + var textPixels = (config.textSize*radius/2); + var textFinalValue = parseFloat(value).toFixed(2); + var textStartValue = config.valueCountUp?config.minValue:textFinalValue; + var percentText = config.displayPercent?"%":""; + var circleThickness = config.circleThickness * radius; + var circleFillGap = config.circleFillGap * radius; + var fillCircleMargin = circleThickness + circleFillGap; + var fillCircleRadius = radius - fillCircleMargin; + var waveHeight = fillCircleRadius*waveHeightScale(fillPercent*100); + + var waveLength = fillCircleRadius*2/config.waveCount; + var waveClipCount = 1+config.waveCount; + var waveClipWidth = waveLength*waveClipCount; + + // Rounding functions so that the correct number of decimal places is always displayed as the value counts up. + var textRounder = function(value){ return Math.round(value); }; + if(parseFloat(textFinalValue) != parseFloat(textRounder(textFinalValue))){ + textRounder = function(value){ return parseFloat(value).toFixed(1); }; + } + if(parseFloat(textFinalValue) != parseFloat(textRounder(textFinalValue))){ + textRounder = function(value){ return parseFloat(value).toFixed(2); }; + } + + // Data for building the clip wave area. + var data = []; + for(var i = 0; i <= 40*waveClipCount; i++){ + data.push({x: i/(40*waveClipCount), y: (i/(40))}); + } + + // Scales for drawing the outer circle. + var gaugeCircleX = d3.scale.linear().range([0,2*Math.PI]).domain([0,1]); + var gaugeCircleY = d3.scale.linear().range([0,radius]).domain([0,radius]); + + // Scales for controlling the size of the clipping path. + var waveScaleX = d3.scale.linear().range([0,waveClipWidth]).domain([0,1]); + var waveScaleY = d3.scale.linear().range([0,waveHeight]).domain([0,1]); + + // Scales for controlling the position of the clipping path. + var waveRiseScale = d3.scale.linear() + // The clipping area size is the height of the fill circle + the wave height, so we position the clip wave + // such that the it will overlap the fill circle at all when at 0%, and will totally cover the fill + // circle at 100%. + .range([(fillCircleMargin+fillCircleRadius*2+waveHeight),(fillCircleMargin-waveHeight)]) + .domain([0,1]); + var waveAnimateScale = d3.scale.linear() + .range([0, waveClipWidth-fillCircleRadius*2]) // Push the clip area one full wave then snap back. + .domain([0,1]); + + // Scale for controlling the position of the text within the gauge. + var textRiseScaleY = d3.scale.linear() + .range([fillCircleMargin+fillCircleRadius*2,(fillCircleMargin+textPixels*0.7)]) + .domain([0,1]); + + // Center the gauge within the parent SVG. + var gaugeGroup = gauge.append("g") + .attr('transform','translate('+locationX+','+locationY+')'); + + // Draw the outer circle. + var gaugeCircleArc = d3.svg.arc() + .startAngle(gaugeCircleX(0)) + .endAngle(gaugeCircleX(1)) + .outerRadius(gaugeCircleY(radius)) + .innerRadius(gaugeCircleY(radius-circleThickness)); + gaugeGroup.append("path") + .attr("d", gaugeCircleArc) + .style("fill", config.circleColor) + .attr('transform','translate('+radius+','+radius+')'); + + // Text where the wave does not overlap. + var text1 = gaugeGroup.append("text") + .text(textRounder(textStartValue) + percentText) + .attr("class", "liquidFillGaugeText") + .attr("text-anchor", "middle") + .attr("font-size", textPixels + "px") + .style("fill", config.textColor) + .attr('transform','translate('+radius+','+textRiseScaleY(config.textVertPosition)+')'); + + // The clipping wave area. + var clipArea = d3.svg.area() + .x(function(d) { return waveScaleX(d.x); } ) + .y0(function(d) { return waveScaleY(Math.sin(Math.PI*2*config.waveOffset*-1 + Math.PI*2*(1-config.waveCount) + d.y*2*Math.PI));} ) + .y1(function(d) { return (fillCircleRadius*2 + waveHeight); } ); + var waveGroup = gaugeGroup.append("defs") + .append("clipPath") + .attr("id", "clipWave" + elementId); + var wave = waveGroup.append("path") + .datum(data) + .attr("d", clipArea) + .attr("T", 0); + + // The inner circle with the clipping wave attached. + var fillCircleGroup = gaugeGroup.append("g") + .attr("clip-path", "url(#clipWave" + elementId + ")"); + fillCircleGroup.append("circle") + .attr("cx", radius) + .attr("cy", radius) + .attr("r", fillCircleRadius) + .style("fill", config.waveColor); + + // Text where the wave does overlap. + var text2 = fillCircleGroup.append("text") + .text(textRounder(textStartValue) + percentText) + .attr("class", "liquidFillGaugeText") + .attr("text-anchor", "middle") + .attr("font-size", textPixels + "px") + .style("fill", config.waveTextColor) + .attr('transform','translate('+radius+','+textRiseScaleY(config.textVertPosition)+')'); + + // Make the value count up. + if(config.valueCountUp){ + var textTween = function(){ + var i = d3.interpolate(this.textContent, textFinalValue); + return function(t) { this.textContent = textRounder(i(t)) + percentText; } + }; + text1.transition() + .duration(config.waveRiseTime) + .tween("text", textTween); + text2.transition() + .duration(config.waveRiseTime) + .tween("text", textTween); + } + + // Make the wave rise. wave and waveGroup are separate so that horizontal and vertical movement can be controlled independently. + var waveGroupXPosition = fillCircleMargin+fillCircleRadius*2-waveClipWidth; + if(config.waveRise){ + waveGroup.attr('transform','translate('+waveGroupXPosition+','+waveRiseScale(0)+')') + .transition() + .duration(config.waveRiseTime) + .attr('transform','translate('+waveGroupXPosition+','+waveRiseScale(fillPercent)+')') + .each("start", function(){ wave.attr('transform','translate(1,0)'); }); // This transform is necessary to get the clip wave positioned correctly when waveRise=true and waveAnimate=false. The wave will not position correctly without this, but it's not clear why this is actually necessary. + } else { + waveGroup.attr('transform','translate('+waveGroupXPosition+','+waveRiseScale(fillPercent)+')'); + } + + if(config.waveAnimate) animateWave(); + + function animateWave() { + wave.attr('transform','translate('+waveAnimateScale(wave.attr('T'))+',0)'); + wave.transition() + .duration(config.waveAnimateTime * (1-wave.attr('T'))) + .ease('linear') + .attr('transform','translate('+waveAnimateScale(1)+',0)') + .attr('T', 1) + .each('end', function(){ + wave.attr('T', 0); + animateWave(config.waveAnimateTime); + }); + } + + function GaugeUpdater(){ + this.update = function(value){ + var newFinalValue = parseFloat(value).toFixed(2); + var textRounderUpdater = function(value){ return Math.round(value); }; + if(parseFloat(newFinalValue) != parseFloat(textRounderUpdater(newFinalValue))){ + textRounderUpdater = function(value){ return parseFloat(value).toFixed(1); }; + } + if(parseFloat(newFinalValue) != parseFloat(textRounderUpdater(newFinalValue))){ + textRounderUpdater = function(value){ return parseFloat(value).toFixed(2); }; + } + + var textTween = function(){ + var i = d3.interpolate(this.textContent, parseFloat(value).toFixed(2)); + return function(t) { this.textContent = textRounderUpdater(i(t)) + percentText; } + }; + + text1.transition() + .duration(config.waveRiseTime) + .tween("text", textTween); + text2.transition() + .duration(config.waveRiseTime) + .tween("text", textTween); + + var fillPercent = Math.max(config.minValue, Math.min(config.maxValue, value))/config.maxValue; + var waveHeight = fillCircleRadius*waveHeightScale(fillPercent*100); + var waveRiseScale = d3.scale.linear() + // The clipping area size is the height of the fill circle + the wave height, so we position the clip wave + // such that the it will overlap the fill circle at all when at 0%, and will totally cover the fill + // circle at 100%. + .range([(fillCircleMargin+fillCircleRadius*2+waveHeight),(fillCircleMargin-waveHeight)]) + .domain([0,1]); + var newHeight = waveRiseScale(fillPercent); + var waveScaleX = d3.scale.linear().range([0,waveClipWidth]).domain([0,1]); + var waveScaleY = d3.scale.linear().range([0,waveHeight]).domain([0,1]); + var newClipArea; + if(config.waveHeightScaling){ + newClipArea = d3.svg.area() + .x(function(d) { return waveScaleX(d.x); } ) + .y0(function(d) { return waveScaleY(Math.sin(Math.PI*2*config.waveOffset*-1 + Math.PI*2*(1-config.waveCount) + d.y*2*Math.PI));} ) + .y1(function(d) { return (fillCircleRadius*2 + waveHeight); } ); + } else { + newClipArea = clipArea; + } + + var newWavePosition = config.waveAnimate?waveAnimateScale(1):0; + wave.transition() + .duration(0) + .transition() + .duration(config.waveAnimate?(config.waveAnimateTime * (1-wave.attr('T'))):(config.waveRiseTime)) + .ease('linear') + .attr('d', newClipArea) + .attr('transform','translate('+newWavePosition+',0)') + .attr('T','1') + .each("end", function(){ + if(config.waveAnimate){ + wave.attr('transform','translate('+waveAnimateScale(0)+',0)'); + animateWave(config.waveAnimateTime); + } + }); + waveGroup.transition() + .duration(config.waveRiseTime) + .attr('transform','translate('+waveGroupXPosition+','+newHeight+')') + } + } + + return new GaugeUpdater(); +} diff --git a/pocwww/static/moment.min.js b/pocwww/static/moment.min.js new file mode 100644 index 0000000..8c70671 --- /dev/null +++ b/pocwww/static/moment.min.js @@ -0,0 +1,551 @@ +//! moment.js +//! version : 2.17.1 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com +!function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):a.moment=b()}(this,function(){"use strict";function a(){return od.apply(null,arguments)} +// This is done to register the method called with moment() +// without creating circular dependencies. +function b(a){od=a}function c(a){return a instanceof Array||"[object Array]"===Object.prototype.toString.call(a)}function d(a){ +// IE8 will treat undefined and null as object if it wasn't for +// input != null +return null!=a&&"[object Object]"===Object.prototype.toString.call(a)}function e(a){var b;for(b in a) +// even if its not own property I'd still call it non-empty +return!1;return!0}function f(a){return"number"==typeof a||"[object Number]"===Object.prototype.toString.call(a)}function g(a){return a instanceof Date||"[object Date]"===Object.prototype.toString.call(a)}function h(a,b){var c,d=[];for(c=0;c0)for(c in rd)d=rd[c],e=b[d],p(e)||(a[d]=e);return a} +// Moment prototype object +function r(b){q(this,b),this._d=new Date(null!=b._d?b._d.getTime():NaN),this.isValid()||(this._d=new Date(NaN)), +// Prevent infinite loop in case updateOffset creates new moment +// objects. +sd===!1&&(sd=!0,a.updateOffset(this),sd=!1)}function s(a){return a instanceof r||null!=a&&null!=a._isAMomentObject}function t(a){return a<0?Math.ceil(a)||0:Math.floor(a)}function u(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=t(b)),c} +// compare two arrays, return the number of differences +function v(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;d0?"future":"past"];return z(c)?c(b):c.replace(/%s/i,b)}function J(a,b){var c=a.toLowerCase();Dd[c]=Dd[c+"s"]=Dd[b]=a}function K(a){return"string"==typeof a?Dd[a]||Dd[a.toLowerCase()]:void 0}function L(a){var b,c,d={};for(c in a)i(a,c)&&(b=K(c),b&&(d[b]=a[c]));return d}function M(a,b){Ed[a]=b}function N(a){var b=[];for(var c in a)b.push({unit:c,priority:Ed[c]});return b.sort(function(a,b){return a.priority-b.priority}),b}function O(b,c){return function(d){return null!=d?(Q(this,b,d),a.updateOffset(this,c),this):P(this,b)}}function P(a,b){return a.isValid()?a._d["get"+(a._isUTC?"UTC":"")+b]():NaN}function Q(a,b,c){a.isValid()&&a._d["set"+(a._isUTC?"UTC":"")+b](c)} +// MOMENTS +function R(a){return a=K(a),z(this[a])?this[a]():this}function S(a,b){if("object"==typeof a){a=L(a);for(var c=N(a),d=0;d=0;return(f?c?"+":"":"-")+Math.pow(10,Math.max(0,e)).toString().substr(1)+d} +// token: 'M' +// padded: ['MM', 2] +// ordinal: 'Mo' +// callback: function () { this.month() + 1 } +function U(a,b,c,d){var e=d;"string"==typeof d&&(e=function(){return this[d]()}),a&&(Id[a]=e),b&&(Id[b[0]]=function(){return T(e.apply(this,arguments),b[1],b[2])}),c&&(Id[c]=function(){return this.localeData().ordinal(e.apply(this,arguments),a)})}function V(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function W(a){var b,c,d=a.match(Fd);for(b=0,c=d.length;b=0&&Gd.test(a);)a=a.replace(Gd,c),Gd.lastIndex=0,d-=1;return a}function Z(a,b,c){$d[a]=z(b)?b:function(a,d){return a&&c?c:b}}function $(a,b){return i($d,a)?$d[a](b._strict,b._locale):new RegExp(_(a))} +// Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript +function _(a){return aa(a.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e}))}function aa(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function ba(a,b){var c,d=b;for("string"==typeof a&&(a=[a]),f(b)&&(d=function(a,c){c[b]=u(a)}),c=0;c=0&&isFinite(h.getFullYear())&&h.setFullYear(a),h}function ta(a){var b=new Date(Date.UTC.apply(null,arguments)); +//the Date.UTC function remaps years 0-99 to 1900-1999 +return a<100&&a>=0&&isFinite(b.getUTCFullYear())&&b.setUTCFullYear(a),b} +// start-of-first-week - start-of-year +function ua(a,b,c){var// first-week day -- which january is always in the first week (4 for iso, 1 for other) +d=7+b-c, +// first-week day local weekday -- which local weekday is fwd +e=(7+ta(a,0,d).getUTCDay()-b)%7;return-e+d-1} +//http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday +function va(a,b,c,d,e){var f,g,h=(7+c-d)%7,i=ua(a,d,e),j=1+7*(b-1)+h+i;return j<=0?(f=a-1,g=pa(f)+j):j>pa(a)?(f=a+1,g=j-pa(a)):(f=a,g=j),{year:f,dayOfYear:g}}function wa(a,b,c){var d,e,f=ua(a.year(),b,c),g=Math.floor((a.dayOfYear()-f-1)/7)+1;return g<1?(e=a.year()-1,d=g+xa(e,b,c)):g>xa(a.year(),b,c)?(d=g-xa(a.year(),b,c),e=a.year()+1):(e=a.year(),d=g),{week:d,year:e}}function xa(a,b,c){var d=ua(a,b,c),e=ua(a+1,b,c);return(pa(a)-d+e)/7} +// HELPERS +// LOCALES +function ya(a){return wa(a,this._week.dow,this._week.doy).week}function za(){return this._week.dow}function Aa(){return this._week.doy} +// MOMENTS +function Ba(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")}function Ca(a){var b=wa(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")} +// HELPERS +function Da(a,b){return"string"!=typeof a?a:isNaN(a)?(a=b.weekdaysParse(a),"number"==typeof a?a:null):parseInt(a,10)}function Ea(a,b){return"string"==typeof a?b.weekdaysParse(a)%7||7:isNaN(a)?null:a}function Fa(a,b){return a?c(this._weekdays)?this._weekdays[a.day()]:this._weekdays[this._weekdays.isFormat.test(b)?"format":"standalone"][a.day()]:this._weekdays}function Ga(a){return a?this._weekdaysShort[a.day()]:this._weekdaysShort}function Ha(a){return a?this._weekdaysMin[a.day()]:this._weekdaysMin}function Ia(a,b,c){var d,e,f,g=a.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],d=0;d<7;++d)f=k([2e3,1]).day(d),this._minWeekdaysParse[d]=this.weekdaysMin(f,"").toLocaleLowerCase(),this._shortWeekdaysParse[d]=this.weekdaysShort(f,"").toLocaleLowerCase(),this._weekdaysParse[d]=this.weekdays(f,"").toLocaleLowerCase();return c?"dddd"===b?(e=je.call(this._weekdaysParse,g),e!==-1?e:null):"ddd"===b?(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:null):(e=je.call(this._minWeekdaysParse,g),e!==-1?e:null):"dddd"===b?(e=je.call(this._weekdaysParse,g),e!==-1?e:(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:(e=je.call(this._minWeekdaysParse,g),e!==-1?e:null))):"ddd"===b?(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:(e=je.call(this._weekdaysParse,g),e!==-1?e:(e=je.call(this._minWeekdaysParse,g),e!==-1?e:null))):(e=je.call(this._minWeekdaysParse,g),e!==-1?e:(e=je.call(this._weekdaysParse,g),e!==-1?e:(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:null)))}function Ja(a,b,c){var d,e,f;if(this._weekdaysParseExact)return Ia.call(this,a,b,c);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),d=0;d<7;d++){ +// test the regex +if( +// make the regex if we don't have it already +e=k([2e3,1]).day(d),c&&!this._fullWeekdaysParse[d]&&(this._fullWeekdaysParse[d]=new RegExp("^"+this.weekdays(e,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[d]=new RegExp("^"+this.weekdaysShort(e,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[d]=new RegExp("^"+this.weekdaysMin(e,"").replace(".",".?")+"$","i")),this._weekdaysParse[d]||(f="^"+this.weekdays(e,"")+"|^"+this.weekdaysShort(e,"")+"|^"+this.weekdaysMin(e,""),this._weekdaysParse[d]=new RegExp(f.replace(".",""),"i")),c&&"dddd"===b&&this._fullWeekdaysParse[d].test(a))return d;if(c&&"ddd"===b&&this._shortWeekdaysParse[d].test(a))return d;if(c&&"dd"===b&&this._minWeekdaysParse[d].test(a))return d;if(!c&&this._weekdaysParse[d].test(a))return d}} +// MOMENTS +function Ka(a){if(!this.isValid())return null!=a?this:NaN;var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=Da(a,this.localeData()),this.add(a-b,"d")):b}function La(a){if(!this.isValid())return null!=a?this:NaN;var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")}function Ma(a){if(!this.isValid())return null!=a?this:NaN; +// behaves the same as moment#day except +// as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) +// as a setter, sunday should belong to the previous week. +if(null!=a){var b=Ea(a,this.localeData());return this.day(this.day()%7?b:b-7)}return this.day()||7}function Na(a){return this._weekdaysParseExact?(i(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysStrictRegex:this._weekdaysRegex):(i(this,"_weekdaysRegex")||(this._weekdaysRegex=ue),this._weekdaysStrictRegex&&a?this._weekdaysStrictRegex:this._weekdaysRegex)}function Oa(a){return this._weekdaysParseExact?(i(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(i(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=ve),this._weekdaysShortStrictRegex&&a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)}function Pa(a){return this._weekdaysParseExact?(i(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(i(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=we),this._weekdaysMinStrictRegex&&a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)}function Qa(){function a(a,b){return b.length-a.length}var b,c,d,e,f,g=[],h=[],i=[],j=[];for(b=0;b<7;b++) +// make the regex if we don't have it already +c=k([2e3,1]).day(b),d=this.weekdaysMin(c,""),e=this.weekdaysShort(c,""),f=this.weekdays(c,""),g.push(d),h.push(e),i.push(f),j.push(d),j.push(e),j.push(f);for( +// Sorting makes sure if one weekday (or abbr) is a prefix of another it +// will match the longer piece. +g.sort(a),h.sort(a),i.sort(a),j.sort(a),b=0;b<7;b++)h[b]=aa(h[b]),i[b]=aa(i[b]),j[b]=aa(j[b]);this._weekdaysRegex=new RegExp("^("+j.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+i.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+h.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+g.join("|")+")","i")} +// FORMATTING +function Ra(){return this.hours()%12||12}function Sa(){return this.hours()||24}function Ta(a,b){U(a,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),b)})} +// PARSING +function Ua(a,b){return b._meridiemParse} +// LOCALES +function Va(a){ +// IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays +// Using charAt should be more compatible. +return"p"===(a+"").toLowerCase().charAt(0)}function Wa(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"}function Xa(a){return a?a.toLowerCase().replace("_","-"):a} +// pick the locale from the array +// try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each +// substring from most specific to least, but move to the next array item if it's a more specific variant than the current root +function Ya(a){for(var b,c,d,e,f=0;f0;){if(d=Za(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&v(e,c,!0)>=b-1) +//the next array item is better than a shallower substring of this one +break;b--}f++}return null}function Za(a){var b=null; +// TODO: Find a better way to register and load all the locales in Node +if(!Be[a]&&"undefined"!=typeof module&&module&&module.exports)try{b=xe._abbr,require("./locale/"+a), +// because defineLocale currently also sets the global locale, we +// want to undo that for lazy loaded locales +$a(b)}catch(a){}return Be[a]} +// This function will load locale and then set the global locale. If +// no arguments are passed in, it will simply return the current global +// locale key. +function $a(a,b){var c; +// moment.duration._locale = moment._locale = data; +return a&&(c=p(b)?bb(a):_a(a,b),c&&(xe=c)),xe._abbr}function _a(a,b){if(null!==b){var c=Ae;if(b.abbr=a,null!=Be[a])y("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),c=Be[a]._config;else if(null!=b.parentLocale){if(null==Be[b.parentLocale])return Ce[b.parentLocale]||(Ce[b.parentLocale]=[]),Ce[b.parentLocale].push({name:a,config:b}),null;c=Be[b.parentLocale]._config} +// backwards compat for now: also set the locale +// make sure we set the locale AFTER all child locales have been +// created, so we won't end up with the child locale set. +return Be[a]=new C(B(c,b)),Ce[a]&&Ce[a].forEach(function(a){_a(a.name,a.config)}),$a(a),Be[a]} +// useful for testing +return delete Be[a],null}function ab(a,b){if(null!=b){var c,d=Ae; +// MERGE +null!=Be[a]&&(d=Be[a]._config),b=B(d,b),c=new C(b),c.parentLocale=Be[a],Be[a]=c, +// backwards compat for now: also set the locale +$a(a)}else +// pass null for config to unupdate, useful for tests +null!=Be[a]&&(null!=Be[a].parentLocale?Be[a]=Be[a].parentLocale:null!=Be[a]&&delete Be[a]);return Be[a]} +// returns locale data +function bb(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return xe;if(!c(a)){if( +//short-circuit everything else +b=Za(a))return b;a=[a]}return Ya(a)}function cb(){return wd(Be)}function db(a){var b,c=a._a;return c&&m(a).overflow===-2&&(b=c[be]<0||c[be]>11?be:c[ce]<1||c[ce]>ea(c[ae],c[be])?ce:c[de]<0||c[de]>24||24===c[de]&&(0!==c[ee]||0!==c[fe]||0!==c[ge])?de:c[ee]<0||c[ee]>59?ee:c[fe]<0||c[fe]>59?fe:c[ge]<0||c[ge]>999?ge:-1,m(a)._overflowDayOfYear&&(bce)&&(b=ce),m(a)._overflowWeeks&&b===-1&&(b=he),m(a)._overflowWeekday&&b===-1&&(b=ie),m(a).overflow=b),a} +// date from iso format +function eb(a){var b,c,d,e,f,g,h=a._i,i=De.exec(h)||Ee.exec(h);if(i){for(m(a).iso=!0,b=0,c=Ge.length;bpa(e)&&(m(a)._overflowDayOfYear=!0),c=ta(e,0,a._dayOfYear),a._a[be]=c.getUTCMonth(),a._a[ce]=c.getUTCDate()),b=0;b<3&&null==a._a[b];++b)a._a[b]=f[b]=d[b]; +// Zero out whatever was not defaulted, including time +for(;b<7;b++)a._a[b]=f[b]=null==a._a[b]?2===b?1:0:a._a[b]; +// Check for 24:00:00.000 +24===a._a[de]&&0===a._a[ee]&&0===a._a[fe]&&0===a._a[ge]&&(a._nextDay=!0,a._a[de]=0),a._d=(a._useUTC?ta:sa).apply(null,f), +// Apply timezone offset from input. The actual utcOffset can be changed +// with parseZone. +null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[de]=24)}}function jb(a){var b,c,d,e,f,g,h,i;if(b=a._w,null!=b.GG||null!=b.W||null!=b.E)f=1,g=4, +// TODO: We need to take the current isoWeekYear, but that depends on +// how we interpret now (local, utc, fixed offset). So create +// a now version of current config (take local/utc/offset flags, and +// create now). +c=gb(b.GG,a._a[ae],wa(sb(),1,4).year),d=gb(b.W,1),e=gb(b.E,1),(e<1||e>7)&&(i=!0);else{f=a._locale._week.dow,g=a._locale._week.doy;var j=wa(sb(),f,g);c=gb(b.gg,a._a[ae],j.year), +// Default to current week. +d=gb(b.w,j.week),null!=b.d?( +// weekday -- low day numbers are considered next week +e=b.d,(e<0||e>6)&&(i=!0)):null!=b.e?( +// local weekday -- counting starts from begining of week +e=b.e+f,(b.e<0||b.e>6)&&(i=!0)): +// default to begining of week +e=f}d<1||d>xa(c,f,g)?m(a)._overflowWeeks=!0:null!=i?m(a)._overflowWeekday=!0:(h=va(c,d,e,f,g),a._a[ae]=h.year,a._dayOfYear=h.dayOfYear)} +// date from string and format string +function kb(b){ +// TODO: Move this to another part of the creation flow to prevent circular deps +if(b._f===a.ISO_8601)return void eb(b);b._a=[],m(b).empty=!0; +// This array is used to make a Date, either with `new Date` or `Date.UTC` +var c,d,e,f,g,h=""+b._i,i=h.length,j=0;for(e=Y(b._f,b._locale).match(Fd)||[],c=0;c0&&m(b).unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),j+=d.length), +// don't parse if it's not a known token +Id[f]?(d?m(b).empty=!1:m(b).unusedTokens.push(f),da(f,d,b)):b._strict&&!d&&m(b).unusedTokens.push(f); +// add remaining unparsed input length to the string +m(b).charsLeftOver=i-j,h.length>0&&m(b).unusedInput.push(h), +// clear _12h flag if hour is <= 12 +b._a[de]<=12&&m(b).bigHour===!0&&b._a[de]>0&&(m(b).bigHour=void 0),m(b).parsedDateParts=b._a.slice(0),m(b).meridiem=b._meridiem, +// handle meridiem +b._a[de]=lb(b._locale,b._a[de],b._meridiem),ib(b),db(b)}function lb(a,b,c){var d; +// Fallback +return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&b<12&&(b+=12),d||12!==b||(b=0),b):b} +// date from string and array of format strings +function mb(a){var b,c,d,e,f;if(0===a._f.length)return m(a).invalidFormat=!0,void(a._d=new Date(NaN));for(e=0;e +// 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset +// +0200, so we adjust the time as needed, to be valid. +// +// Keeping the time actually adds/subtracts (one hour) +// from the actual represented time. That is why we call updateOffset +// a second time. In case it wants us to change the offset again +// _changeInProgress == true case, then we have to adjust, because +// there is no such time in the given timezone. +function Db(b,c){var d,e=this._offset||0;if(!this.isValid())return null!=b?this:NaN;if(null!=b){if("string"==typeof b){if(b=Ab(Xd,b),null===b)return this}else Math.abs(b)<16&&(b=60*b);return!this._isUTC&&c&&(d=Cb(this)),this._offset=b,this._isUTC=!0,null!=d&&this.add(d,"m"),e!==b&&(!c||this._changeInProgress?Tb(this,Ob(b-e,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this}return this._isUTC?e:Cb(this)}function Eb(a,b){return null!=a?("string"!=typeof a&&(a=-a),this.utcOffset(a,b),this):-this.utcOffset()}function Fb(a){return this.utcOffset(0,a)}function Gb(a){return this._isUTC&&(this.utcOffset(0,a),this._isUTC=!1,a&&this.subtract(Cb(this),"m")),this}function Hb(){if(null!=this._tzm)this.utcOffset(this._tzm);else if("string"==typeof this._i){var a=Ab(Wd,this._i);null!=a?this.utcOffset(a):this.utcOffset(0,!0)}return this}function Ib(a){return!!this.isValid()&&(a=a?sb(a).utcOffset():0,(this.utcOffset()-a)%60===0)}function Jb(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Kb(){if(!p(this._isDSTShifted))return this._isDSTShifted;var a={};if(q(a,this),a=pb(a),a._a){var b=a._isUTC?k(a._a):sb(a._a);this._isDSTShifted=this.isValid()&&v(a._a,b.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Lb(){return!!this.isValid()&&!this._isUTC}function Mb(){return!!this.isValid()&&this._isUTC}function Nb(){return!!this.isValid()&&(this._isUTC&&0===this._offset)}function Ob(a,b){var c,d,e,g=a, +// matching against regexp is expensive, do it on demand +h=null;// checks for null or undefined +return xb(a)?g={ms:a._milliseconds,d:a._days,M:a._months}:f(a)?(g={},b?g[b]=a:g.milliseconds=a):(h=Ne.exec(a))?(c="-"===h[1]?-1:1,g={y:0,d:u(h[ce])*c,h:u(h[de])*c,m:u(h[ee])*c,s:u(h[fe])*c,ms:u(yb(1e3*h[ge]))*c}):(h=Oe.exec(a))?(c="-"===h[1]?-1:1,g={y:Pb(h[2],c),M:Pb(h[3],c),w:Pb(h[4],c),d:Pb(h[5],c),h:Pb(h[6],c),m:Pb(h[7],c),s:Pb(h[8],c)}):null==g?g={}:"object"==typeof g&&("from"in g||"to"in g)&&(e=Rb(sb(g.from),sb(g.to)),g={},g.ms=e.milliseconds,g.M=e.months),d=new wb(g),xb(a)&&i(a,"_locale")&&(d._locale=a._locale),d}function Pb(a,b){ +// We'd normally use ~~inp for this, but unfortunately it also +// converts floats to ints. +// inp may be undefined, so careful calling replace on it. +var c=a&&parseFloat(a.replace(",",".")); +// apply sign while we're at it +return(isNaN(c)?0:c)*b}function Qb(a,b){var c={milliseconds:0,months:0};return c.months=b.month()-a.month()+12*(b.year()-a.year()),a.clone().add(c.months,"M").isAfter(b)&&--c.months,c.milliseconds=+b-+a.clone().add(c.months,"M"),c}function Rb(a,b){var c;return a.isValid()&&b.isValid()?(b=Bb(b,a),a.isBefore(b)?c=Qb(a,b):(c=Qb(b,a),c.milliseconds=-c.milliseconds,c.months=-c.months),c):{milliseconds:0,months:0}} +// TODO: remove 'name' arg after deprecation is removed +function Sb(a,b){return function(c,d){var e,f; +//invert the arguments, but complain about it +return null===d||isNaN(+d)||(y(b,"moment()."+b+"(period, number) is deprecated. Please use moment()."+b+"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info."),f=c,c=d,d=f),c="string"==typeof c?+c:c,e=Ob(c,d),Tb(this,e,a),this}}function Tb(b,c,d,e){var f=c._milliseconds,g=yb(c._days),h=yb(c._months);b.isValid()&&(e=null==e||e,f&&b._d.setTime(b._d.valueOf()+f*d),g&&Q(b,"Date",P(b,"Date")+g*d),h&&ja(b,P(b,"Month")+h*d),e&&a.updateOffset(b,g||h))}function Ub(a,b){var c=a.diff(b,"days",!0);return c<-6?"sameElse":c<-1?"lastWeek":c<0?"lastDay":c<1?"sameDay":c<2?"nextDay":c<7?"nextWeek":"sameElse"}function Vb(b,c){ +// We want to compare the start of today, vs this. +// Getting start-of-today depends on whether we're local/utc/offset or not. +var d=b||sb(),e=Bb(d,this).startOf("day"),f=a.calendarFormat(this,e)||"sameElse",g=c&&(z(c[f])?c[f].call(this,d):c[f]);return this.format(g||this.localeData().calendar(f,this,sb(d)))}function Wb(){return new r(this)}function Xb(a,b){var c=s(a)?a:sb(a);return!(!this.isValid()||!c.isValid())&&(b=K(p(b)?"millisecond":b),"millisecond"===b?this.valueOf()>c.valueOf():c.valueOf()f&&(b=f),Fc.call(this,a,b,c,d,e))}function Fc(a,b,c,d,e){var f=va(a,b,c,d,e),g=ta(f.year,0,f.dayOfYear);return this.year(g.getUTCFullYear()),this.month(g.getUTCMonth()),this.date(g.getUTCDate()),this} +// MOMENTS +function Gc(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)} +// HELPERS +// MOMENTS +function Hc(a){var b=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")}function Ic(a,b){b[ge]=u(1e3*("0."+a))} +// MOMENTS +function Jc(){return this._isUTC?"UTC":""}function Kc(){return this._isUTC?"Coordinated Universal Time":""}function Lc(a){return sb(1e3*a)}function Mc(){return sb.apply(null,arguments).parseZone()}function Nc(a){return a}function Oc(a,b,c,d){var e=bb(),f=k().set(d,b);return e[c](f,a)}function Pc(a,b,c){if(f(a)&&(b=a,a=void 0),a=a||"",null!=b)return Oc(a,b,c,"month");var d,e=[];for(d=0;d<12;d++)e[d]=Oc(a,d,c,"month");return e} +// () +// (5) +// (fmt, 5) +// (fmt) +// (true) +// (true, 5) +// (true, fmt, 5) +// (true, fmt) +function Qc(a,b,c,d){"boolean"==typeof a?(f(b)&&(c=b,b=void 0),b=b||""):(b=a,c=b,a=!1,f(b)&&(c=b,b=void 0),b=b||"");var e=bb(),g=a?e._week.dow:0;if(null!=c)return Oc(b,(c+g)%7,d,"day");var h,i=[];for(h=0;h<7;h++)i[h]=Oc(b,(h+g)%7,d,"day");return i}function Rc(a,b){return Pc(a,b,"months")}function Sc(a,b){return Pc(a,b,"monthsShort")}function Tc(a,b,c){return Qc(a,b,c,"weekdays")}function Uc(a,b,c){return Qc(a,b,c,"weekdaysShort")}function Vc(a,b,c){return Qc(a,b,c,"weekdaysMin")}function Wc(){var a=this._data;return this._milliseconds=Ze(this._milliseconds),this._days=Ze(this._days),this._months=Ze(this._months),a.milliseconds=Ze(a.milliseconds),a.seconds=Ze(a.seconds),a.minutes=Ze(a.minutes),a.hours=Ze(a.hours),a.months=Ze(a.months),a.years=Ze(a.years),this}function Xc(a,b,c,d){var e=Ob(b,c);return a._milliseconds+=d*e._milliseconds,a._days+=d*e._days,a._months+=d*e._months,a._bubble()} +// supports only 2.0-style add(1, 's') or add(duration) +function Yc(a,b){return Xc(this,a,b,1)} +// supports only 2.0-style subtract(1, 's') or subtract(duration) +function Zc(a,b){return Xc(this,a,b,-1)}function $c(a){return a<0?Math.floor(a):Math.ceil(a)}function _c(){var a,b,c,d,e,f=this._milliseconds,g=this._days,h=this._months,i=this._data; +// if we have a mix of positive and negative values, bubble down first +// check: https://github.com/moment/moment/issues/2166 +// The following code bubbles up values, see the tests for +// examples of what that means. +// convert days to months +// 12 months -> 1 year +return f>=0&&g>=0&&h>=0||f<=0&&g<=0&&h<=0||(f+=864e5*$c(bd(h)+g),g=0,h=0),i.milliseconds=f%1e3,a=t(f/1e3),i.seconds=a%60,b=t(a/60),i.minutes=b%60,c=t(b/60),i.hours=c%24,g+=t(c/24),e=t(ad(g)),h+=e,g-=$c(bd(e)),d=t(h/12),h%=12,i.days=g,i.months=h,i.years=d,this}function ad(a){ +// 400 years have 146097 days (taking into account leap year rules) +// 400 years have 12 months === 4800 +return 4800*a/146097}function bd(a){ +// the reverse of daysToMonths +return 146097*a/4800}function cd(a){var b,c,d=this._milliseconds;if(a=K(a),"month"===a||"year"===a)return b=this._days+d/864e5,c=this._months+ad(b),"month"===a?c:c/12;switch( +// handle milliseconds separately because of floating point math errors (issue #1867) +b=this._days+Math.round(bd(this._months)),a){case"week":return b/7+d/6048e5;case"day":return b+d/864e5;case"hour":return 24*b+d/36e5;case"minute":return 1440*b+d/6e4;case"second":return 86400*b+d/1e3; +// Math.floor prevents floating point math errors here +case"millisecond":return Math.floor(864e5*b)+d;default:throw new Error("Unknown unit "+a)}} +// TODO: Use this.as('ms')? +function dd(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*u(this._months/12)}function ed(a){return function(){return this.as(a)}}function fd(a){return a=K(a),this[a+"s"]()}function gd(a){return function(){return this._data[a]}}function hd(){return t(this.days()/7)} +// helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize +function id(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function jd(a,b,c){var d=Ob(a).abs(),e=of(d.as("s")),f=of(d.as("m")),g=of(d.as("h")),h=of(d.as("d")),i=of(d.as("M")),j=of(d.as("y")),k=e0,k[4]=c,id.apply(null,k)} +// This function allows you to set the rounding function for relative time strings +function kd(a){return void 0===a?of:"function"==typeof a&&(of=a,!0)} +// This function allows you to set a threshold for relative time strings +function ld(a,b){return void 0!==pf[a]&&(void 0===b?pf[a]:(pf[a]=b,!0))}function md(a){var b=this.localeData(),c=jd(this,!a,b);return a&&(c=b.pastFuture(+this,c)),b.postformat(c)}function nd(){ +// for ISO strings we do not use the normal bubbling rules: +// * milliseconds bubble up until they become hours +// * days do not bubble at all +// * months bubble up until they become years +// This is because there is no context-free conversion between hours and days +// (think of clock changes) +// and also not between days and months (28-31 days per month) +var a,b,c,d=qf(this._milliseconds)/1e3,e=qf(this._days),f=qf(this._months); +// 3600 seconds -> 60 minutes -> 1 hour +a=t(d/60),b=t(a/60),d%=60,a%=60, +// 12 months -> 1 year +c=t(f/12),f%=12; +// inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js +var g=c,h=f,i=e,j=b,k=a,l=d,m=this.asSeconds();return m?(m<0?"-":"")+"P"+(g?g+"Y":"")+(h?h+"M":"")+(i?i+"D":"")+(j||k||l?"T":"")+(j?j+"H":"")+(k?k+"M":"")+(l?l+"S":""):"P0D"}var od,pd;pd=Array.prototype.some?Array.prototype.some:function(a){for(var b=Object(this),c=b.length>>>0,d=0;d68?1900:2e3)}; +// MOMENTS +var pe=O("FullYear",!0); +// FORMATTING +U("w",["ww",2],"wo","week"),U("W",["WW",2],"Wo","isoWeek"), +// ALIASES +J("week","w"),J("isoWeek","W"), +// PRIORITIES +M("week",5),M("isoWeek",5), +// PARSING +Z("w",Od),Z("ww",Od,Kd),Z("W",Od),Z("WW",Od,Kd),ca(["w","ww","W","WW"],function(a,b,c,d){b[d.substr(0,1)]=u(a)});var qe={dow:0,// Sunday is the first day of the week. +doy:6}; +// FORMATTING +U("d",0,"do","day"),U("dd",0,0,function(a){return this.localeData().weekdaysMin(this,a)}),U("ddd",0,0,function(a){return this.localeData().weekdaysShort(this,a)}),U("dddd",0,0,function(a){return this.localeData().weekdays(this,a)}),U("e",0,0,"weekday"),U("E",0,0,"isoWeekday"), +// ALIASES +J("day","d"),J("weekday","e"),J("isoWeekday","E"), +// PRIORITY +M("day",11),M("weekday",11),M("isoWeekday",11), +// PARSING +Z("d",Od),Z("e",Od),Z("E",Od),Z("dd",function(a,b){return b.weekdaysMinRegex(a)}),Z("ddd",function(a,b){return b.weekdaysShortRegex(a)}),Z("dddd",function(a,b){return b.weekdaysRegex(a)}),ca(["dd","ddd","dddd"],function(a,b,c,d){var e=c._locale.weekdaysParse(a,d,c._strict); +// if we didn't get a weekday name, mark the date as invalid +null!=e?b.d=e:m(c).invalidWeekday=a}),ca(["d","e","E"],function(a,b,c,d){b[d]=u(a)}); +// LOCALES +var re="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),se="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),te="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),ue=Zd,ve=Zd,we=Zd;U("H",["HH",2],0,"hour"),U("h",["hh",2],0,Ra),U("k",["kk",2],0,Sa),U("hmm",0,0,function(){return""+Ra.apply(this)+T(this.minutes(),2)}),U("hmmss",0,0,function(){return""+Ra.apply(this)+T(this.minutes(),2)+T(this.seconds(),2)}),U("Hmm",0,0,function(){return""+this.hours()+T(this.minutes(),2)}),U("Hmmss",0,0,function(){return""+this.hours()+T(this.minutes(),2)+T(this.seconds(),2)}),Ta("a",!0),Ta("A",!1), +// ALIASES +J("hour","h"), +// PRIORITY +M("hour",13),Z("a",Ua),Z("A",Ua),Z("H",Od),Z("h",Od),Z("HH",Od,Kd),Z("hh",Od,Kd),Z("hmm",Pd),Z("hmmss",Qd),Z("Hmm",Pd),Z("Hmmss",Qd),ba(["H","HH"],de),ba(["a","A"],function(a,b,c){c._isPm=c._locale.isPM(a),c._meridiem=a}),ba(["h","hh"],function(a,b,c){b[de]=u(a),m(c).bigHour=!0}),ba("hmm",function(a,b,c){var d=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d)),m(c).bigHour=!0}),ba("hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d,2)),b[fe]=u(a.substr(e)),m(c).bigHour=!0}),ba("Hmm",function(a,b,c){var d=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d))}),ba("Hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d,2)),b[fe]=u(a.substr(e))});var xe,ye=/[ap]\.?m?\.?/i,ze=O("Hours",!0),Ae={calendar:xd,longDateFormat:yd,invalidDate:zd,ordinal:Ad,ordinalParse:Bd,relativeTime:Cd,months:le,monthsShort:me,week:qe,weekdays:re,weekdaysMin:te,weekdaysShort:se,meridiemParse:ye},Be={},Ce={},De=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Ee=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Fe=/Z|[+-]\d\d(?::?\d\d)?/,Ge=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/], +// YYYYMM is NOT allowed by the standard +["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],He=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],Ie=/^\/?Date\((\-?\d+)/i;a.createFromInputFallback=x("value provided is not in a recognized ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non ISO date formats are discouraged and will be removed in an upcoming major release. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.",function(a){a._d=new Date(a._i+(a._useUTC?" UTC":""))}), +// constant that refers to the ISO standard +a.ISO_8601=function(){};var Je=x("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var a=sb.apply(null,arguments);return this.isValid()&&a.isValid()?athis?this:a:o()}),Le=function(){return Date.now?Date.now():+new Date};zb("Z",":"),zb("ZZ",""), +// PARSING +Z("Z",Xd),Z("ZZ",Xd),ba(["Z","ZZ"],function(a,b,c){c._useUTC=!0,c._tzm=Ab(Xd,a)}); +// HELPERS +// timezone chunker +// '+10:00' > ['10', '00'] +// '-1530' > ['-15', '30'] +var Me=/([\+\-]|\d\d)/gi; +// HOOKS +// This function will be called whenever a moment is mutated. +// It is intended to keep the offset in sync with the timezone. +a.updateOffset=function(){}; +// ASP.NET json date format regex +var Ne=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/,Oe=/^(-)?P(?:(-?[0-9,.]*)Y)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)W)?(?:(-?[0-9,.]*)D)?(?:T(?:(-?[0-9,.]*)H)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)S)?)?$/;Ob.fn=wb.prototype;var Pe=Sb(1,"add"),Qe=Sb(-1,"subtract");a.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",a.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var Re=x("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(a){return void 0===a?this.localeData():this.locale(a)}); +// FORMATTING +U(0,["gg",2],0,function(){return this.weekYear()%100}),U(0,["GG",2],0,function(){return this.isoWeekYear()%100}),zc("gggg","weekYear"),zc("ggggg","weekYear"),zc("GGGG","isoWeekYear"),zc("GGGGG","isoWeekYear"), +// ALIASES +J("weekYear","gg"),J("isoWeekYear","GG"), +// PRIORITY +M("weekYear",1),M("isoWeekYear",1), +// PARSING +Z("G",Vd),Z("g",Vd),Z("GG",Od,Kd),Z("gg",Od,Kd),Z("GGGG",Sd,Md),Z("gggg",Sd,Md),Z("GGGGG",Td,Nd),Z("ggggg",Td,Nd),ca(["gggg","ggggg","GGGG","GGGGG"],function(a,b,c,d){b[d.substr(0,2)]=u(a)}),ca(["gg","GG"],function(b,c,d,e){c[e]=a.parseTwoDigitYear(b)}), +// FORMATTING +U("Q",0,"Qo","quarter"), +// ALIASES +J("quarter","Q"), +// PRIORITY +M("quarter",7), +// PARSING +Z("Q",Jd),ba("Q",function(a,b){b[be]=3*(u(a)-1)}), +// FORMATTING +U("D",["DD",2],"Do","date"), +// ALIASES +J("date","D"), +// PRIOROITY +M("date",9), +// PARSING +Z("D",Od),Z("DD",Od,Kd),Z("Do",function(a,b){return a?b._ordinalParse:b._ordinalParseLenient}),ba(["D","DD"],ce),ba("Do",function(a,b){b[ce]=u(a.match(Od)[0],10)}); +// MOMENTS +var Se=O("Date",!0); +// FORMATTING +U("DDD",["DDDD",3],"DDDo","dayOfYear"), +// ALIASES +J("dayOfYear","DDD"), +// PRIORITY +M("dayOfYear",4), +// PARSING +Z("DDD",Rd),Z("DDDD",Ld),ba(["DDD","DDDD"],function(a,b,c){c._dayOfYear=u(a)}), +// FORMATTING +U("m",["mm",2],0,"minute"), +// ALIASES +J("minute","m"), +// PRIORITY +M("minute",14), +// PARSING +Z("m",Od),Z("mm",Od,Kd),ba(["m","mm"],ee); +// MOMENTS +var Te=O("Minutes",!1); +// FORMATTING +U("s",["ss",2],0,"second"), +// ALIASES +J("second","s"), +// PRIORITY +M("second",15), +// PARSING +Z("s",Od),Z("ss",Od,Kd),ba(["s","ss"],fe); +// MOMENTS +var Ue=O("Seconds",!1); +// FORMATTING +U("S",0,0,function(){return~~(this.millisecond()/100)}),U(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),U(0,["SSS",3],0,"millisecond"),U(0,["SSSS",4],0,function(){return 10*this.millisecond()}),U(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),U(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),U(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),U(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),U(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}), +// ALIASES +J("millisecond","ms"), +// PRIORITY +M("millisecond",16), +// PARSING +Z("S",Rd,Jd),Z("SS",Rd,Kd),Z("SSS",Rd,Ld);var Ve;for(Ve="SSSS";Ve.length<=9;Ve+="S")Z(Ve,Ud);for(Ve="S";Ve.length<=9;Ve+="S")ba(Ve,Ic); +// MOMENTS +var We=O("Milliseconds",!1); +// FORMATTING +U("z",0,0,"zoneAbbr"),U("zz",0,0,"zoneName");var Xe=r.prototype;Xe.add=Pe,Xe.calendar=Vb,Xe.clone=Wb,Xe.diff=bc,Xe.endOf=oc,Xe.format=gc,Xe.from=hc,Xe.fromNow=ic,Xe.to=jc,Xe.toNow=kc,Xe.get=R,Xe.invalidAt=xc,Xe.isAfter=Xb,Xe.isBefore=Yb,Xe.isBetween=Zb,Xe.isSame=$b,Xe.isSameOrAfter=_b,Xe.isSameOrBefore=ac,Xe.isValid=vc,Xe.lang=Re,Xe.locale=lc,Xe.localeData=mc,Xe.max=Ke,Xe.min=Je,Xe.parsingFlags=wc,Xe.set=S,Xe.startOf=nc,Xe.subtract=Qe,Xe.toArray=sc,Xe.toObject=tc,Xe.toDate=rc,Xe.toISOString=ec,Xe.inspect=fc,Xe.toJSON=uc,Xe.toString=dc,Xe.unix=qc,Xe.valueOf=pc,Xe.creationData=yc, +// Year +Xe.year=pe,Xe.isLeapYear=ra, +// Week Year +Xe.weekYear=Ac,Xe.isoWeekYear=Bc, +// Quarter +Xe.quarter=Xe.quarters=Gc, +// Month +Xe.month=ka,Xe.daysInMonth=la, +// Week +Xe.week=Xe.weeks=Ba,Xe.isoWeek=Xe.isoWeeks=Ca,Xe.weeksInYear=Dc,Xe.isoWeeksInYear=Cc, +// Day +Xe.date=Se,Xe.day=Xe.days=Ka,Xe.weekday=La,Xe.isoWeekday=Ma,Xe.dayOfYear=Hc, +// Hour +Xe.hour=Xe.hours=ze, +// Minute +Xe.minute=Xe.minutes=Te, +// Second +Xe.second=Xe.seconds=Ue, +// Millisecond +Xe.millisecond=Xe.milliseconds=We, +// Offset +Xe.utcOffset=Db,Xe.utc=Fb,Xe.local=Gb,Xe.parseZone=Hb,Xe.hasAlignedHourOffset=Ib,Xe.isDST=Jb,Xe.isLocal=Lb,Xe.isUtcOffset=Mb,Xe.isUtc=Nb,Xe.isUTC=Nb, +// Timezone +Xe.zoneAbbr=Jc,Xe.zoneName=Kc, +// Deprecations +Xe.dates=x("dates accessor is deprecated. Use date instead.",Se),Xe.months=x("months accessor is deprecated. Use month instead",ka),Xe.years=x("years accessor is deprecated. Use year instead",pe),Xe.zone=x("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",Eb),Xe.isDSTShifted=x("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",Kb);var Ye=C.prototype;Ye.calendar=D,Ye.longDateFormat=E,Ye.invalidDate=F,Ye.ordinal=G,Ye.preparse=Nc,Ye.postformat=Nc,Ye.relativeTime=H,Ye.pastFuture=I,Ye.set=A, +// Month +Ye.months=fa,Ye.monthsShort=ga,Ye.monthsParse=ia,Ye.monthsRegex=na,Ye.monthsShortRegex=ma, +// Week +Ye.week=ya,Ye.firstDayOfYear=Aa,Ye.firstDayOfWeek=za, +// Day of Week +Ye.weekdays=Fa,Ye.weekdaysMin=Ha,Ye.weekdaysShort=Ga,Ye.weekdaysParse=Ja,Ye.weekdaysRegex=Na,Ye.weekdaysShortRegex=Oa,Ye.weekdaysMinRegex=Pa, +// Hours +Ye.isPM=Va,Ye.meridiem=Wa,$a("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(a){var b=a%10,c=1===u(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}), +// Side effect imports +a.lang=x("moment.lang is deprecated. Use moment.locale instead.",$a),a.langData=x("moment.langData is deprecated. Use moment.localeData instead.",bb);var Ze=Math.abs,$e=ed("ms"),_e=ed("s"),af=ed("m"),bf=ed("h"),cf=ed("d"),df=ed("w"),ef=ed("M"),ff=ed("y"),gf=gd("milliseconds"),hf=gd("seconds"),jf=gd("minutes"),kf=gd("hours"),lf=gd("days"),mf=gd("months"),nf=gd("years"),of=Math.round,pf={s:45,// seconds to minute +m:45,// minutes to hour +h:22,// hours to day +d:26,// days to month +M:11},qf=Math.abs,rf=wb.prototype; +// Deprecations +// Side effect imports +// FORMATTING +// PARSING +// Side effect imports +return rf.abs=Wc,rf.add=Yc,rf.subtract=Zc,rf.as=cd,rf.asMilliseconds=$e,rf.asSeconds=_e,rf.asMinutes=af,rf.asHours=bf,rf.asDays=cf,rf.asWeeks=df,rf.asMonths=ef,rf.asYears=ff,rf.valueOf=dd,rf._bubble=_c,rf.get=fd,rf.milliseconds=gf,rf.seconds=hf,rf.minutes=jf,rf.hours=kf,rf.days=lf,rf.weeks=hd,rf.months=mf,rf.years=nf,rf.humanize=md,rf.toISOString=nd,rf.toString=nd,rf.toJSON=nd,rf.locale=lc,rf.localeData=mc,rf.toIsoString=x("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",nd),rf.lang=Re,U("X",0,0,"unix"),U("x",0,0,"valueOf"),Z("x",Vd),Z("X",Yd),ba("X",function(a,b,c){c._d=new Date(1e3*parseFloat(a,10))}),ba("x",function(a,b,c){c._d=new Date(u(a))}),a.version="2.17.1",b(sb),a.fn=Xe,a.min=ub,a.max=vb,a.now=Le,a.utc=k,a.unix=Lc,a.months=Rc,a.isDate=g,a.locale=$a,a.invalid=o,a.duration=Ob,a.isMoment=s,a.weekdays=Tc,a.parseZone=Mc,a.localeData=bb,a.isDuration=xb,a.monthsShort=Sc,a.weekdaysMin=Vc,a.defineLocale=_a,a.updateLocale=ab,a.locales=cb,a.weekdaysShort=Uc,a.normalizeUnits=K,a.relativeTimeRounding=kd,a.relativeTimeThreshold=ld,a.calendarFormat=Ub,a.prototype=Xe,a}); \ No newline at end of file diff --git a/pocwww/static/pyramid-16x16.png b/pocwww/static/pyramid-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..979203112e76ba4cfdb8cd6f108f4275e987d99a GIT binary patch literal 1319 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n3Xd_B1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxR5#hc&_u!9QqR!T z(8R(}N5ROz&{*HVSl`fC*U-qyz|zXlQ~?TIxIyg#@@$ndN=gc>^!3Zj z%k|2Q_413-^$jg8E%gnI^o@*kfhu&1EAvVcD|GXUm0>2hq!uR^WfqiV=I1GZOiWD5 zFD$Tv3bSNU;+l1ennz|zM-B0$V)JVzP|XC=H|jx7ncO3BHWAB;NpiyW)Z+ZoqGVvir744~DzI`cN=+=uFAB-e&w+(vKt_H^esM;Afr7KMf`)Hma%LWg zuL;)R>ucqiS6q^qmz?V9Vygr+LN7Bj#mdRl*wM}0&C<--)xyxw)!5S9(bdAp)YZVy zz`)Yd%><^`B|o_|H#M&WrZ)wl*Ab^)P+G_>0NU)5T9jFqn&MWJpQ`}&vsET;x0vHJ z52`l>w_7Z5>eUB2MjsTjNHGl)0wy026P|8?9C*r4%>yR)B4E1CQ{KVEz`!`m)5S5Q z;#SXOUk#T)k>lxKj4~&v95M;sSJp8lNikLV)bX~~P1`qaNLZPZ<6^QgASli8jmk<% zZdJ~1%}JBkHhu^^q;ghbe{lMjv_0X)ug#yI+xz_S{hiM(g*saf_f6Pt#!wis>2u+! z{;RjXUlmP?^Kq|yJMn}2uJ~!L3EU-*{+zn6Ya6$jYueOB4G#Lx+=R;oM4sqe*Sq0$ zQ2Nf{-BFQxlX@Dog;`l=SkyQh`W)n22F}BoCTQYYC=p8g*kiK(1`FEnD$cY`NZX8<>GJicU)2$Vj5iRl-7k*od z3K)m{Gsp@4JM)eDo7z=gtG;>hu{QvcXxLU8eD@D+$BKJ@RM`zyYKvG z-8a3ur|aAMt1Vr-yH|CEDauQLkO+_f002lzQdIf%fB4Ui0QY*V)U3(^0FZ<%L_`#& zL`1-fj&^1i)}{b}Bq%eIA9uoG!laCfFcwoR zb`OTl9xm%u?u}UK6Z+-0LfvF1uNzRlu;BSs+a-xXQEAzvn#Z125}lrEE$o@!cYog? z@lko^ANF`uyQDsu%o2*s(%P^-sbKEJ1>90;Svb?qHr@sbgo4>hFv21pO(baNe1U?G_am z$%uaYhJupCKc=2k-Lpftu1m0%A~@dHZKRf6W*s6Qm&D`7K|3 zP8#?(KABe7=AZNd-k*6CTcqHJ?f3yA6ws8mf*wHc;}7VpNW)zn=9RJ4PSI>0zxN+V zk#)jtw`7ILRrYRCqD>sB@)+LaZvtH~TpCmeT z5;T(}&;kNeCnT`+Is{plpj-ki?E!QC9#b�i5=5IxreNAbVsKKM4p@aIXvt)VjX~ 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 li > a { + padding-right: 20px; + padding-left: 20px; +} +.nav-sidebar > .active > a, +.nav-sidebar > .active > a:hover, +.nav-sidebar > .active > a:focus { + color: #fff; + background-color: #428bca; +} + + +/* + * Main content + */ + +.main { + padding: 20px; +} +@media (min-width: 768px) { + .main { + padding-right: 40px; + padding-left: 40px; + } +} +.main .page-header { + margin-top: 0; +} + + +/* + * Placeholder dashboard ideas + */ + +.placeholders { + margin-bottom: 30px; + text-align: center; +} +.placeholders h4 { + margin-bottom: 0; +} +.placeholder { + margin-bottom: 20px; +} +.placeholder img { + display: inline-block; + border-radius: 50%; +} + +.card { + font: 10px sans-serif; +} + +.axis path, +.axis line { + fill: none; + stroke: #000; + shape-rendering: crispEdges; +} + +.grid path, +.grid line { + fill: none; + stroke: rgba(0, 0, 0, 0.25); + shape-rendering: crispEdges; +} + +.x.axis path { + display: none; +} + +.line { + fill: none; + stroke-width: 2.5px; +} + +#time-range p { + font-family:"Arial", sans-serif; + font-size:14px; + color:#333; +} +.ui-slider-horizontal { + height: 8px; + background: #D7D7D7; + border: 1px solid #BABABA; + box-shadow: 0 1px 0 #FFF, 0 1px 0 #CFCFCF inset; + clear: both; + margin: 8px 0; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + -ms-border-radius: 6px; + -o-border-radius: 6px; + border-radius: 6px; +} +.ui-slider { + position: relative; + text-align: left; +} +.ui-slider-horizontal .ui-slider-range { + top: -1px; + height: 100%; +} +.ui-slider .ui-slider-range { + position: absolute; + z-index: 1; + height: 8px; + font-size: .7em; + display: block; + border: 1px solid #5BA8E1; + box-shadow: 0 1px 0 #AAD6F6 inset; + -moz-border-radius: 6px; + -webkit-border-radius: 6px; + -khtml-border-radius: 6px; + border-radius: 6px; + background: #81B8F3; + background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgi…pZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); + background-size: 100%; + background-image: -webkit-gradient(linear, 50% 0, 50% 100%, color-stop(0%, #A0D4F5), color-stop(100%, #81B8F3)); + background-image: -webkit-linear-gradient(top, #A0D4F5, #81B8F3); + background-image: -moz-linear-gradient(top, #A0D4F5, #81B8F3); + background-image: -o-linear-gradient(top, #A0D4F5, #81B8F3); + background-image: linear-gradient(top, #A0D4F5, #81B8F3); +} +.ui-slider .ui-slider-handle { + border-radius: 50%; + background: #F9FBFA; + background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgi…pZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); + background-size: 100%; + background-image: -webkit-gradient(linear, 50% 0, 50% 100%, color-stop(0%, #C7CED6), color-stop(100%, #F9FBFA)); + background-image: -webkit-linear-gradient(top, #C7CED6, #F9FBFA); + background-image: -moz-linear-gradient(top, #C7CED6, #F9FBFA); + background-image: -o-linear-gradient(top, #C7CED6, #F9FBFA); + background-image: linear-gradient(top, #C7CED6, #F9FBFA); + width: 22px; + height: 22px; + -webkit-box-shadow: 0 2px 3px -1px rgba(0, 0, 0, 0.6), 0 -1px 0 1px rgba(0, 0, 0, 0.15) inset, 0 1px 0 1px rgba(255, 255, 255, 0.9) inset; + -moz-box-shadow: 0 2px 3px -1px rgba(0, 0, 0, 0.6), 0 -1px 0 1px rgba(0, 0, 0, 0.15) inset, 0 1px 0 1px rgba(255, 255, 255, 0.9) inset; + box-shadow: 0 2px 3px -1px rgba(0, 0, 0, 0.6), 0 -1px 0 1px rgba(0, 0, 0, 0.15) inset, 0 1px 0 1px rgba(255, 255, 255, 0.9) inset; + -webkit-transition: box-shadow .3s; + -moz-transition: box-shadow .3s; + -o-transition: box-shadow .3s; + transition: box-shadow .3s; +} +.ui-slider .ui-slider-handle { + position: absolute; + z-index: 2; + width: 22px; + height: 22px; + cursor: default; + border: none; + cursor: pointer; +} +.ui-slider .ui-slider-handle:after { + content:""; + position: absolute; + width: 8px; + height: 8px; + border-radius: 50%; + top: 50%; + margin-top: -4px; + left: 50%; + margin-left: -4px; + background: #30A2D2; + -webkit-box-shadow: 0 1px 1px 1px rgba(22, 73, 163, 0.7) inset, 0 1px 0 0 #FFF; + -moz-box-shadow: 0 1px 1px 1px rgba(22, 73, 163, 0.7) inset, 0 1px 0 0 white; + box-shadow: 0 1px 1px 1px rgba(22, 73, 163, 0.7) inset, 0 1px 0 0 #FFF; +} +.ui-slider-horizontal .ui-slider-handle { + top: -.5em; + margin-left: -.6em; +} +.ui-slider a:focus { + outline:none; +} + +.slider-time { + + +} diff --git a/pocwww/templates/admin.jinja2 b/pocwww/templates/admin.jinja2 new file mode 100644 index 0000000..e4f8d61 --- /dev/null +++ b/pocwww/templates/admin.jinja2 @@ -0,0 +1,59 @@ +{% from 'pagination.jinja2' import render_pagination %} +{% extends "layout.jinja2" %} + +{% block content %} + +
+
+
+

Well Configuration

+
+
+ + +
+
+ + + +
+
+
+
+ + +{% endblock content %} diff --git a/pocwww/templates/cardlist.jinja2 b/pocwww/templates/cardlist.jinja2 new file mode 100644 index 0000000..9c610eb --- /dev/null +++ b/pocwww/templates/cardlist.jinja2 @@ -0,0 +1,40 @@ +{% from 'pagination.jinja2' import render_pagination %} + + +{% extends "layout.jinja2" %} + +{% block content %} +
+

Cards for {{cards_date | datestring('long')}}

+ +
+ {{render_pagination(pagination, "/cards/" + cards_date)}} +
+ + + + + + + + + {% for card in cards %} + + + + + {% endfor %} + +
StrokeDatetime
{{card.strokeNumber}}{{card.timestamp | datetime('long')}}
+
+{% endblock content %} + +{% block card_dates %} +

Card Dates

+ +{% endblock card_dates %} diff --git a/pocwww/templates/cardsingle.jinja2 b/pocwww/templates/cardsingle.jinja2 new file mode 100644 index 0000000..355c90c --- /dev/null +++ b/pocwww/templates/cardsingle.jinja2 @@ -0,0 +1,24 @@ +{% extends "layout.jinja2" %} + +{% block content %} + +
+ BACK +

{{card.timestamp | datetime('long')}}

+ + + + +
+ + +{% endblock content %} diff --git a/pocwww/templates/config.jinja2 b/pocwww/templates/config.jinja2 new file mode 100644 index 0000000..6d5c8c0 --- /dev/null +++ b/pocwww/templates/config.jinja2 @@ -0,0 +1,174 @@ +{% from 'pagination.jinja2' import render_pagination %} +{% extends "layout.jinja2" %} + +{% block content %} + +
+
+
+
+

Well Configuration

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+

Taper Setup

+
+ + + + + + + + + + + + + {% for taper in config.tapers %} + + + + + + + + + {% endfor %} + + +
TaperLengthDiameterMaterialDamping Factor
{{loop.index}}
+ +
+ + + + + + +
+
+
+
+
+ + + {% endblock content %} diff --git a/pocwww/templates/dashboard.jinja2 b/pocwww/templates/dashboard.jinja2 new file mode 100644 index 0000000..e5415d8 --- /dev/null +++ b/pocwww/templates/dashboard.jinja2 @@ -0,0 +1,92 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+
+
+

Run Status

+

+

at

+
+ + +
+
+
+
+

Latest Tag Values

+ {% if tag_values|length > 0 %} +
+ + + + + + + + + + + + + {% for t in tag_values %} + + + + + + + + {% endfor %} + + +
NameValueTimestamp
{{t._id}} + + {{t.value | round(3)}}{{t.timestamp | datetime('short')}}
+
+ {% else %} +

No tag values yet...

+ {% endif %} + + +
+
+

Latest Card

+ {% if card is defined %} +

{{card.timestamp | datetime('long')}}

+ + + {% else %} +

No card data yet...

+ {% endif %} +
+
+
+ + + {% endblock content %} diff --git a/pocwww/templates/datelist.jinja2 b/pocwww/templates/datelist.jinja2 new file mode 100644 index 0000000..501002b --- /dev/null +++ b/pocwww/templates/datelist.jinja2 @@ -0,0 +1,23 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Card Dates

+ + + + + + + + + {% for day in datelist %} + + + + + {% endfor %} + +
DateCards
{{day.date | date('long')}}{{day.count}} cards
+
+{% endblock content %} diff --git a/pocwww/templates/fluidshots.jinja2 b/pocwww/templates/fluidshots.jinja2 new file mode 100644 index 0000000..141ba0d --- /dev/null +++ b/pocwww/templates/fluidshots.jinja2 @@ -0,0 +1,37 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+
+
+

Fluid Shots

+ {% if data|length > 0 %} +
+ + + + + + + + + + + {% for d in data %} + + + + + + + {% endfor %} + +
TimestampFluid LevelPump Intake PressureFriction Estimate
{{d.timestamp | datetime('medium')}}{{d.fluidLevel | round(3)}}{{d.pumpIntakePressure | round(3)}}{{d.frictionEstimate | round(3)}}
+
+ {% else %} +

No fluid shots yet...

+ {% endif %} +
+
+
+{% endblock content %} diff --git a/pocwww/templates/gaugeoff_all.jinja2 b/pocwww/templates/gaugeoff_all.jinja2 new file mode 100644 index 0000000..0723fbe --- /dev/null +++ b/pocwww/templates/gaugeoff_all.jinja2 @@ -0,0 +1,43 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+
+
+

Gauge Off Data

+ {% if data is defined %} + + {% for d in data %} +

{{d.date|date('long')}}

+
+ + + + + + + + + + + + + {% for t in d.tags %} + + + + + + + + + {% endfor %} + +
NameLastMaxMinAverageTotal
{{t}}{{d['tags'][t].last | round(3)}}{{d['tags'][t].max | round(3)}}{{d['tags'][t].min | round(3)}}{{d['tags'][t].average | round(3)}}{{d['tags'][t].total | round(3)}}
+
+ {% endfor %} + {% endif %} +
+
+
+{% endblock content %} diff --git a/pocwww/templates/layout.jinja2 b/pocwww/templates/layout.jinja2 new file mode 100644 index 0000000..c76c11b --- /dev/null +++ b/pocwww/templates/layout.jinja2 @@ -0,0 +1,194 @@ + + + + + + + + + + + + Henry POC + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ {% block content %} +

No content

+ {% endblock content %} +
+
+
+ + + + + + + + + diff --git a/pocwww/templates/pagination.jinja2 b/pocwww/templates/pagination.jinja2 new file mode 100644 index 0000000..48c433f --- /dev/null +++ b/pocwww/templates/pagination.jinja2 @@ -0,0 +1,41 @@ +{% macro render_pagination(pagination, endpoint) %} + +
    + + {% if pagination.has_prev %} +
  • + + + +
  • + {% endif %} + + {% for p in pagination.iter_pages(left_edge=1, left_current=2, right_current=3, right_edge=1) %} + {% if p %} + {% if p != pagination.page %} +
  • + {{ p }} +
  • + {% else %} +
  • + {{ p }} +
  • + {% endif %} + {% else %} +
  • + +
  • + {% endif %} + {% endfor %} + + {% if pagination.has_next %} +
  • + + + +
  • + {% endif %} + +
+ +{% endmacro %} diff --git a/pocwww/templates/register.jinja2 b/pocwww/templates/register.jinja2 new file mode 100644 index 0000000..41c8f04 --- /dev/null +++ b/pocwww/templates/register.jinja2 @@ -0,0 +1,187 @@ +{% extends "layout.jinja2" %} + +{% block content %} + + {% if request.authenticated_userid %} + +

User Management

+
+

New User

+
+
+ +
+
+ +
+
+ +
+
+ +
+ + + +
+

Change Password

+
+
+ + +
+
+ +
+
+
+ + + +
+ + + +

All Users

+
+ + + + + + + + + +
Username
+
+ + + {% endif %} +{% endblock content %} diff --git a/pocwww/templates/runstatus.jinja2 b/pocwww/templates/runstatus.jinja2 new file mode 100644 index 0000000..b050ff1 --- /dev/null +++ b/pocwww/templates/runstatus.jinja2 @@ -0,0 +1,39 @@ +{% from 'pagination.jinja2' import render_pagination %} +{% extends "layout.jinja2" %} + +{% block content %} +
+
+
+

Run Status Log

+ {% if data|length > 0 %} +
+ {{render_pagination(pagination, "/runstatus")}} +
+
+ + + + + + + + + + {% for d in data %} + + + + + + {% endfor %} + +
TimestampStatusInitiator
{{d.timestamp | datetime('medium')}}{{d.status}}{{d.initiator}}
+
+ {% else %} +

No statuses stored yet...

+ {% endif %} +
+
+
+{% endblock content %} diff --git a/pocwww/templates/setpoints.jinja2 b/pocwww/templates/setpoints.jinja2 new file mode 100644 index 0000000..3cd6512 --- /dev/null +++ b/pocwww/templates/setpoints.jinja2 @@ -0,0 +1,127 @@ + +{% extends "layout.jinja2" %} + +{% block content %} + +
+
+
+
+

Run Mode: Loading...

+ + + +

+

Set by Loading... at Loading... +

+
+
+
+

POC Setpoints

+
+ + + + + + + + + + + + +
NameValueStored ByLast Time Stored
+
+
+
+
+
+ + + {% endblock content %} diff --git a/pocwww/templates/values_single.jinja2 b/pocwww/templates/values_single.jinja2 new file mode 100644 index 0000000..8012303 --- /dev/null +++ b/pocwww/templates/values_single.jinja2 @@ -0,0 +1,57 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+
+
+

Latest Tag Values: {{tagname}}

+
+
+
+
+
+

+
+
+
+
+
+
+
+
+ +
+
+
+ + + +{% endblock content %} diff --git a/pocwww/templates/valuesall.jinja2 b/pocwww/templates/valuesall.jinja2 new file mode 100644 index 0000000..6ef108a --- /dev/null +++ b/pocwww/templates/valuesall.jinja2 @@ -0,0 +1,94 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+
+
+

Latest Tag Values

+
+ + + + + + + + + + + + + + {% for t in current_tag_values %} + + + + + + + + + + {% endfor %} + +
NameValueMaxMinAverageTotalLast Stored
{{t._id}}{{t.value | round(3)}}{{t.max | round(3)}}{{t.min | round(3)}}{{t.average | round(3)}}{{t.total | round(3)}}{{t.timestamp | datetime('medium')}}
+
+ +
+
+
+
+ +
+
+
+
+
+

+
+
+
+
+
+ +
+
+
+
+ + +{% endblock content %} diff --git a/pocwww/templates/welltests.jinja2 b/pocwww/templates/welltests.jinja2 new file mode 100644 index 0000000..c8daea5 --- /dev/null +++ b/pocwww/templates/welltests.jinja2 @@ -0,0 +1,49 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+
+
+

Well Tests

+ {% if data|length > 0 %} +
+ + + + + + + + + + + + + + + + + {% for d in data %} + + + + + + + + + + + + + {% endfor %} + +
Test StartTest HoursTotal BBLOil BBLWater BBLGas MCFkFactorOil RatioWater RatioGas Ratio
{{d.testStartTime | datetime('medium')}}{{d.testHours}}{{d.testTotalBBL | round(3)}}{{d.testOilBBL | round(3)}}{{d.testWaterBBL | round(3)}}{{d.testGasMCF | round(3)}}{{d.kFactor | round(3)}}{{d.oilRatio | round(3)}}{{d.waterRatio | round(3)}}{{d.gasMCFRatio | round(3)}}
+
+ {% else %} +

No Well Tests yet...

+ {% endif %} +
+
+
+{% endblock content %} diff --git a/pocwww/tests.py b/pocwww/tests.py new file mode 100644 index 0000000..39c284a --- /dev/null +++ b/pocwww/tests.py @@ -0,0 +1,29 @@ +import unittest + +from pyramid import testing + + +class ViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_my_view(self): + from .views import my_view + request = testing.DummyRequest() + info = my_view(request) + self.assertEqual(info['project'], 'POC Web Interface') + + +class FunctionalTests(unittest.TestCase): + def setUp(self): + from pocwww import main + app = main({}) + from webtest import TestApp + self.testapp = TestApp(app) + + def test_root(self): + res = self.testapp.get('/', status=200) + self.assertTrue(b'Pyramid' in res.body) diff --git a/pocwww/view_helpers.py b/pocwww/view_helpers.py new file mode 100644 index 0000000..203d204 --- /dev/null +++ b/pocwww/view_helpers.py @@ -0,0 +1,108 @@ +from datetime import datetime, timedelta +from math import ceil +from passlib.apps import custom_app_context as poc_pwd_context +from dateutil import tz +from .pagination import Pagination + +from_zone = tz.tzutc() +to_zone = tz.tzlocal() + + +def get_lastest_tag_values(request): + latest_tag_values = [] + latesttag_agg = request.db['wellData'].aggregate([ + { + '$sort': {"tagname": 1, "timestamp": 1} + }, + { + '$group': { + '_id': "$tagname", + 'timestamp': {'$last': "$timestamp"}, + 'value': {'$last': "$currentValue"}, + 'max': {'$last': "$maxDailyValue"}, + 'min': {'$last': "$minDailyValue"}, + 'average': {'$last': "$dailyAverage"}, + 'total': {'$last': "$dailyTotal"} + } + } + ]) + for t in latesttag_agg: + latest_tag_values.append(t) + return latest_tag_values + + +def get_latest_card(request): + try: + latest_card = list(request.db['cards'].find().sort("timestamp", -1).limit(1))[0] + return latest_card + except IndexError: + return [] + + +def card_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'} + + +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) + + +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 + + +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/views.py b/pocwww/views.py new file mode 100644 index 0000000..9c3163e --- /dev/null +++ b/pocwww/views.py @@ -0,0 +1,172 @@ +from pyramid.view import view_config +from pyramid.httpexceptions import HTTPFound +from pyramid.security import remember, forget +from datetime import datetime, timedelta +from math import ceil +from .view_helpers import * + + +@view_config(route_name='home', renderer='templates/dashboard.jinja2') +@view_config(route_name="json_snapshot", renderer="prettyjson") +def my_view(request): + return {'project': 'POC Web Interface', 'navgroup': 'dashboard', 'tag_values': get_lastest_tag_values(request), 'card': get_latest_card(request)} + + +@view_config(route_name='cards', renderer='templates/datelist.jinja2') +@view_config(route_name='json_cards', 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='json_cards_date', renderer='prettyjson') +@view_config(route_name='json_cards_page', renderer='prettyjson') +def cards_page(request): + cp = card_page(request) + cp['datelist'] = get_all_dates_with_cards(request) + return cp + + +@view_config(route_name='card_single', renderer='templates/cardsingle.jinja2') +@view_config(route_name='json_card_single', 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} + + +@view_config(route_name='values_all', renderer="templates/valuesall.jinja2") +@view_config(route_name='json_values_all', renderer='prettyjson') +def values_all(request): + return {'navgroup': 'values', 'current_tag_values': get_lastest_tag_values(request)} + + +@view_config(route_name="values_tag", renderer="templates/values_single.jinja2") +def values_tag(request): + tagname = request.matchdict['tagname'] + return {'navgroup': 'values', 'tagname': tagname} + + +@view_config(route_name="gaugeoff_all", renderer="templates/gaugeoff_all.jinja2") +@view_config(route_name="json_gaugeoff_all", 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} + + +@view_config(route_name="fluidshots_all", renderer="templates/fluidshots.jinja2") +@view_config(route_name="json_fluidshots_all", renderer="prettyjson") +def fluidshots_all(request): + fluidshots = list(request.db['fluidShots'].find()) + return {'navgroup': 'fluidshots', 'data': fluidshots} + + +@view_config(route_name="welltests_all", renderer="templates/welltests.jinja2") +@view_config(route_name="json_welltests_all", renderer="prettyjson") +def welltests_all(request): + welltests = list(request.db['wellTests'].find()) + return {'navgroup': 'welltests', 'data': welltests} + + +@view_config(route_name="runstatus", renderer="templates/runstatus.jinja2") +@view_config(route_name="json_runstatus", renderer="prettyjson") +@view_config(route_name="json_runstatus_page", 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="config", renderer="templates/config.jinja2", permission="edit") +@view_config(route_name="json_config", 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} + + +@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} + + +@view_config(route_name="auth") +def sign_in_out(request): + username = request.POST.get('username') + if username: + if check_password(request, username, request.POST.get('password')): + headers = remember(request, username) + else: + headers = forget(request) + else: + headers = forget(request) + return HTTPFound(location=request.route_url('home'), headers=headers) + + +@view_config(route_name='register', + renderer='templates/register.jinja2', permission="edit") +def register(request): + return {"navgroup": "user"} + + +@view_config(route_name='setpoints', renderer='templates/setpoints.jinja2', permission='edit') +def setpoints(request): + return {"navgroup": "setpoints"} diff --git a/production.ini b/production.ini new file mode 100644 index 0000000..5b98c2f --- /dev/null +++ b/production.ini @@ -0,0 +1,66 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:pocwww] +use = egg:pocwww#main +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +mongo_uri = mongodb://localhost:27017/poc + + +#---------- Pipeline Configuration ---------- +[filter:paste_prefix] +use = egg:PasteDeploy#prefix + +[pipeline:main] +pipeline = + paste_prefix + pocwww + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +host = 127.0.0.1 +port = %(http_port)s + + + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, pocwww + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_pocwww] +level = WARN +handlers = +qualname = pocwww + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..1bdc32f --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = pocwww +python_files = *.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..dc0377d --- /dev/null +++ b/setup.py @@ -0,0 +1,55 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'waitress', + 'pymongo', + 'requests', + 'passlib', + 'python-dateutil', +] + +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', + 'pytest-cov', +] + +setup( + name='pocwww', + version='0.0', + description='POC Web Interface', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + 'Programming Language :: Python', + 'Framework :: Pyramid', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', + ], + author='', + author_email='', + url='', + keywords='web pyramid pylons', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + extras_require={ + 'testing': tests_require, + }, + install_requires=requires, + entry_points={ + 'paste.app_factory': [ + 'main = pocwww:main', + ], + }, +) diff --git a/supervisor.conf b/supervisor.conf new file mode 100644 index 0000000..0ca2be7 --- /dev/null +++ b/supervisor.conf @@ -0,0 +1,27 @@ +[unix_http_server] +file=%(here)s/env/supervisor.sock + +[supervisord] +pidfile=%(here)s/env/supervisord.pid +logfile=%(here)s/env/supervisord.log +logfile_maxbytes=50MB +logfile_backups=10 +loglevel=info +nodaemon=false +minfds=1024 +minprocs=200 + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl=unix://%(here)s/env/supervisor.sock + +[program:pocwww] +autorestart=true +command=%(here)s/env/bin/pserve %(here)s/production.ini http_port=50%(process_num)02d +process_name=%(program_name)s-%(process_num)01d +numprocs=2 +numprocs_start=0 +redirect_stderr=true +stdout_logfile=%(here)s/env/%(program_name)s-%(process_num)01d.log