mirror of
https://github.com/FAUSheppy/tmnf-replay-server.git
synced 2025-12-07 15:41:36 +01:00
wip: map info datatables
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ instance/
|
|||||||
*.sqlite
|
*.sqlite
|
||||||
uploads/
|
uploads/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
*.db
|
||||||
|
|||||||
112
server.py
112
server.py
@@ -10,6 +10,7 @@ import datetime
|
|||||||
|
|
||||||
from pygbx import Gbx, GbxType
|
from pygbx import Gbx, GbxType
|
||||||
|
|
||||||
|
import sqlalchemy
|
||||||
from sqlalchemy import Column, Integer, String, Boolean, or_, and_, asc, desc
|
from sqlalchemy import Column, Integer, String, Boolean, or_, and_, asc, desc
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ class Map(db.Model):
|
|||||||
|
|
||||||
__tablename__ = "maps"
|
__tablename__ = "maps"
|
||||||
|
|
||||||
map_uid = Column(Integer, primary_key=True)
|
map_uid = Column(String, primary_key=True)
|
||||||
mapname = Column(String)
|
mapname = Column(String)
|
||||||
|
|
||||||
class ParsedReplay(db.Model):
|
class ParsedReplay(db.Model):
|
||||||
@@ -59,6 +60,94 @@ class ParsedReplay(db.Model):
|
|||||||
time=self.get_human_readable_time(),
|
time=self.get_human_readable_time(),
|
||||||
map_n=self.guess_map(), login=self.login, uploader=self.uploader)
|
map_n=self.guess_map(), login=self.login, uploader=self.uploader)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
d = dict()
|
||||||
|
d.update({ "login" : self.login })
|
||||||
|
d.update({ "race_time" : self.get_human_readable_time() })
|
||||||
|
d.update({ "filepath" : self.filepath })
|
||||||
|
d.update({ "upload_dt" : self.upload_dt })
|
||||||
|
return d
|
||||||
|
|
||||||
|
class DataTable():
|
||||||
|
|
||||||
|
def __init__(self, d, cols):
|
||||||
|
self.draw = int(d["draw"])
|
||||||
|
self.start = int(d["start"])
|
||||||
|
self.length = int(d["length"])
|
||||||
|
self.trueLength = -1
|
||||||
|
self.searchValue = d["search[value]"]
|
||||||
|
self.searchIsRegex = d["search[regex]"]
|
||||||
|
self.cols = cols
|
||||||
|
self.orderByCol = int(d["order[0][column]"])
|
||||||
|
self.orderDirection = d["order[0][dir]"]
|
||||||
|
|
||||||
|
# order variable for use with pythong sorted etc #
|
||||||
|
self.orderAsc = self.orderDirection == "asc"
|
||||||
|
|
||||||
|
# oder variable for use with sqlalchemy
|
||||||
|
if self.orderAsc:
|
||||||
|
self.orderAscDbClass = sqlalchemy.asc
|
||||||
|
self.orderAscDbClassReverse = sqlalchemy.desc
|
||||||
|
else:
|
||||||
|
self.orderAscDbClass = sqlalchemy.desc
|
||||||
|
self.orderAscDbClassReverse = sqlalchemy.asc
|
||||||
|
|
||||||
|
def __build(self, results, total, filtered):
|
||||||
|
|
||||||
|
self.cacheResults = results
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
resultDicts = [ r.to_dict() for r in results ]
|
||||||
|
|
||||||
|
# data list must have the correct order (same as table scheme) #
|
||||||
|
rows = []
|
||||||
|
for r in resultDicts:
|
||||||
|
singleRow = []
|
||||||
|
for key in self.cols:
|
||||||
|
singleRow.append(r[key])
|
||||||
|
rows.append(singleRow)
|
||||||
|
|
||||||
|
|
||||||
|
d = dict()
|
||||||
|
d.update({ "draw" : self.draw })
|
||||||
|
d.update({ "recordsTotal" : total })
|
||||||
|
d.update({ "recordsFiltered" : filtered })
|
||||||
|
d.update({ "data" : rows })
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
def get(self, map_uid=None):
|
||||||
|
|
||||||
|
filtered = 0
|
||||||
|
total = 0
|
||||||
|
|
||||||
|
# base query
|
||||||
|
query = db.session.query(ParsedReplay)
|
||||||
|
if map_uid:
|
||||||
|
print("Filter for map: {}".format(map_uid))
|
||||||
|
query = query.filter(ParsedReplay.map_uid == map_uid)
|
||||||
|
|
||||||
|
total = query.count()
|
||||||
|
if self.searchValue:
|
||||||
|
|
||||||
|
# search string (search for all substrings individually #
|
||||||
|
filterQuery = query
|
||||||
|
|
||||||
|
for substr in self.searchValue.split(" "):
|
||||||
|
searchSubstr = "%{}%".format(substr.strip())
|
||||||
|
filterQuery = filterQuery.filter(ParsedReplay.tags.like(searchSubstr))
|
||||||
|
|
||||||
|
filtered = filterQuery.count()
|
||||||
|
results = filterQuery.offset(self.start).limit(self.length).all()
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
query = query.order_by(self.orderAscDbClassReverse(ParsedReplay.race_time))
|
||||||
|
results = query.offset(self.start).limit(self.length).all()
|
||||||
|
filtered = total
|
||||||
|
|
||||||
|
return self.__build(results, total, filtered)
|
||||||
|
|
||||||
def replay_from_path(fullpath, uploader=None):
|
def replay_from_path(fullpath, uploader=None):
|
||||||
|
|
||||||
if not fullpath.endswith(".gbx"):
|
if not fullpath.endswith(".gbx"):
|
||||||
@@ -90,17 +179,30 @@ def replay_from_path(fullpath, uploader=None):
|
|||||||
if uploader in app.config["TRUSTED_UPLOADERS"]:
|
if uploader in app.config["TRUSTED_UPLOADERS"]:
|
||||||
m = Map(map_uid=replay.map_uid, mapname=replay.guess_map())
|
m = Map(map_uid=replay.map_uid, mapname=replay.guess_map())
|
||||||
db.session.merge(m)
|
db.session.merge(m)
|
||||||
dn.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return replay
|
return replay
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/map")
|
||||||
def list():
|
def list():
|
||||||
# TODO list maps by mapnames
|
# TODO list maps by mapnames
|
||||||
# TODO list replays by mapnames
|
# TODO list replays by mapnames
|
||||||
# TODO list by user
|
# TODO list by user
|
||||||
# TODO show all/show only best
|
# TODO show all/show only best
|
||||||
return flask.render_template("index.html")
|
header_col = ["Player", "Time", "Date", "Replay"]
|
||||||
|
map_uid = flask.request.args.get("map_uid")
|
||||||
|
return flask.render_template("index.html", header_col=header_col, map_uid=map_uid)
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def mapnames():
|
||||||
|
|
||||||
|
@app.route("/data-source<path:path>", methods=["POST"])
|
||||||
|
def source():
|
||||||
|
|
||||||
|
# path = map_uid
|
||||||
|
dt = DataTable(flask.request.form.to_dict(), ["login", "race_time", "upload_dt", "filepath" ])
|
||||||
|
jsonDict = dt.get(path)
|
||||||
|
return flask.Response(json.dumps(jsonDict), 200, mimetype='application/json')
|
||||||
|
|
||||||
@app.route("/upload", methods = ['GET', 'POST'])
|
@app.route("/upload", methods = ['GET', 'POST'])
|
||||||
def upload():
|
def upload():
|
||||||
@@ -111,7 +213,7 @@ def upload():
|
|||||||
fname = werkzeug.utils.secure_filename(f_storage.filename)
|
fname = werkzeug.utils.secure_filename(f_storage.filename)
|
||||||
fullpath = os.path.join("uploads/", fname)
|
fullpath = os.path.join("uploads/", fname)
|
||||||
f_storage.save(fullpath)
|
f_storage.save(fullpath)
|
||||||
replay = replay_from_path(fullpath)
|
replay = replay_from_path(fullpath, uploader="sheppy")
|
||||||
print(replay)
|
print(replay)
|
||||||
db.session.add(replay)
|
db.session.add(replay)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|||||||
34
templates/datatable.html
Normal file
34
templates/datatable.html
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<div style="font-size: 16px; font-weight: 300;" class="mt-5 mb-3 ml-2 mr-2" role="main">
|
||||||
|
<table id="tableMain" class="table table-striped table-bordered table-sm"
|
||||||
|
cellspacing="0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{% for item in header_col %}
|
||||||
|
<th class="th-sm font-weighIt-bold">{{ item }}</th>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<!-- tbody serverside processing -->
|
||||||
|
</table>
|
||||||
|
<script defer>
|
||||||
|
var dt = null
|
||||||
|
$(document).ready(function () {
|
||||||
|
dt = $('#tableMain').DataTable({
|
||||||
|
serverSide: true,
|
||||||
|
ajax: {
|
||||||
|
url: '/data-source',
|
||||||
|
type: 'POST'
|
||||||
|
},
|
||||||
|
"columnDefs": [
|
||||||
|
{
|
||||||
|
"targets": 3,
|
||||||
|
"render": function ( data, type, full, meta ) {
|
||||||
|
return '<a href=\"/static/'+data+'\" download>Download</a>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
$('.dataTables_length').addClass('bs-select');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
||||||
<meta name="description" content="Image factory providing automatically resized and re-encoded images.">
|
|
||||||
<meta name="author" content="Yannik Schmidt">
|
|
||||||
<meta name="title" content="Image Factory">
|
|
||||||
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:title" content="Image Factory" />
|
|
||||||
<meta property="og:description" content="Image Factory developed by Yannik Schmidt" />
|
|
||||||
<meta property="og:url" content="/" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="dev-info">
|
|
||||||
Image Factory for internal use,
|
|
||||||
developed by <a href="https://potaris.de">Yannik Schmidt</a><br>
|
|
||||||
Availiable on
|
|
||||||
<a href="https://github.com/FAUSheppy/python-flask-picture-factory">FAUSheppy GitHub</a>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
Public Images List:
|
|
||||||
</p>
|
|
||||||
<div class="overview">
|
|
||||||
<p>
|
|
||||||
{% for path in paths %}
|
|
||||||
{{ path }}<br>
|
|
||||||
{% endfor %}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
31
templates/map-info.html
Normal file
31
templates/map-info.html
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="description" content="Image factory providing automatically resized and re-encoded images.">
|
||||||
|
<meta name="author" content="Yannik Schmidt">
|
||||||
|
<meta name="title" content="TM Replays">
|
||||||
|
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:title" content="Trackmania Replay Server" />
|
||||||
|
<meta property="og:description" content="TM Replay Server developed by Yannik Schmidt" />
|
||||||
|
<meta property="og:url" content="/" />
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="/defaultFavicon.ico">
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Bootstrap core JS -->
|
||||||
|
<script src="https://cdn.atlantishq.de/js/jquery.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Bootstrap core CSS -->
|
||||||
|
<link href="https://cdn.atlantishq.de/fontawesome/css/all.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.atlantishq.de/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<script defer src="https://cdn.atlantishq.de/js/bootstrap.min.js"></script>
|
||||||
|
<script defer src="https://cdn.atlantishq.de/js/addons/datatables.min.js"></script>
|
||||||
|
|
||||||
|
<!-- mdb -->
|
||||||
|
<link href="https://cdn.atlantishq.de/css/mdb.min.css" rel="stylesheet">
|
||||||
|
<script defer src="https://cdn.atlantishq.de/js/mdb.min.js"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% include "datatable.html" %}
|
||||||
|
</body>
|
||||||
Reference in New Issue
Block a user