WIP, to be cleaned and merged
This commit is contained in:
parent
314adbeb1d
commit
d1e3152a83
30 changed files with 1498 additions and 248 deletions
|
@ -0,0 +1 @@
|
|||
from .app import app, templates, db
|
|
@ -1,100 +0,0 @@
|
|||
from flask import Flask, jsonify
|
||||
import uuid
|
||||
import json
|
||||
from webargs import fields, validate
|
||||
from webargs.flaskparser import use_args
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy_utils import Timestamp, generic_repr
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker, relationship, backref
|
||||
from sqlalchemy.types import Float, String, Boolean
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///jobs.db"
|
||||
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
|
||||
@generic_repr
|
||||
class Job(db.Model, Timestamp):
|
||||
id = db.Column(db.String, default=lambda: str(uuid.uuid4()), primary_key=True)
|
||||
jump_range = db.Column(db.Float, nullable=False)
|
||||
mode = db.Column(db.String, default="bfs")
|
||||
systems = db.Column(db.String)
|
||||
permute = db.Column(db.String, default=None, nullable=True)
|
||||
primary = db.Column(db.Boolean, default=False)
|
||||
factor = db.Column(db.Float, default=0.5)
|
||||
done = db.Column(db.DateTime, nullable=True, default=None)
|
||||
started = db.Column(db.DateTime, nullable=True, default=None)
|
||||
progress = db.Column(db.Float, default=0.0)
|
||||
|
||||
# ============================================================
|
||||
|
||||
@classmethod
|
||||
def new(cls, **kwargs):
|
||||
obj = cls(**kwargs)
|
||||
db.session.add(obj)
|
||||
db.session.commit()
|
||||
print(obj)
|
||||
return obj
|
||||
|
||||
@property
|
||||
def dict(self):
|
||||
ret = {}
|
||||
for col in self.__table__.columns:
|
||||
ret[col.name] = getattr(self, col.name)
|
||||
ret["systems"] = json.loads(ret["systems"])
|
||||
return ret
|
||||
|
||||
@dict.setter
|
||||
def set_dict(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
db.create_all()
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@app.errorhandler(422)
|
||||
@app.errorhandler(400)
|
||||
def handle_error(err):
|
||||
headers = err.data.get("headers", None)
|
||||
messages = err.data.get("messages", ["Invalid request."])
|
||||
if headers:
|
||||
return jsonify({"errors": messages}), err.code, headers
|
||||
else:
|
||||
return jsonify({"errors": messages}), err.code
|
||||
|
||||
|
||||
@app.route("/route", methods=["GET", "POST"])
|
||||
@use_args(
|
||||
{
|
||||
"jump_range": fields.Float(required=True),
|
||||
"mode": fields.String(
|
||||
missing="bfs", validate=validate.OneOf(["bfs", "greedy", "a-star"])
|
||||
),
|
||||
"systems": fields.DelimitedList(fields.String, required=True),
|
||||
"permute": fields.String(
|
||||
missing=None,
|
||||
validate=validate.OneOf(["all", "keep_first", "keep_last", "keep_both"]),
|
||||
),
|
||||
"primary": fields.Boolean(missing=False),
|
||||
"factor": fields.Float(missing=0.5),
|
||||
}
|
||||
)
|
||||
def route(args):
|
||||
args["systems"] = json.dumps(args["systems"])
|
||||
for k, v in args.items():
|
||||
print(k, v)
|
||||
return jsonify({"id": Job.new(**args).id})
|
||||
|
||||
|
||||
@app.route("/status/<uuid:job_id>")
|
||||
def status(job_id):
|
||||
job = db.session.query(Job).get_or_404(str(job_id))
|
||||
return jsonify(job.dict)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=3777, debug=True)
|
665
ed_lrr_gui/web/app.py
Normal file
665
ed_lrr_gui/web/app.py
Normal file
|
@ -0,0 +1,665 @@
|
|||
from flask import (
|
||||
Flask,
|
||||
jsonify,
|
||||
session,
|
||||
render_template,
|
||||
redirect,
|
||||
url_for,
|
||||
send_from_directory,
|
||||
request,
|
||||
flash,
|
||||
current_app
|
||||
)
|
||||
from flask.json.tag import JSONTag
|
||||
import uuid
|
||||
import pickle
|
||||
import os
|
||||
import time
|
||||
import random
|
||||
import base64
|
||||
import gevent
|
||||
from functools import wraps
|
||||
from concurrent.futures.process import BrokenProcessPool
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from multiprocessing import Queue
|
||||
from webargs import fields, validate
|
||||
from webargs.flaskparser import use_kwargs
|
||||
|
||||
from flask_executor import Executor
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_bootstrap import Bootstrap
|
||||
|
||||
from flask_nav import Nav, register_renderer
|
||||
from flask_nav.elements import Navbar, View
|
||||
|
||||
from flask_admin import Admin
|
||||
from flask_admin.contrib.sqla import ModelView
|
||||
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
|
||||
from flask_login import (
|
||||
LoginManager,
|
||||
current_user,
|
||||
logout_user,
|
||||
UserMixin,
|
||||
AnonymousUserMixin,
|
||||
login_user,
|
||||
login_required,
|
||||
)
|
||||
|
||||
from flask_debugtoolbar import DebugToolbarExtension
|
||||
|
||||
from werkzeug.http import HTTP_STATUS_CODES
|
||||
from sqlalchemy_utils import generic_repr, JSONType, PasswordType, UUIDType
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker, relationship, backref
|
||||
from sqlalchemy.types import Float, String, DateTime
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
from .forms import RouteForm, LoginForm, RegisterForm, ChangePasswordForm
|
||||
from .utils import prepare_route, BootsrapRenderer, is_safe_url
|
||||
|
||||
import _ed_lrr as ed_lrr
|
||||
|
||||
templates = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
|
||||
app = Flask(__name__, template_folder=templates)
|
||||
app.config.from_pyfile("config.py")
|
||||
|
||||
executor = Executor(app)
|
||||
db = SQLAlchemy(app)
|
||||
bootstrap = Bootstrap(app)
|
||||
csrf = CSRFProtect(app)
|
||||
nav = Nav(app)
|
||||
login_manager = LoginManager(app)
|
||||
login_manager.login_view = "login"
|
||||
login_manager.session_protection = "strong"
|
||||
admin = Admin(app, name="ED_LRR", template_mode="bootstrap3")
|
||||
app.debug=True
|
||||
toolbar = DebugToolbarExtension(app)
|
||||
|
||||
|
||||
def wants_json_response():
|
||||
return request.accept_mimetypes['application/json'] >= \
|
||||
request.accept_mimetypes['text/html']
|
||||
|
||||
|
||||
@app.errorhandler(422)
|
||||
@app.errorhandler(400)
|
||||
@app.errorhandler(500)
|
||||
@app.errorhandler(404)
|
||||
def handle_error(err):
|
||||
if wants_json_response():
|
||||
return jsonify(error=str(err),code=err.code), err.code
|
||||
templates=["error/{}.html".format(err.code),"error/default.html"]
|
||||
try:
|
||||
print(dir(err))
|
||||
return render_template(templates,error=err),err.code
|
||||
except TemplateNotFound:
|
||||
return err.get_response()
|
||||
|
||||
def role_required(*roles):
|
||||
def wrapper(fn):
|
||||
@wraps(fn)
|
||||
def decorated_view(*args, **kwargs):
|
||||
if not current_user.is_authenticated():
|
||||
return current_app.login_manager.unauthorized()
|
||||
has_role=False
|
||||
user=current_app.login_manager.reload_user()
|
||||
for role in roles:
|
||||
has_role|=user.has_role(role)
|
||||
if not has_role:
|
||||
return current_app.login_manager.unauthorized()
|
||||
return fn(*args, **kwargs)
|
||||
return decorated_view
|
||||
return wrapper
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_name):
|
||||
return User.query.get(user_name)
|
||||
|
||||
|
||||
@login_manager.request_loader
|
||||
def load_user_from_header(header_val):
|
||||
for api_key in [request.args.get('api_key'),request.headers.get('X-API-Key')]:
|
||||
if api_key:
|
||||
user = User.query.filter_by(api_key=api_key).one_or_none()
|
||||
if user:
|
||||
return user
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def left_nav():
|
||||
links=[View("Home", "index"),View("Route", "route"),View("Jobs", "status",job_id=None)]
|
||||
if current_user.has_role('admin') or current_user.has_role('worker_host'):
|
||||
links.insert(2,View("Workers","worker"))
|
||||
return Navbar(
|
||||
"E:D LRR",
|
||||
*links
|
||||
)
|
||||
|
||||
|
||||
def right_nav():
|
||||
links = [View("Login", "login"), View("Register", "register")]
|
||||
if current_user.is_authenticated:
|
||||
links = [View("Change Password", "change_password"), View("Logout", "logout")]
|
||||
if current_user.has_role('admin'):
|
||||
links = [View("Admin", "admin.index")] + links
|
||||
return Navbar("", *links)
|
||||
|
||||
|
||||
register_renderer(app, "bootstrap4", BootsrapRenderer)
|
||||
nav.register_element("left_nav", left_nav)
|
||||
nav.register_element("right_nav", right_nav)
|
||||
|
||||
|
||||
def compute_route(args, kwargs):
|
||||
return ed_lrr.route(*args, **kwargs)
|
||||
|
||||
|
||||
class AnonymousUser(AnonymousUserMixin):
|
||||
|
||||
def has_role(self,role):
|
||||
return False
|
||||
|
||||
@property
|
||||
def roles(self):
|
||||
return []
|
||||
|
||||
@roles.setter
|
||||
def __set_roles(self,value):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
login_manager.anonymous_user = AnonymousUser
|
||||
|
||||
|
||||
@generic_repr
|
||||
class Worker(db.Model):
|
||||
id = db.Column(
|
||||
UUIDType(binary=False, native=False), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
name = db.Column(db.String, unique=True)
|
||||
current_job=db.Column(UUIDType(binary=False, native=False),db.ForeignKey("job.id"), nullable=True,default=None)
|
||||
job = relationship('Job',backref="workers")
|
||||
last_active = db.Column(DateTime, nullable=True,default=None)
|
||||
owner_name = db.Column(
|
||||
db.String, db.ForeignKey("user.name"), nullable=True,index=True
|
||||
)
|
||||
owner = relationship("User",backref="workers")
|
||||
|
||||
user_roles = db.Table('user_roles',
|
||||
db.Column('user_name', db.String, db.ForeignKey('user.name'),primary_key=True),
|
||||
db.Column('role_name', db.String, db.ForeignKey('role.name'),primary_key=True)
|
||||
)
|
||||
|
||||
class Role(db.Model):
|
||||
name = db.Column(db.String, unique=True,index=True,primary_key=True)
|
||||
|
||||
def __init__(self,name):
|
||||
self.name=name
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
||||
class User(db.Model, UserMixin):
|
||||
name = db.Column(db.String, unique=True,index=True,primary_key=True)
|
||||
is_active = db.Column(db.Boolean, default=False)
|
||||
api_key = db.Column(
|
||||
UUIDType(binary=False, native=False), nullable=True, default=uuid.uuid4,index=True
|
||||
)
|
||||
password = db.Column(PasswordType(schemes=["pbkdf2_sha512"], max_length=256))
|
||||
created = db.Column(DateTime, default=datetime.today)
|
||||
|
||||
roles = db.relationship("Role",secondary="user_roles")
|
||||
|
||||
def add_roles(self,roles):
|
||||
for role_name in roles:
|
||||
role=Role.query.filter_by(name=role_name).one()
|
||||
if not role in self.roles:
|
||||
self.roles.append(role)
|
||||
db.session.commit()
|
||||
|
||||
def has_role(self,role_name):
|
||||
return Role.query.join(User.roles).filter(User.name==self.name,Role.name==role_name).count()>0
|
||||
return ret
|
||||
|
||||
def reset_api_key(self):
|
||||
self.api_key=uuid,uuid4()
|
||||
db.session.add(self)
|
||||
db.session.comiit()
|
||||
|
||||
def get_id(self):
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Job(db.Model):
|
||||
id = db.Column(
|
||||
UUIDType(binary=False, native=False), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
user_name = db.Column(
|
||||
db.String, db.ForeignKey("user.name"), nullable=True,index=True
|
||||
)
|
||||
func = db.Column(db.String)
|
||||
args = db.Column(JSONType)
|
||||
kwargs = db.Column(JSONType)
|
||||
state = db.Column(JSONType, default={})
|
||||
priority = db.Column(db.Integer, default=0,nullable=True)
|
||||
created = db.Column(DateTime, default=datetime.today)
|
||||
finished = db.Column(DateTime, nullable=True, default=None)
|
||||
started = db.Column(DateTime, nullable=True, default=None)
|
||||
last_update = db.Column(DateTime, nullable=True, default=None)
|
||||
user = relationship("User",backref="jobs")
|
||||
# ============================================================
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.id)
|
||||
|
||||
|
||||
@property
|
||||
def future(self):
|
||||
fut = executor.futures._futures.get(self.id)
|
||||
return fut
|
||||
|
||||
@property
|
||||
def sort_key(self):
|
||||
state_priorities={"Queued":0,"Starting":1,"Error":1,"Stalled":1,"Running":1}
|
||||
status_key=state_priorities.get(self.status[1],-1)+1
|
||||
user=1-int(self.user is not None)
|
||||
return (user,-status_key,self.priority,self.created)
|
||||
|
||||
@property
|
||||
def age(self):
|
||||
dt=datetime.today()-self.created
|
||||
return dt - dt % timedelta(seconds=1)
|
||||
|
||||
@classmethod
|
||||
def next(cls):
|
||||
for job in sorted(cls.query.all(),key=lambda v:v.sort_key):
|
||||
if job.status[1] in ['Done']:
|
||||
continue
|
||||
return job
|
||||
return None
|
||||
# return cls.query.
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
states=[
|
||||
("primary", "Done"),
|
||||
("danger", "Error"),
|
||||
("info", "Stalled"),
|
||||
("success", "Running"),
|
||||
("secondary", "Starting"),
|
||||
("warning", "Queued")
|
||||
]
|
||||
#return states[self.id.int%len(states)]
|
||||
if self.state.get("result"):
|
||||
return ("primary", "Done")
|
||||
if self.state.get("error"):
|
||||
return ("danger", "Error")
|
||||
if self.state.get("progress"):
|
||||
if (datetime.today() - self.last_update).total_seconds() > (60 * 10):
|
||||
return ("info", "Stalled")
|
||||
return ("success", "Running")
|
||||
if self.started is not None:
|
||||
return ("secondary", "Starting")
|
||||
return ("warning", "Queued")
|
||||
|
||||
@status.setter
|
||||
def __set_status(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"args": self.args,
|
||||
"kwargs": self.kwargs,
|
||||
"state": self.state,
|
||||
"finished": self.finished,
|
||||
"created": self.created,
|
||||
"started": self.started,
|
||||
}
|
||||
|
||||
@dict.setter
|
||||
def __set_dict(self, value):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def route(self):
|
||||
try:
|
||||
return prepare_route(self.state["result"])
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def t_rem(self):
|
||||
if self.started is None:
|
||||
return None
|
||||
runtime = datetime.today() - self.started
|
||||
try:
|
||||
prc_done = self.state["progress"]["prc_done"]
|
||||
if prc_done != 0:
|
||||
t_rem = (runtime / prc_done) * (100 - prc_done)
|
||||
return timedelta(seconds=round(t_rem.total_seconds(), 0))
|
||||
return None
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@t_rem.setter
|
||||
def __set_t_rem(self, value):
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def new(cls, func, args=None, kwargs=None):
|
||||
args = args or ()
|
||||
kwargs = kwargs or {}
|
||||
job = cls(args=args, kwargs=kwargs, func=func.__qualname__)
|
||||
job.__last_upd = 0.0
|
||||
if current_user.is_authenticated:
|
||||
job.user = current_user
|
||||
db.session.add(job)
|
||||
db.session.commit()
|
||||
return job
|
||||
|
||||
def start(self):
|
||||
global executor
|
||||
self.state = {}
|
||||
self.started = None
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
args = self.args + [self.callback]
|
||||
try:
|
||||
future = executor.submit_stored(self.id, compute_route, args, self.kwargs)
|
||||
except (BrokenProcessPool, RuntimeError) as e:
|
||||
print("Error:", e)
|
||||
print("Restarting Executor!")
|
||||
executor = Executor(app)
|
||||
future = executor.submit_stored(self.id, compute_route, args, self.kwargs)
|
||||
future.add_done_callback(self.done)
|
||||
|
||||
def callback(self, cb_state):
|
||||
try:
|
||||
if self.started is None:
|
||||
self.started = datetime.today()
|
||||
if self.last_update is not None:
|
||||
time_since_last_upd = (
|
||||
datetime.today() - self.last_update
|
||||
).total_seconds()
|
||||
if time_since_last_upd < 5.0:
|
||||
return
|
||||
state = dict()
|
||||
state.update(self.state)
|
||||
state.update({"progress": cb_state})
|
||||
self.state = state
|
||||
self.last_update = datetime.today()
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def done(self, future):
|
||||
print(self.id, "DONE")
|
||||
state = dict()
|
||||
state.update(self.state)
|
||||
executor.futures.pop(self.id)
|
||||
exc = future.exception()
|
||||
if exc:
|
||||
state.update(
|
||||
{"error": {"type": type(exc).__name__, "args": list(exc.args)}}
|
||||
)
|
||||
else:
|
||||
state.update({"result": future.result()})
|
||||
self.state = state
|
||||
self.finished = datetime.now()
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
db.create_all()
|
||||
for role in ['admin','user','worker_host']:
|
||||
if Role.query.filter_by(name=role).one_or_none() is None:
|
||||
db.session.add(Role(role))
|
||||
|
||||
def create_user(name,password,roles,active=False):
|
||||
user=User.query.filter_by(name=name).one_or_none()
|
||||
if user:
|
||||
db.session.delete(user)
|
||||
user=User(name=name,password=password,is_active=active)
|
||||
user.add_roles(roles)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user
|
||||
|
||||
create_user('admin','admin',['admin','user'],True)
|
||||
create_user('user','user',['user'],True)
|
||||
create_user('host','host',['user','worker_host'],True)
|
||||
|
||||
|
||||
|
||||
class SQLAView(ModelView):
|
||||
column_exclude_list = ["password"]
|
||||
column_editable_list = []
|
||||
create_modal = True
|
||||
edit_modal = True
|
||||
can_view_details = True
|
||||
column_display_pk = True
|
||||
|
||||
def is_accessible(self):
|
||||
return current_user.is_authenticated and current_user.has_role('admin')
|
||||
|
||||
def inaccessible_callback(self, name, **kwargs):
|
||||
return redirect(url_for("login"))
|
||||
|
||||
|
||||
class UserView(SQLAView):
|
||||
from wtforms import PasswordField
|
||||
|
||||
column_list = ("name", "active", "password", "api_key","roles")
|
||||
column_formatters = {
|
||||
"password": lambda view, context, model, name: "",
|
||||
"api_key": lambda view, context, model, name: model.api_key or "",
|
||||
}
|
||||
form_extra_fields = {"password": PasswordField("Password")}
|
||||
|
||||
|
||||
class JobView(SQLAView):
|
||||
# Job.id,Job.user,Job.func,Job.args,Job.kwargs,Job.state,Job.created,Job.finished,Job.started,Job.last_update
|
||||
column_list = ("id", "status", "user", "created", "started", "finished")
|
||||
column_formatters = {
|
||||
"status": lambda view, context, model, name: model.status[1],
|
||||
}
|
||||
|
||||
|
||||
class WorkerView(SQLAView):
|
||||
pass
|
||||
# # Job.id,Job.user,Job.func,Job.args,Job.kwargs,Job.state,Job.created,Job.finished,Job.started,Job.last_update
|
||||
# column_list = ("id", "status", "user", "created", "started", "finished")
|
||||
# column_formatters = {
|
||||
# "user": lambda view, context, model, name: model.user.name
|
||||
# if model.user
|
||||
# else "",
|
||||
# "status": lambda view, context, model, name: model.status[1],
|
||||
# }
|
||||
|
||||
admin.add_view(JobView(Job, db.session))
|
||||
admin.add_view(UserView(User, db.session))
|
||||
admin.add_view(SQLAView(Worker, db.session))
|
||||
admin.add_view(SQLAView(Role, db.session))
|
||||
|
||||
|
||||
def submit_job(func, *args, **kwargs):
|
||||
job = Job.new(func, args, kwargs)
|
||||
job.start()
|
||||
return job.id
|
||||
|
||||
|
||||
@app.route("/api/route", methods=["GET", "POST"])
|
||||
@use_kwargs(
|
||||
{
|
||||
"jump_range": fields.Float(required=True),
|
||||
"mode": fields.String(
|
||||
missing="bfs", validate=validate.OneOf(["bfs", "greedy", "a-star"])
|
||||
),
|
||||
"systems": fields.DelimitedList(fields.String, required=True),
|
||||
"permute": fields.String(
|
||||
missing=None,
|
||||
validate=validate.OneOf(
|
||||
["off", "all", "keep_first", "keep_last", "keep_both"]
|
||||
),
|
||||
),
|
||||
"primary": fields.Boolean(missing=False),
|
||||
"factor": fields.Float(missing=0.5),
|
||||
}
|
||||
)
|
||||
def api_route(_=None, **args):
|
||||
if args["permute"] == "off":
|
||||
args["permute"] = None
|
||||
args["systems"] = [s.strip() for s in args["systems"]]
|
||||
args = (
|
||||
args["systems"],
|
||||
args["jump_range"],
|
||||
None,
|
||||
args["mode"],
|
||||
args["primary"],
|
||||
args["permute"] is not None,
|
||||
args["permute"] in ["keep_first", "keep_both"],
|
||||
args["permute"] in ["keep_last", "keep_both"],
|
||||
args["factor"],
|
||||
None,
|
||||
r"D:\devel\rust\ED_LRR\stars.csv",
|
||||
app.config['ROUTE_WORKERS']
|
||||
)
|
||||
return jsonify({"id": submit_job(ed_lrr.route, *args)})
|
||||
|
||||
|
||||
@app.route("/api/status")
|
||||
def api_status():
|
||||
info = {"queued_jobs": len(executor.futures._futures)}
|
||||
return jsonify(info)
|
||||
|
||||
|
||||
@app.route("/api/whoami")
|
||||
def api_whoami():
|
||||
return jsonify({'name':current_user.name})
|
||||
|
||||
|
||||
@app.route("/api/status/<uuid:job_id>")
|
||||
def api_job_status(job_id):
|
||||
job = Job.query.get_or_404(str(job_id))
|
||||
return jsonify(job.dict)
|
||||
|
||||
|
||||
@app.route("/static/<path:path>")
|
||||
def send_static(path):
|
||||
return send_from_directory("static", path)
|
||||
|
||||
|
||||
@app.route("/route", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def route():
|
||||
form = RouteForm()
|
||||
if form.validate_on_submit():
|
||||
data = dict(form.data)
|
||||
if data["permute"] == "off":
|
||||
data["permute"] = None
|
||||
del data["csrf_token"]
|
||||
del data["submit"]
|
||||
job = api_route(data)
|
||||
return redirect(url_for("status", job_id=job.json["id"]))
|
||||
return render_template("form.html", form=form, title="Plot Route")
|
||||
|
||||
|
||||
@app.route("/status/",defaults={'job_id':None})
|
||||
@app.route("/status/<uuid:job_id>")
|
||||
@login_required
|
||||
def status(job_id=None):
|
||||
if job_id is not None:
|
||||
job=Job.query.get_or_404(str(job_id))
|
||||
return render_template("job.html", job=job)
|
||||
return render_template(
|
||||
"status.html", Job=Job, state=request.args.get("state")
|
||||
)
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html")
|
||||
|
||||
@app.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for("index"))
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(name=form.data["username"]).one_or_none()
|
||||
if (user is None) or (user.password != form.data["password"]):
|
||||
flash("Invalid credentials!", "danger")
|
||||
return redirect(url_for("login"))
|
||||
if not user.is_active:
|
||||
flash("Account is deactivated!", "warning")
|
||||
return redirect(url_for("login"))
|
||||
login_user(user, remember=form.data["remember"])
|
||||
next = request.args.get('next')
|
||||
if not is_safe_url(next):
|
||||
next=None
|
||||
return redirect(next or url_for("status"))
|
||||
return render_template("form.html", form=form, title="Login")
|
||||
|
||||
|
||||
@app.route("/register", methods=["GET", "POST"])
|
||||
def register():
|
||||
form = RegisterForm()
|
||||
if form.validate_on_submit():
|
||||
if User.query.filter_by(name=form.data["username"]).one_or_none() is not None:
|
||||
flash('Username already exists','danger')
|
||||
return render_template("form.html", form=form, title="Register")
|
||||
user = User()
|
||||
user.name = form.data["username"]
|
||||
user.password = form.data["password"]
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
login_user(user)
|
||||
return redirect(url_for("status"))
|
||||
return render_template("form.html", form=form, title="Register")
|
||||
|
||||
|
||||
@app.route("/change_password", methods=["GET", "POST"])
|
||||
def change_password():
|
||||
if current_user.is_anonymous:
|
||||
return redirect(url_for("index"))
|
||||
form = ChangePasswordForm()
|
||||
if form.validate_on_submit():
|
||||
if form.data["old_password"] == current_user.password:
|
||||
current_user.password = form.data["password"]
|
||||
flash("Password changed!", "success")
|
||||
else:
|
||||
flash("Wrong password!", "danger")
|
||||
return render_template("form.html", form=form, title="Register")
|
||||
return redirect(url_for("status"))
|
||||
return render_template("form.html", form=form, title="Register")
|
||||
|
||||
@app.route("/workers/")
|
||||
@login_required
|
||||
def worker():
|
||||
return render_template("workers.html")
|
||||
|
||||
@app.route("/logout")
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for("login"))
|
||||
|
||||
|
||||
@app.before_first_request
|
||||
def resume_jobs():
|
||||
print(Job.next())
|
||||
with app.test_request_context():
|
||||
for job in Job.query.all():
|
||||
if job.status[1] != "Done":
|
||||
print("Restarting {} with state {}".format(job.id, job.status[1]))
|
||||
job.start()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="127.0.0.1", port=3777, debug=True)
|
18
ed_lrr_gui/web/config.py
Normal file
18
ed_lrr_gui/web/config.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
import os
|
||||
|
||||
SECRET_KEY = "ED_LRR_WEBAPP"
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = "sqlite:///ed_lrr_web_ui.db"
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
ROUTE_WORKERS = 0
|
||||
|
||||
EXECUTOR_TYPE = "process"
|
||||
EXECUTOR_MAX_WORKERS = os.cpu_count()-1
|
||||
EXECUTOR_FUTURES_MAX_LENGTH = 500
|
||||
|
||||
FLASK_ADMIN_SWATCH = "Darkly"
|
||||
|
||||
DEBUG_TB_TEMPLATE_EDITOR_ENABLED = True
|
||||
|
||||
MAIL_DEFAULT_SENDER = '"ED_LRR Admin" <ed_lrr@gmail.com>'
|
104
ed_lrr_gui/web/forms.py
Normal file
104
ed_lrr_gui/web/forms.py
Normal file
|
@ -0,0 +1,104 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import (
|
||||
StringField,
|
||||
PasswordField,
|
||||
FieldList,
|
||||
FloatField,
|
||||
BooleanField,
|
||||
SelectField,
|
||||
SubmitField,
|
||||
validators,
|
||||
Field,
|
||||
)
|
||||
from wtforms.widgets.html5 import NumberInput
|
||||
from wtforms.widgets import TextInput
|
||||
from wtforms.validators import ValidationError
|
||||
|
||||
class StringListField(Field):
|
||||
widget = TextInput()
|
||||
|
||||
def _value(self):
|
||||
if self.data:
|
||||
return u",".join(self.data)
|
||||
else:
|
||||
return u""
|
||||
|
||||
def process_formdata(self, valuelist):
|
||||
if valuelist:
|
||||
self.data = [x.strip() for x in valuelist[0].split(",")]
|
||||
else:
|
||||
self.data = []
|
||||
|
||||
|
||||
class RouteForm(FlaskForm):
|
||||
systems = StringListField("Systems", [validators.DataRequired()])
|
||||
jump_range = FloatField(
|
||||
"Jump Range (Ly)",
|
||||
[validators.DataRequired(), validators.NumberRange(0, None)],
|
||||
widget=NumberInput(min=0, step=0.1),
|
||||
)
|
||||
mode = SelectField(
|
||||
"Routing Mode",
|
||||
choices=[
|
||||
("bfs", "Breadth-First Search"),
|
||||
("greedy", "Greedy Search"),
|
||||
("a-star", "A*-Search"),
|
||||
],
|
||||
)
|
||||
permute = SelectField(
|
||||
"Permutation Mode",
|
||||
choices=[
|
||||
("off", "Off"),
|
||||
("keep_first", "Keep starting system"),
|
||||
("keep_last", "Keep destination system"),
|
||||
("keep_both", "Keep both endpoints"),
|
||||
],
|
||||
)
|
||||
primary = BooleanField("Only route through primary stars")
|
||||
factor = FloatField(
|
||||
"Greedyness for A*-Search (%)",
|
||||
[validators.NumberRange(0, 100)],
|
||||
default=50,
|
||||
widget=NumberInput(min=0, max=100, step=1),
|
||||
)
|
||||
|
||||
priority = FloatField(
|
||||
"Priority (0=max, 100=min)",
|
||||
[validators.NumberRange(0, 100)],
|
||||
default=0,
|
||||
widget=NumberInput(min=0, max=100, step=1),
|
||||
)
|
||||
submit = SubmitField("GO!")
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
username = StringField("Username", [validators.Required()])
|
||||
password = PasswordField("Password", [validators.Required()])
|
||||
remember = BooleanField("Remember me")
|
||||
submit = SubmitField("Login")
|
||||
|
||||
|
||||
class RegisterForm(FlaskForm):
|
||||
username = StringField("Username", [validators.Required()])
|
||||
password = PasswordField(
|
||||
"Password",
|
||||
[
|
||||
validators.Required(),
|
||||
validators.EqualTo("confirm", message="Passwords must match"),
|
||||
],
|
||||
)
|
||||
confirm = PasswordField("Verify password", [validators.Required()])
|
||||
submit = SubmitField("Login")
|
||||
|
||||
|
||||
class ChangePasswordForm(FlaskForm):
|
||||
old_password = PasswordField("Current Password", [validators.Required()])
|
||||
password = PasswordField(
|
||||
"Password",
|
||||
[
|
||||
validators.Required(),
|
||||
validators.EqualTo("confirm", message="Passwords must match"),
|
||||
],
|
||||
)
|
||||
confirm = PasswordField("Verify password", [validators.Required()])
|
||||
submit = SubmitField("Change")
|
23
ed_lrr_gui/web/static/theme.css
Normal file
23
ed_lrr_gui/web/static/theme.css
Normal file
|
@ -0,0 +1,23 @@
|
|||
body,input,select,pre {
|
||||
background-color: #222 !important;
|
||||
color: #eee;
|
||||
}
|
||||
table {
|
||||
line-height: 1;
|
||||
}
|
||||
.progress {
|
||||
background-color: #444;
|
||||
}
|
||||
.progress-bar {
|
||||
background-color: #f70;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
color: #eee !important;
|
||||
}
|
||||
|
||||
#graph {
|
||||
border: 1px solid #eee;
|
||||
width: 512px;
|
||||
height: 512px;
|
||||
}
|
5
ed_lrr_gui/web/templates/admin/index.html
Normal file
5
ed_lrr_gui/web/templates/admin/index.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
{% extends 'admin/master.html' %}
|
||||
|
||||
{% block body %}
|
||||
<p>Hello world</p>
|
||||
{% endblock %}
|
48
ed_lrr_gui/web/templates/base.html
Normal file
48
ed_lrr_gui/web/templates/base.html
Normal file
|
@ -0,0 +1,48 @@
|
|||
{% extends "bootstrap/base.html" %}
|
||||
{% import "bootstrap/utils.html" as utils %}
|
||||
{% block title %}Elite: Dangerous Long Range Router{% endblock %}
|
||||
|
||||
{% block scrips %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<link rel="stylesheet" href="{{url_for('static', filename='theme.css')}}">
|
||||
{% endblock %}
|
||||
|
||||
{% block navbar %}
|
||||
<nav class="navbar navbar-expand-lg navbar-dark" style="background-color: #222;">
|
||||
<a class="navbar-brand" href="/">E:D LRR</a>
|
||||
<ul class="navbar-nav mr-auto">
|
||||
{{nav.left_nav.render(renderer='bootstrap4')}}
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav ml-auto">
|
||||
{% if current_user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link">
|
||||
Logged in as {{current_user.name}}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{{nav.right_nav.render(renderer='bootstrap4')}}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category,message in messages %}
|
||||
<div class="alert alert-{{category}}" role="{{category}}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{# application content needs to be provided in the app_content block #}
|
||||
{% block app_content %}{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
6
ed_lrr_gui/web/templates/error/404.html
Normal file
6
ed_lrr_gui/web/templates/error/404.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block app_content %}
|
||||
<h1>404 Not Found</h1>
|
||||
<p><a href="{{ url_for('index') }}"><button type="button" class="btn btn-secondary">Back</button></a></p>
|
||||
{% endblock %}
|
16
ed_lrr_gui/web/templates/form.html
Normal file
16
ed_lrr_gui/web/templates/form.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
{% extends "base.html" %}
|
||||
{% import "bootstrap/wtf.html" as wtf %}
|
||||
|
||||
{% block app_content %}
|
||||
<h1>{{title}}</h1>
|
||||
{% for field in form %}
|
||||
{% for error in field.errors %}
|
||||
<div class="alert alert-danger" role="danger">{{error}}</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
{{ wtf.quick_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
10
ed_lrr_gui/web/templates/index.html
Normal file
10
ed_lrr_gui/web/templates/index.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block app_content %}
|
||||
<h1>E:D LRR</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
Number of Jobs: {{current_user.jobs|count}}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
131
ed_lrr_gui/web/templates/job.html
Normal file
131
ed_lrr_gui/web/templates/job.html
Normal file
|
@ -0,0 +1,131 @@
|
|||
{% extends "base.html" %}
|
||||
{% block app_content %}
|
||||
<h1>Job Status <span class="badge badge-{{job.status[0]}}">{{ job.status[1] }}</span></h1>
|
||||
<div class="row">
|
||||
<div class="col-lg-0">
|
||||
{% if job.state.error %}
|
||||
<ul>
|
||||
{% for err in job.state.error.args %}
|
||||
<li>{{err}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if job.state.progress %}
|
||||
<p class="lead">Routing from <b>{{ job.state.progress.from }}</b> to <b>{{ job.state.progress.to }}</b> using
|
||||
{{ job.state.progress.mode }}</p>
|
||||
<p>Current system: <b>{{ job.state.progress.system }}</b></p>
|
||||
<p>Search queue size: <b>{{"{:,}".format(job.state.progress.queue_size) }}</b></p>
|
||||
<p>Number of systems checked: <b>{{"{:,}".format(job.state.progress.n_seen) }}
|
||||
({{job.state.progress.prc_seen|round(2)}} %)</b></p>
|
||||
<p>Estimated time remaining: <b>{{job.t_rem}}</b></p>
|
||||
<p>Search Depth: <b>{{job.state.progress.depth}}</b></p>
|
||||
<div class="progress" style="width: 100%;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
style="width: {{job.state.progress.prc_done}}%;" role="progressbar"
|
||||
aria-valuenow="{{job.state.progress.prc_done|round(2)}}" aria-valuemin="0" aria-valuemax="100">
|
||||
{{job.state.progress.prc_done|round(2)}} %
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if job.state.result %}
|
||||
<h2>Result</h2>
|
||||
|
||||
<h3>Map</h3>
|
||||
<div id="graph">
|
||||
</div>
|
||||
<script src="https://d3js.org/d3.v5.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
function dist(a, b) {
|
||||
var sum = 0;
|
||||
for (var i = 0; i < a.length; ++i) {
|
||||
sum += Math.pow(a[i] - b[i], 2)
|
||||
}
|
||||
return Math.pow(sum, 0.5);
|
||||
}
|
||||
var width = 512;
|
||||
var height = 512;
|
||||
var route = {{job.route | tojson}};
|
||||
var vis = d3.select("#graph")
|
||||
.append("svg").attr("viewBox", [0, 0, width, height]);
|
||||
|
||||
vis.attr("width", width)
|
||||
.attr("height", height);
|
||||
var g = vis.append("g");
|
||||
|
||||
vis.call(d3.zoom()
|
||||
.extent([
|
||||
[0, 0],
|
||||
[width, height]
|
||||
])
|
||||
.on("zoom", () => {
|
||||
g.attr("transform", d3.event.transform);
|
||||
}));
|
||||
|
||||
var lines = [];
|
||||
for (var i = 0; i < route.length - 1; ++i) {
|
||||
lines.push({
|
||||
x1: route[i].pos[1],
|
||||
x2: route[i + 1].pos[1],
|
||||
y1: -route[i].pos[2],
|
||||
y2: -route[i + 1].pos[2],
|
||||
dist: dist(route[i].pos, route[i + 1].pos),
|
||||
color: route[i].color || '#eee'
|
||||
})
|
||||
}
|
||||
|
||||
g.selectAll(".line")
|
||||
.data(lines)
|
||||
.enter()
|
||||
.append("line")
|
||||
.attr("x1", (l) => l.x1)
|
||||
.attr("y1", (l) => l.y1)
|
||||
.attr("x2", (l) => l.x2)
|
||||
.attr("y2", (l) => l.y2)
|
||||
.style("stroke", (l) => l.color)
|
||||
.style("stroke-width", 5)
|
||||
.append("title")
|
||||
.text((l) => Math.round(l.dist * 100) / 100 + " Ly");
|
||||
|
||||
g.selectAll("circle .nodes")
|
||||
.data(route)
|
||||
.enter()
|
||||
.append("svg:circle")
|
||||
.attr("class", "nodes")
|
||||
.attr("cx", (d) => d.pos[1])
|
||||
.attr("cy", (d) => -d.pos[2])
|
||||
.attr("r", 10)
|
||||
.attr("fill", (d) => d.color)
|
||||
.append("title")
|
||||
.text((d) => d.body + " (" + d.star_type + ")")
|
||||
</script>
|
||||
<h3>Jumps</h3>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Num</th>
|
||||
<th scope="col">Body</th>
|
||||
<th scope="col">Type</th>
|
||||
<th scope="col">Distance from arrival</th>
|
||||
<th scope="col">Jump distance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for sys in job.route %}
|
||||
<tr>
|
||||
<th scope="row">{{sys.num}}</td>
|
||||
<td>{{sys.body}}</td>
|
||||
<td style="color: {{sys.color}}">{{sys.star_type}}</td>
|
||||
<td>{{"{:,}".format(sys.distance)}} Ls</td>
|
||||
<td>{{"{:,}".format(sys.jump_dist|round(2))}} Ly</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<th scope="row" colspan=4>Total Distance</th>
|
||||
<td>{{"{:,}".format(job.route|sum(attribute='jump_dist')|round(2))}} Ly</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
87
ed_lrr_gui/web/templates/status.html
Normal file
87
ed_lrr_gui/web/templates/status.html
Normal file
|
@ -0,0 +1,87 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block app_content %}
|
||||
{% if current_user.has_role('admin') %}
|
||||
{% set jobs = Job.query.all() %}
|
||||
{% else %}
|
||||
{% set jobs = current_user.jobs %}
|
||||
{% endif %}
|
||||
<h1>System Status</h1>
|
||||
<div class="row">
|
||||
<h2>Overview</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="table table-striped table-bordered" style="width: 1px;">
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Count</th>
|
||||
</tr>
|
||||
{% for group in (jobs|groupby('status')) %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="?state={{group.grouper[1]}}">
|
||||
<span class="badge badge-{{ group.grouper[0] }}">{{ group.grouper[1] }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{group.list|count}}
|
||||
</td>
|
||||
<tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="?">
|
||||
Total
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{jobs|count}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h2>Jobs</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<!-- Next: {{Job.next().id}} -->
|
||||
<table class="table table-striped table-bordered" style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">ID</th>
|
||||
<th scope="col">Systems</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">User</th>
|
||||
<th scope="col">Priority</th>
|
||||
<th scope="col">Progess</th>
|
||||
<th scope="col">ETC</th>
|
||||
<th scope="col">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for job in (jobs|sort(attribute='sort_key')) %}
|
||||
{% if (state==None) or job.status[1]==state %}
|
||||
<tr>
|
||||
<td style="width: 1px; white-space: nowrap;"><a href="{{url_for('status',job_id=job.id)}}">{{job.id}}</a></td>
|
||||
<td style="width: 1px; white-space: nowrap;">{{job.args[0]|join(', ')}}</td>
|
||||
<td style="width: 1px; white-space: nowrap;"><span class="badge badge-{{job.status[0]}}">{{ job.status[1] }}</span></td>
|
||||
<td style="width: 1px; white-space: nowrap;">{{job.user.name}}</td>
|
||||
<td style="width: 1px; white-space: nowrap;">{{job.priority}}</td>
|
||||
{% if job.state.progress %}
|
||||
<td>
|
||||
<div class="progress" style="width: 100%;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" style="width: {{job.state.progress.prc_done}}%;" role="progressbar" aria-valuenow="{{job.state.progress.prc_done|round(2)}}" aria-valuemin="0" aria-valuemax="100">
|
||||
{{job.state.progress.prc_done|round(2)}} %
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>Unknown</td>
|
||||
{% endif %}
|
||||
<td style="width: 1px; white-space: nowrap;">{{job.t_rem}}</td>
|
||||
<td style="width: 1px; white-space: nowrap;">{{job.age}} ago</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
14
ed_lrr_gui/web/templates/workers.html
Normal file
14
ed_lrr_gui/web/templates/workers.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block app_content %}
|
||||
<h1>Workers</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
{% if current_user.is_authenticated %}
|
||||
Hello {{current_user.name}}!
|
||||
{% else %}
|
||||
Nothing to see here!
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
73
ed_lrr_gui/web/utils.py
Normal file
73
ed_lrr_gui/web/utils.py
Normal file
|
@ -0,0 +1,73 @@
|
|||
from flask_nav.renderers import Renderer
|
||||
from dominate import tags
|
||||
from urllib.parse import urlparse,urljoin
|
||||
from flask import request
|
||||
|
||||
def is_safe_url(target):
|
||||
ref_url = urlparse(request.host_url)
|
||||
test_url = urlparse(urljoin(request.host_url, target))
|
||||
return test_url.scheme in ('http', 'https') and \
|
||||
ref_url.netloc == test_url.netloc
|
||||
|
||||
def dist(p1, p2):
|
||||
s = 0
|
||||
for c1, c2 in zip(p1, p2):
|
||||
s += (c1 - c2) ** 2
|
||||
return s ** 0.5
|
||||
|
||||
|
||||
class BootsrapRenderer(Renderer):
|
||||
def visit_Navbar(self, node):
|
||||
sub = []
|
||||
for item in node.items:
|
||||
sub.append(self.visit(item))
|
||||
return "".join([v.render() for v in sub])
|
||||
|
||||
def visit_View(self, node):
|
||||
classes = ["nav-link"]
|
||||
if node.active:
|
||||
classes.append("active")
|
||||
return tags.li(
|
||||
tags.a(node.text, href=node.get_url(), cls=" ".join(classes)),
|
||||
cls="nav-item",
|
||||
)
|
||||
|
||||
def visit_Subgroup(self, node):
|
||||
# almost the same as visit_Navbar, but written a bit more concise
|
||||
return tags.div(node.title, *[self.visit(item) for item in node.items])
|
||||
|
||||
|
||||
colors = {
|
||||
"O": "#0000FF",
|
||||
"B": "#140AF0",
|
||||
"A": "#3C1EDC",
|
||||
"F": "#EEEEEE",
|
||||
"G": "#969646",
|
||||
"K": "#B43C1E",
|
||||
"M": "#FF280A",
|
||||
"L": "#FF1E00",
|
||||
"T": "#800000",
|
||||
"Y": "#800000",
|
||||
"White Dwarf": "#5D67EF",
|
||||
"Neutron": "#99A0FF",
|
||||
}
|
||||
|
||||
|
||||
def prepare_route(route):
|
||||
entries = []
|
||||
prev = route[0]
|
||||
num = 1
|
||||
for hop in route[1:]:
|
||||
prev["jump_dist"] = dist(hop["pos"], prev["pos"])
|
||||
prev["num"] = num
|
||||
prev["color"] = colors.get(prev["star_type"].split()[0], "#eee")
|
||||
prev["distance"] = prev["distance"]
|
||||
entries.append(prev)
|
||||
prev = hop
|
||||
num += 1
|
||||
prev["jump_dist"] = 0
|
||||
prev["distance"] = prev["distance"]
|
||||
prev["num"] = num
|
||||
prev["color"] = colors.get(prev["star_type"].split()[0], "#eee")
|
||||
entries.append(prev)
|
||||
return entries
|
6
ed_lrr_gui/web/worker.py
Normal file
6
ed_lrr_gui/web/worker.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
import requests as RQ
|
||||
import _ed_lrr as ed_lrr
|
||||
|
||||
funcs = {
|
||||
func: getattr(ed_lrr, func) for func in dir(ed_lrr) if not func.startswith("_")
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue