Initial Commit after moving from POC-Java repo

This commit is contained in:
Patrick McDonagh
2017-03-09 18:12:18 -06:00
commit 072974c8c5
40 changed files with 3787 additions and 0 deletions

3
.coveragerc Normal file
View File

@@ -0,0 +1,3 @@
[run]
source = pocwww
omit = pocwww/test*

4
CHANGES.txt Normal file
View File

@@ -0,0 +1,4 @@
0.0
---
- Initial version.

2
MANIFEST.in Normal file
View File

@@ -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

29
README.txt Normal file
View File

@@ -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

63
development.ini Normal file
View File

@@ -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

7
generate_cert.sh Normal file
View File

@@ -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/

46
poc.conf Normal file
View File

@@ -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;
}
}

194
pocwww/__init__.py Normal file
View File

@@ -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()

293
pocwww/json.py Normal file
View File

@@ -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"}

36
pocwww/pagination.py Normal file
View File

@@ -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

10
pocwww/security.py Normal file
View File

@@ -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

280
pocwww/static/graphs.js Normal file
View File

@@ -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
});
}
});
}

View File

@@ -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();
}

551
pocwww/static/moment.min.js vendored Normal file
View File

@@ -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;c<a.length;++c)d.push(b(a[c],c));return d}function i(a,b){return Object.prototype.hasOwnProperty.call(a,b)}function j(a,b){for(var c in b)i(b,c)&&(a[c]=b[c]);return i(b,"toString")&&(a.toString=b.toString),i(b,"valueOf")&&(a.valueOf=b.valueOf),a}function k(a,b,c,d){return rb(a,b,c,d,!0).utc()}function l(){
// We need to deep clone this object.
return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1,parsedDateParts:[],meridiem:null}}function m(a){return null==a._pf&&(a._pf=l()),a._pf}function n(a){if(null==a._isValid){var b=m(a),c=qd.call(b.parsedDateParts,function(a){return null!=a}),d=!isNaN(a._d.getTime())&&b.overflow<0&&!b.empty&&!b.invalidMonth&&!b.invalidWeekday&&!b.nullInput&&!b.invalidFormat&&!b.userInvalidated&&(!b.meridiem||b.meridiem&&c);if(a._strict&&(d=d&&0===b.charsLeftOver&&0===b.unusedTokens.length&&void 0===b.bigHour),null!=Object.isFrozen&&Object.isFrozen(a))return d;a._isValid=d}return a._isValid}function o(a){var b=k(NaN);return null!=a?j(m(b),a):m(b).userInvalidated=!0,b}function p(a){return void 0===a}function q(a,b){var c,d,e;if(p(b._isAMomentObject)||(a._isAMomentObject=b._isAMomentObject),p(b._i)||(a._i=b._i),p(b._f)||(a._f=b._f),p(b._l)||(a._l=b._l),p(b._strict)||(a._strict=b._strict),p(b._tzm)||(a._tzm=b._tzm),p(b._isUTC)||(a._isUTC=b._isUTC),p(b._offset)||(a._offset=b._offset),p(b._pf)||(a._pf=m(b)),p(b._locale)||(a._locale=b._locale),rd.length>0)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;d<e;d++)(c&&a[d]!==b[d]||!c&&u(a[d])!==u(b[d]))&&g++;return g+f}function w(b){a.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+b)}function x(b,c){var d=!0;return j(function(){if(null!=a.deprecationHandler&&a.deprecationHandler(null,b),d){for(var e,f=[],g=0;g<arguments.length;g++){if(e="","object"==typeof arguments[g]){e+="\n["+g+"] ";for(var h in arguments[0])e+=h+": "+arguments[0][h]+", ";e=e.slice(0,-2)}else e=arguments[g];f.push(e)}w(b+"\nArguments: "+Array.prototype.slice.call(f).join("")+"\n"+(new Error).stack),d=!1}return c.apply(this,arguments)},c)}function y(b,c){null!=a.deprecationHandler&&a.deprecationHandler(b,c),td[b]||(w(c),td[b]=!0)}function z(a){return a instanceof Function||"[object Function]"===Object.prototype.toString.call(a)}function A(a){var b,c;for(c in a)b=a[c],z(b)?this[c]=b:this["_"+c]=b;this._config=a,
// Lenient ordinal parsing accepts just a number in addition to
// number + (possibly) stuff coming from _ordinalParseLenient.
this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function B(a,b){var c,e=j({},a);for(c in b)i(b,c)&&(d(a[c])&&d(b[c])?(e[c]={},j(e[c],a[c]),j(e[c],b[c])):null!=b[c]?e[c]=b[c]:delete e[c]);for(c in a)i(a,c)&&!i(b,c)&&d(a[c])&&(
// make sure changes to properties don't modify parent config
e[c]=j({},e[c]));return e}function C(a){null!=a&&this.set(a)}function D(a,b,c){var d=this._calendar[a]||this._calendar.sameElse;return z(d)?d.call(b,c):d}function E(a){var b=this._longDateFormat[a],c=this._longDateFormat[a.toUpperCase()];return b||!c?b:(this._longDateFormat[a]=c.replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a])}function F(){return this._invalidDate}function G(a){return this._ordinal.replace("%d",a)}function H(a,b,c,d){var e=this._relativeTime[c];return z(e)?e(a,b,c,d):e.replace(/%d/i,a)}function I(a,b){var c=this._relativeTime[a>0?"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<c.length;d++)this[c[d].unit](a[c[d].unit])}else if(a=K(a),z(this[a]))return this[a](b);return this}function T(a,b,c){var d=""+Math.abs(a),e=b-d.length,f=a>=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<c;b++)Id[d[b]]?d[b]=Id[d[b]]:d[b]=V(d[b]);return function(b){var e,f="";for(e=0;e<c;e++)f+=d[e]instanceof Function?d[e].call(b,a):d[e];return f}}
// format date using native date object
function X(a,b){return a.isValid()?(b=Y(b,a.localeData()),Hd[b]=Hd[b]||W(b),Hd[b](a)):a.localeData().invalidDate()}function Y(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(Gd.lastIndex=0;d>=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<a.length;c++)_d[a[c]]=d}function ca(a,b){ba(a,function(a,c,d,e){d._w=d._w||{},b(a,d._w,d,e)})}function da(a,b,c){null!=b&&i(_d,a)&&_d[a](b,c._a,c,a)}function ea(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function fa(a,b){return a?c(this._months)?this._months[a.month()]:this._months[(this._months.isFormat||ke).test(b)?"format":"standalone"][a.month()]:this._months}function ga(a,b){return a?c(this._monthsShort)?this._monthsShort[a.month()]:this._monthsShort[ke.test(b)?"format":"standalone"][a.month()]:this._monthsShort}function ha(a,b,c){var d,e,f,g=a.toLocaleLowerCase();if(!this._monthsParse)for(
// this is not used
this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],d=0;d<12;++d)f=k([2e3,d]),this._shortMonthsParse[d]=this.monthsShort(f,"").toLocaleLowerCase(),this._longMonthsParse[d]=this.months(f,"").toLocaleLowerCase();return c?"MMM"===b?(e=je.call(this._shortMonthsParse,g),e!==-1?e:null):(e=je.call(this._longMonthsParse,g),e!==-1?e:null):"MMM"===b?(e=je.call(this._shortMonthsParse,g),e!==-1?e:(e=je.call(this._longMonthsParse,g),e!==-1?e:null)):(e=je.call(this._longMonthsParse,g),e!==-1?e:(e=je.call(this._shortMonthsParse,g),e!==-1?e:null))}function ia(a,b,c){var d,e,f;if(this._monthsParseExact)return ha.call(this,a,b,c);
// TODO: add sorting
// Sorting makes sure if one month (or abbr) is a prefix of another
// see sorting in computeMonthsParse
for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),d=0;d<12;d++){
// test the regex
if(
// make the regex if we don't have it already
e=k([2e3,d]),c&&!this._longMonthsParse[d]&&(this._longMonthsParse[d]=new RegExp("^"+this.months(e,"").replace(".","")+"$","i"),this._shortMonthsParse[d]=new RegExp("^"+this.monthsShort(e,"").replace(".","")+"$","i")),c||this._monthsParse[d]||(f="^"+this.months(e,"")+"|^"+this.monthsShort(e,""),this._monthsParse[d]=new RegExp(f.replace(".",""),"i")),c&&"MMMM"===b&&this._longMonthsParse[d].test(a))return d;if(c&&"MMM"===b&&this._shortMonthsParse[d].test(a))return d;if(!c&&this._monthsParse[d].test(a))return d}}
// MOMENTS
function ja(a,b){var c;if(!a.isValid())
// No op
return a;if("string"==typeof b)if(/^\d+$/.test(b))b=u(b);else
// TODO: Another silent failure?
if(b=a.localeData().monthsParse(b),!f(b))return a;return c=Math.min(a.date(),ea(a.year(),b)),a._d["set"+(a._isUTC?"UTC":"")+"Month"](b,c),a}function ka(b){return null!=b?(ja(this,b),a.updateOffset(this,!0),this):P(this,"Month")}function la(){return ea(this.year(),this.month())}function ma(a){return this._monthsParseExact?(i(this,"_monthsRegex")||oa.call(this),a?this._monthsShortStrictRegex:this._monthsShortRegex):(i(this,"_monthsShortRegex")||(this._monthsShortRegex=ne),this._monthsShortStrictRegex&&a?this._monthsShortStrictRegex:this._monthsShortRegex)}function na(a){return this._monthsParseExact?(i(this,"_monthsRegex")||oa.call(this),a?this._monthsStrictRegex:this._monthsRegex):(i(this,"_monthsRegex")||(this._monthsRegex=oe),this._monthsStrictRegex&&a?this._monthsStrictRegex:this._monthsRegex)}function oa(){function a(a,b){return b.length-a.length}var b,c,d=[],e=[],f=[];for(b=0;b<12;b++)
// make the regex if we don't have it already
c=k([2e3,b]),d.push(this.monthsShort(c,"")),e.push(this.months(c,"")),f.push(this.months(c,"")),f.push(this.monthsShort(c,""));for(
// Sorting makes sure if one month (or abbr) is a prefix of another it
// will match the longer piece.
d.sort(a),e.sort(a),f.sort(a),b=0;b<12;b++)d[b]=aa(d[b]),e[b]=aa(e[b]);for(b=0;b<24;b++)f[b]=aa(f[b]);this._monthsRegex=new RegExp("^("+f.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+e.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+d.join("|")+")","i")}
// HELPERS
function pa(a){return qa(a)?366:365}function qa(a){return a%4===0&&a%100!==0||a%400===0}function ra(){return qa(this.year())}function sa(a,b,c,d,e,f,g){
//can't just apply() to create a date:
//http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
var h=new Date(a,b,c,d,e,f,g);
//the date constructor remaps years 0-99 to 1900-1999
return a<100&&a>=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;f<a.length;){for(e=Xa(a[f]).split("-"),b=e.length,c=Xa(a[f+1]),c=c?c.split("-"):null;b>0;){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&&(b<ae||b>ce)&&(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;b<c;b++)if(Ge[b][1].exec(i[1])){e=Ge[b][0],d=Ge[b][2]!==!1;break}if(null==e)return void(a._isValid=!1);if(i[3]){for(b=0,c=He.length;b<c;b++)if(He[b][1].exec(i[3])){
// match[2] should be 'T' or space
f=(i[2]||" ")+He[b][0];break}if(null==f)return void(a._isValid=!1)}if(!d&&null!=f)return void(a._isValid=!1);if(i[4]){if(!Fe.exec(i[4]))return void(a._isValid=!1);g="Z"}a._f=e+(f||"")+(g||""),kb(a)}else a._isValid=!1}
// date from iso format or fallback
function fb(b){var c=Ie.exec(b._i);return null!==c?void(b._d=new Date(+c[1])):(eb(b),void(b._isValid===!1&&(delete b._isValid,a.createFromInputFallback(b))))}
// Pick the first defined of two or three arguments.
function gb(a,b,c){return null!=a?a:null!=b?b:c}function hb(b){
// hooks is actually the exported moment object
var c=new Date(a.now());return b._useUTC?[c.getUTCFullYear(),c.getUTCMonth(),c.getUTCDate()]:[c.getFullYear(),c.getMonth(),c.getDate()]}
// convert an array to a date.
// the array should mirror the parameters below
// note: all values past the year are optional and will default to the lowest possible value.
// [year, month, day , hour, minute, second, millisecond]
function ib(a){var b,c,d,e,f=[];if(!a._d){
// Default to current date.
// * if no year, month, day of month are given, default to today
// * if day of month is given, default month and year
// * if month is given, default only year
// * if year is given, don't default anything
for(d=hb(a),
//compute day of the year from weeks and weekdays
a._w&&null==a._a[ce]&&null==a._a[be]&&jb(a),
//if the day of the year is set, figure out what it is
a._dayOfYear&&(e=gb(a._a[ae],d[ae]),a._dayOfYear>pa(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;c<e.length;c++)f=e[c],d=(h.match($(f,b))||[])[0],
// console.log('token', token, 'parsedInput', parsedInput,
// 'regex', getParseRegexForToken(token, config));
d&&(g=h.substr(0,h.indexOf(d)),g.length>0&&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<a._f.length;e++)f=0,b=q({},a),null!=a._useUTC&&(b._useUTC=a._useUTC),b._f=a._f[e],kb(b),n(b)&&(
// if there is any input that was not parsed add a penalty for that format
f+=m(b).charsLeftOver,
//or tokens
f+=10*m(b).unusedTokens.length,m(b).score=f,(null==d||f<d)&&(d=f,c=b));j(a,c||b)}function nb(a){if(!a._d){var b=L(a._i);a._a=h([b.year,b.month,b.day||b.date,b.hour,b.minute,b.second,b.millisecond],function(a){return a&&parseInt(a,10)}),ib(a)}}function ob(a){var b=new r(db(pb(a)));
// Adding is smart enough around DST
return b._nextDay&&(b.add(1,"d"),b._nextDay=void 0),b}function pb(a){var b=a._i,d=a._f;return a._locale=a._locale||bb(a._l),null===b||void 0===d&&""===b?o({nullInput:!0}):("string"==typeof b&&(a._i=b=a._locale.preparse(b)),s(b)?new r(db(b)):(g(b)?a._d=b:c(d)?mb(a):d?kb(a):qb(a),n(a)||(a._d=null),a))}function qb(b){var d=b._i;void 0===d?b._d=new Date(a.now()):g(d)?b._d=new Date(d.valueOf()):"string"==typeof d?fb(b):c(d)?(b._a=h(d.slice(0),function(a){return parseInt(a,10)}),ib(b)):"object"==typeof d?nb(b):f(d)?
// from milliseconds
b._d=new Date(d):a.createFromInputFallback(b)}function rb(a,b,f,g,h){var i={};
// object construction must be done this way.
// https://github.com/moment/moment/issues/1423
return f!==!0&&f!==!1||(g=f,f=void 0),(d(a)&&e(a)||c(a)&&0===a.length)&&(a=void 0),i._isAMomentObject=!0,i._useUTC=i._isUTC=h,i._l=f,i._i=a,i._f=b,i._strict=g,ob(i)}function sb(a,b,c,d){return rb(a,b,c,d,!1)}
// Pick a moment m from moments so that m[fn](other) is true for all
// other. This relies on the function fn to be transitive.
//
// moments should either be an array of moment objects or an array, whose
// first element is an array of moment objects.
function tb(a,b){var d,e;if(1===b.length&&c(b[0])&&(b=b[0]),!b.length)return sb();for(d=b[0],e=1;e<b.length;++e)b[e].isValid()&&!b[e][a](d)||(d=b[e]);return d}
// TODO: Use [].sort instead?
function ub(){var a=[].slice.call(arguments,0);return tb("isBefore",a)}function vb(){var a=[].slice.call(arguments,0);return tb("isAfter",a)}function wb(a){var b=L(a),c=b.year||0,d=b.quarter||0,e=b.month||0,f=b.week||0,g=b.day||0,h=b.hour||0,i=b.minute||0,j=b.second||0,k=b.millisecond||0;
// representation for dateAddRemove
this._milliseconds=+k+1e3*j+// 1000
6e4*i+// 1000 * 60
1e3*h*60*60,//using 1000 * 60 * 60 instead of 36e5 to avoid floating point rounding errors https://github.com/moment/moment/issues/2978
// Because of dateAddRemove treats 24 hours as different from a
// day when working around DST, we need to store them separately
this._days=+g+7*f,
// It is impossible translate months into days without knowing
// which months you are are talking about, so we have to store
// it separately.
this._months=+e+3*d+12*c,this._data={},this._locale=bb(),this._bubble()}function xb(a){return a instanceof wb}function yb(a){return a<0?Math.round(-1*a)*-1:Math.round(a)}
// FORMATTING
function zb(a,b){U(a,0,0,function(){var a=this.utcOffset(),c="+";return a<0&&(a=-a,c="-"),c+T(~~(a/60),2)+b+T(~~a%60,2)})}function Ab(a,b){var c=(b||"").match(a);if(null===c)return null;var d=c[c.length-1]||[],e=(d+"").match(Me)||["-",0,0],f=+(60*e[1])+u(e[2]);return 0===f?0:"+"===e[0]?f:-f}
// Return a moment from input, that is local/utc/zone equivalent to model.
function Bb(b,c){var d,e;
// Use low-level api, because this fn is low-level api.
return c._isUTC?(d=c.clone(),e=(s(b)||g(b)?b.valueOf():sb(b).valueOf())-d.valueOf(),d._d.setTime(d._d.valueOf()+e),a.updateOffset(d,!1),d):sb(b).local()}function Cb(a){
// On Firefox.24 Date#getTimezoneOffset returns a floating point.
// https://github.com/moment/moment/pull/1871
return 15*-Math.round(a._d.getTimezoneOffset()/15)}
// MOMENTS
// keepLocalTime = true means only change the timezone, without
// affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]-->
// 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()<this.clone().startOf(b).valueOf())}function Yb(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():this.clone().endOf(b).valueOf()<c.valueOf())}function Zb(a,b,c,d){return d=d||"()",("("===d[0]?this.isAfter(a,c):!this.isBefore(a,c))&&(")"===d[1]?this.isBefore(b,c):!this.isAfter(b,c))}function $b(a,b){var c,d=s(a)?a:sb(a);return!(!this.isValid()||!d.isValid())&&(b=K(b||"millisecond"),"millisecond"===b?this.valueOf()===d.valueOf():(c=d.valueOf(),this.clone().startOf(b).valueOf()<=c&&c<=this.clone().endOf(b).valueOf()))}function _b(a,b){return this.isSame(a,b)||this.isAfter(a,b)}function ac(a,b){return this.isSame(a,b)||this.isBefore(a,b)}function bc(a,b,c){var d,e,f,g;// 1000
// 1000 * 60
// 1000 * 60 * 60
// 1000 * 60 * 60 * 24, negate dst
// 1000 * 60 * 60 * 24 * 7, negate dst
return this.isValid()?(d=Bb(a,this),d.isValid()?(e=6e4*(d.utcOffset()-this.utcOffset()),b=K(b),"year"===b||"month"===b||"quarter"===b?(g=cc(this,d),"quarter"===b?g/=3:"year"===b&&(g/=12)):(f=this-d,g="second"===b?f/1e3:"minute"===b?f/6e4:"hour"===b?f/36e5:"day"===b?(f-e)/864e5:"week"===b?(f-e)/6048e5:f),c?g:t(g)):NaN):NaN}function cc(a,b){
// difference in months
var c,d,e=12*(b.year()-a.year())+(b.month()-a.month()),
// b is in (anchor - 1 month, anchor + 1 month)
f=a.clone().add(e,"months");
//check for negative zero, return zero if negative zero
// linear across the month
// linear across the month
return b-f<0?(c=a.clone().add(e-1,"months"),d=(b-f)/(f-c)):(c=a.clone().add(e+1,"months"),d=(b-f)/(c-f)),-(e+d)||0}function dc(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function ec(){var a=this.clone().utc();return 0<a.year()&&a.year()<=9999?z(Date.prototype.toISOString)?this.toDate().toISOString():X(a,"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]"):X(a,"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]")}/**
* Return a human readable representation of a moment that can
* also be evaluated to get a new moment which is the same
*
* @link https://nodejs.org/dist/latest/docs/api/util.html#util_custom_inspect_function_on_objects
*/
function fc(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var a="moment",b="";this.isLocal()||(a=0===this.utcOffset()?"moment.utc":"moment.parseZone",b="Z");var c="["+a+'("]',d=0<this.year()&&this.year()<=9999?"YYYY":"YYYYYY",e="-MM-DD[T]HH:mm:ss.SSS",f=b+'[")]';return this.format(c+d+e+f)}function gc(b){b||(b=this.isUtc()?a.defaultFormatUtc:a.defaultFormat);var c=X(this,b);return this.localeData().postformat(c)}function hc(a,b){return this.isValid()&&(s(a)&&a.isValid()||sb(a).isValid())?Ob({to:this,from:a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate()}function ic(a){return this.from(sb(),a)}function jc(a,b){return this.isValid()&&(s(a)&&a.isValid()||sb(a).isValid())?Ob({from:this,to:a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate()}function kc(a){return this.to(sb(),a)}
// If passed a locale key, it will set the locale for this
// instance. Otherwise, it will return the locale configuration
// variables for this instance.
function lc(a){var b;return void 0===a?this._locale._abbr:(b=bb(a),null!=b&&(this._locale=b),this)}function mc(){return this._locale}function nc(a){
// the following switch intentionally omits break keywords
// to utilize falling through the cases.
switch(a=K(a)){case"year":this.month(0);/* falls through */
case"quarter":case"month":this.date(1);/* falls through */
case"week":case"isoWeek":case"day":case"date":this.hours(0);/* falls through */
case"hour":this.minutes(0);/* falls through */
case"minute":this.seconds(0);/* falls through */
case"second":this.milliseconds(0)}
// weeks are a special case
// quarters are also special
return"week"===a&&this.weekday(0),"isoWeek"===a&&this.isoWeekday(1),"quarter"===a&&this.month(3*Math.floor(this.month()/3)),this}function oc(a){
// 'date' is an alias for 'day', so it should be considered as such.
return a=K(a),void 0===a||"millisecond"===a?this:("date"===a&&(a="day"),this.startOf(a).add(1,"isoWeek"===a?"week":a).subtract(1,"ms"))}function pc(){return this._d.valueOf()-6e4*(this._offset||0)}function qc(){return Math.floor(this.valueOf()/1e3)}function rc(){return new Date(this.valueOf())}function sc(){var a=this;return[a.year(),a.month(),a.date(),a.hour(),a.minute(),a.second(),a.millisecond()]}function tc(){var a=this;return{years:a.year(),months:a.month(),date:a.date(),hours:a.hours(),minutes:a.minutes(),seconds:a.seconds(),milliseconds:a.milliseconds()}}function uc(){
// new Date(NaN).toJSON() === null
return this.isValid()?this.toISOString():null}function vc(){return n(this)}function wc(){return j({},m(this))}function xc(){return m(this).overflow}function yc(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}}function zc(a,b){U(0,[a,a.length],0,b)}
// MOMENTS
function Ac(a){return Ec.call(this,a,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)}function Bc(a){return Ec.call(this,a,this.isoWeek(),this.isoWeekday(),1,4)}function Cc(){return xa(this.year(),1,4)}function Dc(){var a=this.localeData()._week;return xa(this.year(),a.dow,a.doy)}function Ec(a,b,c,d,e){var f;return null==a?wa(this,d,e).year:(f=xa(a,d,e),b>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=e<pf.s&&["s",e]||f<=1&&["m"]||f<pf.m&&["mm",f]||g<=1&&["h"]||g<pf.h&&["hh",g]||h<=1&&["d"]||h<pf.d&&["dd",h]||i<=1&&["M"]||i<pf.M&&["MM",i]||j<=1&&["y"]||["yy",j];return k[2]=b,k[3]=+a>0,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;d<c;d++)if(d in b&&a.call(this,b[d],d,b))return!0;return!1};var qd=pd,rd=a.momentProperties=[],sd=!1,td={};a.suppressDeprecationWarnings=!1,a.deprecationHandler=null;var ud;ud=Object.keys?Object.keys:function(a){var b,c=[];for(b in a)i(a,b)&&c.push(b);return c};var vd,wd=ud,xd={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},yd={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},zd="Invalid date",Ad="%d",Bd=/\d{1,2}/,Cd={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},Dd={},Ed={},Fd=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,Gd=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,Hd={},Id={},Jd=/\d/,Kd=/\d\d/,Ld=/\d{3}/,Md=/\d{4}/,Nd=/[+-]?\d{6}/,Od=/\d\d?/,Pd=/\d\d\d\d?/,Qd=/\d\d\d\d\d\d?/,Rd=/\d{1,3}/,Sd=/\d{1,4}/,Td=/[+-]?\d{1,6}/,Ud=/\d+/,Vd=/[+-]?\d+/,Wd=/Z|[+-]\d\d:?\d\d/gi,Xd=/Z|[+-]\d\d(?::?\d\d)?/gi,Yd=/[+-]?\d+(\.\d{1,3})?/,Zd=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,$d={},_d={},ae=0,be=1,ce=2,de=3,ee=4,fe=5,ge=6,he=7,ie=8;vd=Array.prototype.indexOf?Array.prototype.indexOf:function(a){
// I know
var b;for(b=0;b<this.length;++b)if(this[b]===a)return b;return-1};var je=vd;
// FORMATTING
U("M",["MM",2],"Mo",function(){return this.month()+1}),U("MMM",0,0,function(a){return this.localeData().monthsShort(this,a)}),U("MMMM",0,0,function(a){return this.localeData().months(this,a)}),
// ALIASES
J("month","M"),
// PRIORITY
M("month",8),
// PARSING
Z("M",Od),Z("MM",Od,Kd),Z("MMM",function(a,b){return b.monthsShortRegex(a)}),Z("MMMM",function(a,b){return b.monthsRegex(a)}),ba(["M","MM"],function(a,b){b[be]=u(a)-1}),ba(["MMM","MMMM"],function(a,b,c,d){var e=c._locale.monthsParse(a,d,c._strict);
// if we didn't find a month name, mark the date as invalid.
null!=e?b[be]=e:m(c).invalidMonth=a});
// LOCALES
var ke=/D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/,le="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),me="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),ne=Zd,oe=Zd;
// FORMATTING
U("Y",0,0,function(){var a=this.year();return a<=9999?""+a:"+"+a}),U(0,["YY",2],0,function(){return this.year()%100}),U(0,["YYYY",4],0,"year"),U(0,["YYYYY",5],0,"year"),U(0,["YYYYYY",6,!0],0,"year"),
// ALIASES
J("year","y"),
// PRIORITIES
M("year",1),
// PARSING
Z("Y",Vd),Z("YY",Od,Kd),Z("YYYY",Sd,Md),Z("YYYYY",Td,Nd),Z("YYYYYY",Td,Nd),ba(["YYYYY","YYYYYY"],ae),ba("YYYY",function(b,c){c[ae]=2===b.length?a.parseTwoDigitYear(b):u(b)}),ba("YY",function(b,c){c[ae]=a.parseTwoDigitYear(b)}),ba("Y",function(a,b){b[ae]=parseInt(a,10)}),
// HOOKS
a.parseTwoDigitYear=function(a){return u(a)+(u(a)>68?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()?a<this?this:a:o()}),Ke=x("moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var a=sb.apply(null,arguments);return this.isValid()&&a.isValid()?a>this?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});

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
pocwww/static/pyramid.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

261
pocwww/static/theme.css Normal file
View File

@@ -0,0 +1,261 @@
/*@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
body {
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: 300;
color: #ffffff;
background: #bc2131;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: 300;
}*/
/*
* Base structure
*/
/* Move down content because we have a fixed navbar that is 50px tall */
body {
padding-top: 50px;
}
.allcaps{
text-transform: uppercase;
/*font-size: 14px;*/
/*color: #666;*/
font-weight: 400;
letter-spacing: 1px;
z-index: 100;
}
/*
* Global add-ons
*/
.sub-header {
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
/*
* Top navigation
* Hide default border to remove 1px line.
*/
.navbar-fixed-top {
border: 0;
}
/*
* Sidebar
*/
/* Hide for mobile, show later */
.sidebar {
display: none;
}
@media (min-width: 768px) {
.sidebar {
position: fixed;
top: 51px;
bottom: 0;
left: 0;
z-index: 1000;
display: block;
padding: 20px;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
background-color: #f5f5f5;
border-right: 1px solid #eee;
}
}
/* Sidebar navigation */
.nav-sidebar {
margin-right: -21px; /* 20px padding + 1px border */
margin-bottom: 20px;
margin-left: -20px;
}
.nav-sidebar > 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 {
}

View File

@@ -0,0 +1,59 @@
{% from 'pagination.jinja2' import render_pagination %}
{% extends "layout.jinja2" %}
{% block content %}
<div class="content">
<div class="row">
<div class="col-xs-12">
<h2>Well Configuration</h2>
<form>
<div class="form-group">
<label for="pocIPAddress">POC IP Address</label>
<input type="text" class="form-control" id="pocIPAddress" value="{{pocIPAddress}}">
</div>
<hr />
<div class="alert alert-success alert-dismissable hidden" id="update-success">
<span>
<p>Address successfully updated!</p>
</span>
</div>
<div class="alert alert-danger alert-dismissable hidden" id="update-failed">
<span>
<p>Failed to update address.</p>
</span>
</div>
<button id="submit" class="btn btn-default">Update Address</button>
</form>
</div>
</div>
</div>
<script>
function configUpdatedSuccessfully(data){
console.log(data);
if(data.status == "OK"){
$('#update-success').removeClass('hidden');
} else {
$('#update-failed').removeClass('hidden');
}
}
$("#submit").click(function(event){
event.preventDefault();
$('#update-success').addClass('hidden');
$('#update-failed').addClass('hidden');
var newObject = {};
newObject.pocIPAddress = $("#pocIPAddress").val();
$.ajax({
type: "POST",
dataType: 'json',
data: JSON.stringify(newObject),
contentType: "application/json; charset=utf-8",
url: "/json/updatepocaddress",
success: configUpdatedSuccessfully
});
})
</script>
{% endblock content %}

View File

@@ -0,0 +1,40 @@
{% from 'pagination.jinja2' import render_pagination %}
{% extends "layout.jinja2" %}
{% block content %}
<div class="content">
<h1>Cards for {{cards_date | datestring('long')}}</h1>
<div style="text-center">
{{render_pagination(pagination, "/cards/" + cards_date)}}
</div>
<table class="table table-striped"
<thead>
<tr>
<th>Stroke</th>
<th>Datetime</th>
</tr>
</thead>
<tbody>
{% for card in cards %}
<tr>
<td>{{card.strokeNumber}}</td>
<td><a href="/card/view/{{card.strokeNumber}}">{{card.timestamp | datetime('long')}}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock content %}
{% block card_dates %}
<h3>Card Dates</h3>
<ul class="nav nav-sidebar">
{% for d in datelist %}
<li><a href="/cards/{{d.date}}">{{d.date}} <span class="badge">{{d.count}}</span></a></li>
{% endfor %}
</ul>
{% endblock card_dates %}

View File

@@ -0,0 +1,24 @@
{% extends "layout.jinja2" %}
{% block content %}
<div class="content">
<a href="/cards/{{date}}/{{datepage}}"><span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span> BACK</a>
<h1>{{card.timestamp | datetime('long')}}</h1>
<canvas id="surfacecard" height="120"></canvas>
<canvas id="downholecard" height="120"></canvas>
</div>
<script>
var urlparts = decodeURIComponent(window.location).split("/").slice(3)
var cardID = parseInt(urlparts[2]);
$.ajax({
dataType: 'json',
url:"/json/card/view/" + cardID,
success: drawCards
});
</script>
{% endblock content %}

View File

@@ -0,0 +1,174 @@
{% from 'pagination.jinja2' import render_pagination %}
{% extends "layout.jinja2" %}
{% block content %}
<div class="content">
<div class="container">
<div class="row">
<div class="col-xs-12">
<h2>Well Configuration</h2>
<form>
<div class="form-group">
<label for="wellName">Well Name</label>
<input type="text" class="form-control" id="wellName" value="{{config.wellName}}">
</div>
<div class="form-group">
<label for="deltaT">Delta T</label>
<input type="number" class="form-control" id="deltaT" value="{{config.deltaT}}" step="0.001" min="0.001" max="0.2">
</div>
<div class="form-group">
<label for="pumpDiameter">Pump Diameter</label>
<input type="number" class="form-control" id="pumpDiameter" value="{{config.pumpDiameter}}" step="0.001" min="0.001" max="10">
</div>
<div class="form-group">
<label for="fluidGradient">Fluid Gradient</label>
<input type="number" class="form-control" id="fluidGradient" value="{{config.fluidGradient}}" step="0.001" min="0.001" max="2.0">
</div>
<div class="form-group">
<label for="tubingID">Tubing ID</label>
<input type="number" class="form-control" id="tubingID" value="{{config.tubingID}}" step="0.001" min="0.001" max="24">
</div>
<div class="form-group">
<label for="tubingOD">Tubing OD</label>
<input type="number" class="form-control" id="tubingOD" value="{{config.tubingOD}}" step="0.001" min="0.001" max="24">
</div>
<div class="form-group">
<label for="tubingAnchorDepth">Tubing Anchor Depth</label>
<input type="number" class="form-control" id="tubingAnchorDepth" value="{{config.tubingAnchorDepth}}" step="0.001" min="0.001">
</div>
<div class="form-group">
<label for="structuralRating">Structural Rating</label>
<input type="number" class="form-control" id="structuralRating" value="{{config.structuralRating}}" step="0.001" min="0.001">
</div>
<div class="form-group">
<label for="stuffingBoxFriction">Stuffing Box Friction</label>
<input type="number" class="form-control" id="stuffingBoxFriction" value="{{config.stuffingBoxFriction}}" step="0.5" min="0.0">
</div>
<div class="form-group">
<label for="tubingHeadPressure">Tubing Head Pressure</label>
<input type="number" class="form-control" id="tubingHeadPressure" value="{{config.tubingHeadPressure}}" step="0.001" min="0.0">
</div>
<div class="form-group">
<label for="storedBy">Last Stored By</label>
<input type="text" class="form-control disabled" id="storedBy" value="{{config.storedBy}}" disabled>
</div>
<h2>Taper Setup</h2>
<div class="table table-responsive">
<table class="table">
<thead>
<tr>
<th>Taper</th>
<th>Length</th>
<th>Diameter</th>
<th>Material</th>
<th>Damping Factor</th>
</tr>
</thead>
<tbody>
{% for taper in config.tapers %}
<tr>
<td class="taper-number">{{loop.index}}</td>
<td><input type="number" class="form-control" id="taper-{{loop.index}}-length" value="{{taper.length}}" /></td>
<td><input type="number" class="form-control" id="taper-{{loop.index}}-diameter" value="{{taper.diameter}}" /></td>
<td><select class="form-control" id="taper-{{loop.index}}-material" value="{{taper.material}}"><option value="steel">Steel</option><option value="fiberglass">Fiberglass</option></select></td>
<!-- <td><input type="text" class="form-control" id="taper-{{loop.index}}-material" value="{{taper.material}}" /></td> -->
<td><input type="number" class="form-control" id="taper-{{loop.index}}-dampingFactor" value="{{taper.dampingFactor}}" /></td>
</tr>
{% endfor %}
<span id="morerows"></span>
</tbody>
</table>
<button id="add-row" class="btn btn-primary"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Taper</button>
<hr />
<div class="alert alert-success alert-dismissable hidden" id="update-success">
<span>
<p>Configuration successfully updated!</p>
</span>
</div>
<div class="alert alert-danger alert-dismissable hidden" id="update-failed">
<span>
<p>Failed to update configuration.</p>
</span>
</div>
<button id="submit" class="btn btn-default">Update Well Configuration</button>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
$( "#add-row" ).click(function( event ) {
event.preventDefault();
var nextTaper = parseInt($(".taper-number").last().text()) + 1;
if(nextTaper <= 10){
var taperHTML = '<tr><td class="taper-number">' + nextTaper + '</td><td><input type="number" class="form-control" id="taper-'+ nextTaper + '-length" value="0.0" /></td><td><input type="number" class="form-control" id="taper-' + nextTaper + '-diameter" value="0.0" /></td><td><select class="form-control" id="taper-' + nextTaper + '-material" value=""><option value="steel">Steel</option><option value="fiberglass">Fiberglass</option></select></td><td><input type="number" class="form-control" id="taper-' + nextTaper + '-dampingFactor" value="0.0" /></td></tr>';
$("tbody").append(taperHTML);
}
});
function configUpdatedSuccessfully(data){
console.log(data);
if(data.stored_result){
$('#update-success').removeClass('hidden');
} else {
$('#update-failed').removeClass('hidden');
}
}
$("#submit").click(function(event){
event.preventDefault();
$('#update-success').addClass('hidden');
$('#update-failed').addClass('hidden');
var newObject = {};
newObject.wellName = $("#wellName").val();
newObject.deltaT = parseFloat($("#deltaT").val());
newObject.pumpDiameter = parseFloat($("#pumpDiameter").val());
newObject.fluidGradient = parseFloat($("#fluidGradient").val());
newObject.tubingID = parseFloat($("#tubingID").val());
newObject.tubingOD = parseFloat($("#tubingOD").val());
newObject.tubingAnchorDepth = parseFloat($("#tubingAnchorDepth").val());
newObject.structuralRating = parseFloat($("#structuralRating").val());
newObject.stuffingBoxFriction = parseFloat($("#stuffingBoxFriction").val());
newObject.tubingHeadPressure = parseFloat($("#tubingHeadPressure").val());
var taperList = [];
var lastTaper = parseInt($(".taper-number").last().text())
for (var i = 1; i <= lastTaper; i++){
taperList.push({
length: parseFloat($("#taper-" + i + "-length").val()),
diameter: parseFloat($("#taper-" + i + "-diameter").val()),
material: $("#taper-" + i + "-material").val(),
dampingFactor: parseFloat($("#taper-" + i + "-dampingFactor").val()),
})
}
newObject.tapers = taperList;
$.ajax({
type: "POST",
dataType: 'json',
data: JSON.stringify(newObject),
contentType: "application/json; charset=utf-8",
url: "/json/updateconfig",
success: configUpdatedSuccessfully
});
})
</script>
{% endblock content %}

View File

@@ -0,0 +1,92 @@
{% extends "layout.jinja2" %}
{% block content %}
<div class="content">
<div class="row visible-xs-inline">
<div class="col-xs-12 well" style="text-align:center">
<h3 class="allcaps"><a href="/runstatus">Run Status</a></h3>
<h4><span id="db-runstatus"></span></h4>
<p>at</p>
<h5><span id="db-runstatustimestamp"></span></h5>
<button onclick="start()" class="btn btn-success">Start</button>
<button onclick="stop()" class="btn btn-danger">Stop</button>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h2 class="allcaps">Latest Tag Values</h2>
{% if tag_values|length > 0 %}
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th class="allcaps">Name</th>
<th class="allcaps"></th>
<th class="allcaps">Value</th>
<th class="allcaps">Timestamp</th>
<!-- <th>Max</th>
<th>Min</th>
<th>Average</th>
<th>Total</th> -->
</tr>
</thead>
<tbody>
{% for t in tag_values %}
<tr>
<td><a href="/values/tag/{{t._id}}">{{t._id}}</a></td>
<td>
<button type="button" class="btn btn-default" title="{{t._id}}" data-container="body" data-toggle="popover" data-trigger="focus" data-placement="right" data-content="Max: {{t.max | round(3)}}<br \>
Min: {{t.min | round(3)}}<br \>
Average: {{t.average | round(3)}}<br \>
Total: {{t.total | round(3)}}">
Details
</button>
</td>
<td>{{t.value | round(3)}}</td>
<td>{{t.timestamp | datetime('short')}}</td>
<!-- <td>{{t.max | round(3)}}</td>
<td>{{t.min | round(3)}}</td>
<td>{{t.average | round(3)}}</td>
<td>{{t.total | round(3)}}</td> -->
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<h3>No tag values yet...</h3>
{% endif %}
</div>
<div class="col-md-6">
<h2 class="allcaps">Latest Card</h2>
{% if card is defined %}
<h3>{{card.timestamp | datetime('long')}}</h1>
<canvas id="surfacecard" height="140"></canvas>
<canvas id="downholecard" height="140"></canvas>
{% else %}
<h3>No card data yet...</h3>
{% endif %}
</div>
</div>
</div>
<script>
function getCardData(){
$.ajax({
dataType: 'json',
url:"/json",
success: drawCards
});
};
getCardData();
$(function () {
$('[data-toggle="popover"]').popover({html: true})
})
</script>
{% endblock content %}

View File

@@ -0,0 +1,23 @@
{% extends "layout.jinja2" %}
{% block content %}
<div class="content">
<h1>Card Dates </h1>
<table class="table table-striped">
<thead>
<tr>
<th>Date</th>
<th>Cards</th>
</tr>
</thead>
<tbody>
{% for day in datelist %}
<tr>
<td><a href="/cards/{{day.date}}">{{day.date | date('long')}}</a></td>
<td>{{day.count}} cards</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock content %}

View File

@@ -0,0 +1,37 @@
{% extends "layout.jinja2" %}
{% block content %}
<div class="content">
<div class="row">
<div class="col-xs-12">
<h2>Fluid Shots</h2>
{% if data|length > 0 %}
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Timestamp</th>
<th>Fluid Level</th>
<th>Pump Intake Pressure</th>
<th>Friction Estimate</th>
</tr>
</thead>
<tbody>
{% for d in data %}
<tr>
<td>{{d.timestamp | datetime('medium')}}</td>
<td>{{d.fluidLevel | round(3)}}</td>
<td>{{d.pumpIntakePressure | round(3)}}</td>
<td>{{d.frictionEstimate | round(3)}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<h3>No fluid shots yet...</h3>
{% endif %}
</div>
</div>
</div>
{% endblock content %}

View File

@@ -0,0 +1,43 @@
{% extends "layout.jinja2" %}
{% block content %}
<div class="content">
<div class="row">
<div class="col-xs-12">
<h2>Gauge Off Data</h2>
{% if data is defined %}
{% for d in data %}
<h3 class="allcaps">{{d.date|date('long')}}</h3>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Last</th>
<th>Max</th>
<th>Min</th>
<th>Average</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{% for t in d.tags %}
<tr>
<td>{{t}}</td>
<td>{{d['tags'][t].last | round(3)}}</td>
<td>{{d['tags'][t].max | round(3)}}</td>
<td>{{d['tags'][t].min | round(3)}}</td>
<td>{{d['tags'][t].average | round(3)}}</td>
<td>{{d['tags'][t].total | round(3)}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}
{% endif %}
</div>
</div>
</div>
{% endblock content %}

View File

@@ -0,0 +1,194 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content="">
<meta name="author" content="">
<link rel="icon" href="../../favicon.ico">
<title>Henry POC</title>
<!-- BOOTSTRAP -->
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<!-- END BOOTSTRAP -->
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<link href="../../assets/css/ie10-viewport-bug-workaround.css" rel="stylesheet">
<!-- Custom styles for this scaffold -->
<link href="{{request.static_url('pocwww:static/theme.css')}}" rel="stylesheet">
<!-- Just for debugging purposes. Don't actually copy these 2 lines! -->
<!--[if lt IE 9]><script src="../../assets/js/ie8-responsive-file-warning.js"></script><![endif]-->
<script src="../../assets/js/ie-emulation-modes-warning.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.0/jquery-ui.min.js"></script>
<script src="https://d3js.org/d3.v3.js"></script>
<script src="{{request.static_url('pocwww:static/moment.min.js')}}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"></script>
<script type="text/javascript" src="{{request.static_url('pocwww:static/graphs.js')}}"></script>
<!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/"><span id="shake-status"></span> | Henry POC</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li {% if (navgroup == 'dashboard') %}class="visible-xs-inline active" {% else %} class="visible-xs-inline" {% endif %}><a href="/">Dashboard</a></li>
<li {% if (navgroup == 'cards') %}class="visible-xs-inline active" {% else %} class="visible-xs-inline" {% endif %}><a href="/cards">Cards</a></li>
<li {% if (navgroup == 'values') %}class="visible-xs-inline active" {% else %} class="visible-xs-inline" {% endif %}><a href="/values">Values</a></li>
<li {% if (navgroup == 'gaugeoff') %}class="visible-xs-inline active" {% else %} class="visible-xs-inline" {% endif %}><a href="/gaugeoff">Gauge-Off</a></li>
<li {% if (navgroup == 'fluidshots') %}class="visible-xs-inline active" {% else %} class="visible-xs-inline" {% endif %}><a href="/fluidshots">Fluid Shots</a></li>
<li {% if (navgroup == 'welltests') %}class="visible-xs-inline active" {% else %} class="visible-xs-inline" {% endif %}><a href="/welltests">Well Tests</a></li>
{% if request.authenticated_userid %}
<li {% if (navgroup == 'setpoints') %}class="active"{% endif %}><a href="/setpoints">Setpoints</a></li>
<li {% if (navgroup == 'config') %}class="active"{% endif %}><a href="/config">Well Setup</a></li>
<li><a href="/register">{{request.authenticated_userid}}</a></li>
<li><form class="navbar-form"><div class="form-group"><a href="{{request.route_url('auth',action='out')}}" class="btn btn-warning">Sign Out</a></div></form></li>
{% else %}
<form action="{{request.route_url('auth',action='in')}}" method="post" class="form-inline navbar-form">
<div class="form-group">
<input type="text" name="username" class="form-control" placeholder="Username">
</div>
<div class="form-group">
<input type="password" name="password" class="form-control" placeholder="Password">
</div>
<div class="form-group">
<input type="submit" value="Sign in" class="btn btn-default">
</div>
</form>
{% endif %}</li>
</ul>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<div style="text-align:center">
<h3 class="allcaps"><a href="/runstatus">Run Status</a></h3>
<h4><span id="runstatus"></span></h4>
<p>at</p>
<h5><span id="runstatustimestamp"></span></h5>
{% if request.authenticated_userid %}
<button onclick="start()" class="btn btn-success">Start</button>
<button onclick="stop()" class="btn btn-danger">Stop</button>
{% endif %}
</div>
<hr />
<ul class="nav nav-sidebar">
<li {% if (navgroup == 'dashboard') %}class="active"{% endif %}><a href="/">Dashboard</a></li>
<li {% if (navgroup == 'cards') %}class="active"{% endif %}><a href="/cards">Cards</a></li>
<li {% if (navgroup == 'values') %}class="active"{% endif %}><a href="/values">Values</a></li>
<li {% if (navgroup == 'gaugeoff') %}class="active"{% endif %}><a href="/gaugeoff">Gauge-Off</a></li>
</ul>
<ul class="nav nav-sidebar">
<li {% if (navgroup == 'fluidshots') %}class="active"{% endif %}><a href="/fluidshots">Fluid Shots</a></li>
<li {% if (navgroup == 'welltests') %}class="active"{% endif %}><a href="/welltests">Well Tests</a></li>
</ul>
{% block card_dates %}
{% endblock card_dates %}
{% block cards %}
{% endblock cards %}
</div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
{% block content %}
<p>No content</p>
{% endblock content %}
</div>
</div>
</div>
</body>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script>
function setRunStatus(data){
$("#runstatus").text(data.runstatus.status);
$("#db-runstatus").text(data.runstatus.status);
var ts_date = new Date(Date.parse(data.runstatus.timestamp.replace(" ", "T")));
$("#runstatustimestamp").text(ts_date.toString());
$("#db-runstatustimestamp").text(ts_date.toString());
}
function getRunStatus(){
$.ajax({
dataType: 'json',
url: "/json/runstatusnow",
success: setRunStatus
});
};
getRunStatus();
function start(){
$.ajax({
dataType: 'json',
url: "/json/cmd/start",
success: getRunStatus
});
}
function stop(){
$.ajax({
dataType: 'json',
url: "/json/cmd/stop",
success: getRunStatus
});
}
function shakeOK(){
$('#shake-status').html('<span class="glyphicon glyphicon-ok" aria-hidden="true"></span>')
}
function shakeFail(){
$('#shake-status').html('<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>')
}
function shake(){
$.ajax({
dataType: 'json',
url: "/json/cmd/shake",
success: shakeOK,
error: shakeFail
});
}
shake();
setInterval(function() {
getRunStatus();
shake();
}, 10000);
</script>
</html>

View File

@@ -0,0 +1,41 @@
{% macro render_pagination(pagination, endpoint) %}
<ul class="pagination">
{% if pagination.has_prev %}
<li>
<a href="{{endpoint}}/{{pagination.prev_num}}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% 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 %}
<li>
<a href="{{endpoint}}/{{p}}">{{ p }}</a>
</li>
{% else %}
<li class="active">
<a href="{{endpoint}}/{{p}}">{{ p }}</a>
</li>
{% endif %}
{% else %}
<li class="disabled">
<span class="ellipsis">&hellip;</span>
</li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li>
<a href="{{endpoint}}/{{pagination.next_num}}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% endif %}
</ul>
{% endmacro %}

View File

@@ -0,0 +1,187 @@
{% extends "layout.jinja2" %}
{% block content %}
{% if request.authenticated_userid %}
<h1>User Management</h1>
<hr />
<h2>New User</h2>
<form class="form-inline">
<div class="form-group">
<input type="text" name="username" class="form-control" id="username" placeholder="Username">
</div>
<div class="form-group">
<input type="password" name="password" class="form-control" id="password" placeholder="Password">
</div>
<div class="form-group">
<button id="submitNewUser" class="btn btn-primary">Create User</button>
</div>
</form>
<br />
<div class="alert alert-success alert-dismissable hidden" id="add-user-success">
<span>
<p>User successfully added!</p>
</span>
</div>
<div class="alert alert-danger alert-dismissable hidden" id="add-user-failed">
<span>
<p><span id="error-message"></span></p>
</span>
</div>
<hr />
<h2>Change Password</h2>
<form class="form-inline">
<div class="form-group">
<label for="newPassword">New Password</label>
<input type="password" name="newPassword" class="form-control" id="newPassword" placeholder="Password">
</div>
<div class="form-group">
<button id="submitNewPassword" class="btn btn-primary">Update Password</button>
</div>
</form>
<br />
<div class="alert alert-success alert-dismissable hidden" id="update-password-success">
<span>
<p>Password successfully updated!</p>
</span>
</div>
<div class="alert alert-danger alert-dismissable hidden" id="update-password-failed">
<span>
<p><span id="error-message"></span></p>
</span>
</div>
<hr />
<h2>All Users</h2>
<div class="table-reponsive">
<table class="table">
<thead>
<tr>
<th>Username</th>
<th></th>
</thead>
<tbody>
</tbody>
</table>
</div>
<script>
function addedUser(data){
if (data.status == "OK"){
$('#add-user-success').removeClass('hidden');
} else {
$('#add-user-failed').removeClass('hidden');
$('#error-message').text(data.info);
}
getAllUsers();
};
function updatedUserPassword(data){
if (data.status == "OK"){
$('#update-password-success').removeClass('hidden');
} else {
$('#update-password-failed').removeClass('hidden');
$('#error-message').text(data.info);
}
};
function sendNewUser(){
var newUser = {
username: $("#username").val(),
password: $("#password").val()
};
$.ajax({
type: "POST",
dataType: 'json',
data: JSON.stringify(newUser),
contentType: "application/json; charset=utf-8",
url: "/json/users",
success: addedUser
});
};
function sendUpdateUser(){
var newUser = {
username: '{{request.authenticated_userid}}',
password: $("#newPassword").val()
};
$.ajax({
type: "PUT",
dataType: 'json',
data: JSON.stringify(newUser),
contentType: "application/json; charset=utf-8",
url: "/json/users",
success: updatedUserPassword
});
};
$("#submitNewUser").click(function(event){
event.preventDefault();
$('#add-user-success').addClass('hidden');
$('#add-user-failed').addClass('hidden');
sendNewUser();
});
$("#submitNewPassword").click(function(event){
event.preventDefault();
$('#update-password-success').addClass('hidden');
$('#update-password-failed').addClass('hidden');
sendUpdateUser();
});
function showAllUsers(data){
$('tbody').empty();
for(var i = 0; i < data.users.length; i++){
if (data.users[i] == "admin"){
$('tbody').append("<tr><td>" + data.users[i] + '</td><td><button type="button" class="btn btn-disabled" title="What are you doing?" data-container="body" data-toggle="popover" data-trigger="focus" data-placement="right" data-content="You cannot delete the admin user">Delete</button></td></tr>');
} else if (data.users[i] == '{{request.authenticated_userid}}'){
$('tbody').append("<tr><td>" + data.users[i] + '</td><td><button type="button" class="btn btn-disabled" title="What are you doing?" data-container="body" data-toggle="popover" data-trigger="focus" data-placement="right" data-content="You cannot delete yourself. It doesn\'t work like that... ">Delete</button></td></tr>');
} else {
$('tbody').append("<tr><td>" + data.users[i] + '</td><td><button class="btn btn-danger" onclick="deleteUser(\'' + data.users[i] + '\')">Delete</button></td></tr>');
}
$(function () {
$('[data-toggle="popover"]').popover({html: true})
})
}
}
'<button type="button" class="btn btn-default" title="What are you doing?" data-container="body" data-toggle="popover" data-trigger="focus" data-placement="right" data-content="You cannot delete the admin user">Delete</button>'
function getAllUsers(){
$.ajax({
dataType: 'json',
url:"/json/users",
success: showAllUsers
});
}
getAllUsers();
function deleteUser(username){
$.ajax({
type:'DELETE',
dataType: 'json',
data: JSON.stringify({"username": username}),
contentType: "application/json; charset=utf-8",
url: "/json/users",
success: getAllUsers
})
}
$(function () {
$('[data-toggle="popover"]').popover({html: true})
})
</script>
{% endif %}
{% endblock content %}

View File

@@ -0,0 +1,39 @@
{% from 'pagination.jinja2' import render_pagination %}
{% extends "layout.jinja2" %}
{% block content %}
<div class="content">
<div class="row">
<div class="col-xs-12">
<h2>Run Status Log</h2>
{% if data|length > 0 %}
<div style="text-center">
{{render_pagination(pagination, "/runstatus")}}
</div>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Timestamp</th>
<th>Status</th>
<th>Initiator</th>
</tr>
</thead>
<tbody>
{% for d in data %}
<tr>
<td>{{d.timestamp | datetime('medium')}}</td>
<td>{{d.status}}</td>
<td>{{d.initiator}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<h3>No statuses stored yet...</h3>
{% endif %}
</div>
</div>
</div>
{% endblock content %}

View File

@@ -0,0 +1,127 @@
{% extends "layout.jinja2" %}
{% block content %}
<div class="content">
<div class="container">
<div class="row">
<div class="col-xs-12 well" style="text-align:center;">
<h1>Run Mode: <span id="runMode">Loading...</span></h1>
<button class="btn btn-lg btn-warning runModeBtn" data-mode="POC">POC</button>
<button class="btn btn-lg btn-warning runModeBtn" data-mode="MANUAL">Manual</button>
<button class="btn btn-lg btn-warning runModeBtn" data-mode="TIMER">Timer</button>
<br/><br/>
<p>Set by <span id="runModeSetBy">Loading...</span> at <span id="runModeSetTime">Loading...</span>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<h1>POC Setpoints</h1>
<div class="table table-responsive">
<table class="table">
<thead>
<tr>
<td>Name</td>
<td>Value</td>
<td></td>
<td>Stored By</td>
<td>Last Time Stored</td>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
function updateRunMode(data){
$('#runMode').text(runModeToString(data.updated.value));
$('#runModeSetBy').text(data.updated.storedBy);
$('#runModeSetTime').text(data.updated.lastStored);
}
function makeFormForSetpoint(i, s){
if(s.name == "runMode"){
$('#runMode').text(runModeToString(s.value));
$('#runModeSetBy').text(s.storedBy);
$('#runModeSetTime').text(s.lastStored);
return;
}
var row = '<tr>';
row += '<td>' + s.name +'</td>';
row += '<td><input type="number" class="form-control" id="' + s.name + '" value="' + s.value + '"></td>';
row += '<td><button class="btn btn-default setbtn" data-setpoint="'+ s.name + '">Update</button></td>';
row += '<td><input disabled type="text" class="form-control" id="' + s.name + '-storedBy" value="' + s.storedBy + '"></td>';
row += '<td><input disabled type="text" class="form-control" id="' + s.name + '-lastStored" value="' + s.lastStored + '"></td>';
row += '</tr>';
$('tbody').append(row);
}
function runModeToString(rm){
if (rm == 0){
return "POC";
} else if (rm == 1) {
return "MANUAL";
} else if (rm == 2) {
return "TIMER";
}
}
function runModeFromString(rm){
if (rm == "POC"){
return 0;
} else if (rm == "MANUAL") {
return 1;
} else if (rm == "TIMER") {
return 2;
}
}
function updateSingleRow(data){
var name = data.updated.name;
var storedBy = data.updated.storedBy;
var value = data.updated.value;
$("#"+ name).val(value);
}
function getSetpoints(){
$('tbody').empty();
$.get("/json/setpoints", {}, function(data){
$.each(data.setpoints, makeFormForSetpoint);
$('.setbtn').click(function(e){
e.preventDefault();
var setpoint = $(this).data('setpoint');
var value = $("#"+ setpoint).val();
$.post({
dataType: 'json',
data: JSON.stringify({name: setpoint, value: value}),
contentType: "application/json; charset=utf-8",
url: "/json/setpoints",
success: updateSingleRow
});
});
});
}
getSetpoints();
$('.runModeBtn').click(function(e){
e.preventDefault();
var modeStr = $(this).data('mode');
var mode = runModeFromString(modeStr);
$.post({
dataType: 'json',
data: JSON.stringify({mode: mode}),
contentType: "application/json; charset=utf-8",
url: "/json/mode",
success: updateRunMode
});
});
</script>
{% endblock content %}

View File

@@ -0,0 +1,57 @@
{% extends "layout.jinja2" %}
{% block content %}
<div class="content">
<div class="row">
<div class="col-xs-12">
<h2>Latest Tag Values: {{tagname}}</h2>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div id="time-range">
<p><span id="slider-start" class="slider-time pull-left well"></span><span id="slider-end" class="slider-time pull-right well"></span></p>
<div class="sliders_step1">
<div id="slider-range"></div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<canvas id="myChart" height="120"></canvas>
</div>
</div>
</div>
<script>
var urlparts = decodeURIComponent(window.location).split("/").slice(3)
var endD = new Date();
var end = endD.toISOString();
var dummyDate = new Date();
var startD = new Date(dummyDate.setDate(endD.getDate() - 2));
var start = startD.toISOString();
var tagName = "{{tagname}}";
var scatterChart;
if(urlparts.length >= 5){
start = urlparts[3];
end = urlparts[4];
}
$.ajax({
dataType: 'json',
url:"/json/values/tag/{{tagname}}/daterange",
success: drawSingleSlider
});
$.ajax({
dataType: 'json',
url: "/json/values/tag/" + tagName + "/between/" + start + "/" + end,
success: drawSingleGraph
});
</script>
{% endblock content %}

View File

@@ -0,0 +1,94 @@
{% extends "layout.jinja2" %}
{% block content %}
<div class="content">
<div class="row">
<div class="col-xs-12">
<h2>Latest Tag Values</h2>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
<th>Max</th>
<th>Min</th>
<th>Average</th>
<th>Total</th>
<th>Last Stored</th>
</tr>
</thead>
<tbody>
{% for t in current_tag_values %}
<tr>
<td><a href="/values/tag/{{t._id}}">{{t._id}}</a></td>
<td>{{t.value | round(3)}}</td>
<td>{{t.max | round(3)}}</td>
<td>{{t.min | round(3)}}</td>
<td>{{t.average | round(3)}}</td>
<td>{{t.total | round(3)}}</td>
<td>{{t.timestamp | datetime('medium')}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<!-- <pre>
{% for t in all_dates %}
{{t}}
{% endfor %}
</pre> -->
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div id="time-range">
<p><span id="slider-start" class="slider-time pull-left well"></span><span id="slider-end" class="slider-time pull-right well"></span></p>
<div class="sliders_step1">
<div id="slider-range"></div>
</div>
</div>
<div>
<canvas id="valueChart" height="150"></canvas>
</div>
</div>
</div>
</div>
<script>
var urlparts = decodeURIComponent(window.location).split("/").slice(3)
var endD = new Date();
var end = endD.toISOString();
var dummyDate = new Date();
var startD = new Date(dummyDate.setDate(endD.getDate() - 2));
var start = startD.toISOString();
var scatterChart;
if(urlparts.length >= 3){
start = urlparts[1];
end = urlparts[2];
}
$.ajax({
dataType: 'json',
url:"/json/values/daterange",
success: drawAllSlider
});
$.ajax({
dataType: 'json',
url:"/json/values/between/" + start + "/" + end,
success: drawChart
});
</script>
{% endblock content %}

View File

@@ -0,0 +1,49 @@
{% extends "layout.jinja2" %}
{% block content %}
<div class="content">
<div class="row">
<div class="col-xs-12">
<h2>Well Tests</h2>
{% if data|length > 0 %}
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Test Start</th>
<th>Test Hours</th>
<th>Total BBL</th>
<th>Oil BBL</th>
<th>Water BBL</th>
<th>Gas MCF</th>
<th>kFactor</th>
<th>Oil Ratio</th>
<th>Water Ratio</th>
<th>Gas Ratio</th>
</tr>
</thead>
<tbody>
{% for d in data %}
<tr>
<td>{{d.testStartTime | datetime('medium')}}</td>
<td>{{d.testHours}}</td>
<td>{{d.testTotalBBL | round(3)}}</td>
<td>{{d.testOilBBL | round(3)}}</td>
<td>{{d.testWaterBBL | round(3)}}</td>
<td>{{d.testGasMCF | round(3)}}</td>
<td>{{d.kFactor | round(3)}}</td>
<td>{{d.oilRatio | round(3)}}</td>
<td>{{d.waterRatio | round(3)}}</td>
<td>{{d.gasMCFRatio | round(3)}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<h3>No Well Tests yet...</h3>
{% endif %}
</div>
</div>
</div>
{% endblock content %}

29
pocwww/tests.py Normal file
View File

@@ -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)

108
pocwww/view_helpers.py Normal file
View File

@@ -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'])

172
pocwww/views.py Normal file
View File

@@ -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"}

66
production.ini Normal file
View File

@@ -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

3
pytest.ini Normal file
View File

@@ -0,0 +1,3 @@
[pytest]
testpaths = pocwww
python_files = *.py

55
setup.py Normal file
View File

@@ -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',
],
},
)

27
supervisor.conf Normal file
View File

@@ -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