First push
This commit is contained in:
0
ACC/cfgs/__init__.py
Normal file
0
ACC/cfgs/__init__.py
Normal file
5
ACC/cfgs/apps.py
Normal file
5
ACC/cfgs/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CfgsConfig(AppConfig):
|
||||
name = 'cfgs'
|
71
ACC/cfgs/confEdit.py
Normal file
71
ACC/cfgs/confEdit.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from django import forms
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
import re
|
||||
|
||||
from accservermanager import settings
|
||||
from accservermanager.settings import TRACKS, SESSION_TYPES, EVENT_TYPES
|
||||
|
||||
|
||||
def fieldForKey(key, value):
|
||||
# list of fields that get special treatment, other fields are derived from value type
|
||||
if key == 'track':
|
||||
return forms.ChoiceField(
|
||||
widget=forms.Select,
|
||||
choices=sorted(TRACKS),
|
||||
)
|
||||
if key == 'sessionType':
|
||||
return forms.ChoiceField(
|
||||
widget=forms.Select,
|
||||
choices=SESSION_TYPES,
|
||||
)
|
||||
if key == 'eventType':
|
||||
return forms.ChoiceField(
|
||||
widget=forms.Select,
|
||||
choices=EVENT_TYPES,
|
||||
)
|
||||
|
||||
if isinstance(value, list): return forms.CharField()
|
||||
if isinstance(value, int): return forms.IntegerField()
|
||||
if isinstance(value, float): return forms.FloatField()
|
||||
if isinstance(value, str): return forms.CharField()
|
||||
return None
|
||||
|
||||
|
||||
def createLabel(key):
|
||||
""" create label from json key, ie thisIsAKey -> This Is A Key """
|
||||
key = key[0].upper()+key[1:]
|
||||
return ' '.join(re.findall('[A-Z][^A-Z]*', key))
|
||||
|
||||
|
||||
def createForm(obj, path):
|
||||
""" create a form form the objs keys, pre-filled with its values """
|
||||
|
||||
# if the object is a list, create a form for each item
|
||||
if isinstance(obj, list):
|
||||
return [createForm(obj[i], path + [str(i)]) for i in range(len(obj))]
|
||||
|
||||
# if the object is a integer, create a temporary object and proceed
|
||||
if isinstance(obj, int) or isinstance(obj, float) or isinstance(obj, str):
|
||||
obj = {'value': obj}
|
||||
|
||||
# iterate over the objects elements and add fields to the form
|
||||
form = forms.Form()
|
||||
for key, value in sorted(obj.items(), key=lambda x:x[0]):
|
||||
# if the element is a object itself, let the form display a link to further drilldown into this object
|
||||
if isinstance(value, dict) or \
|
||||
(isinstance(value, list) and len(value)>0 and isinstance(value[0], dict)):
|
||||
form.fields[key] = forms.CharField(
|
||||
disabled=True,
|
||||
widget=forms.TextInput(
|
||||
attrs={'placeholder': '', 'disabled': True, 'hidden': True}),
|
||||
label=mark_safe('<a href="/%s">%s</a>'%('/'.join(path+[key]),key)))
|
||||
else:
|
||||
form.fields[key] = fieldForKey(key, value)
|
||||
form.fields[key].label = createLabel(key)
|
||||
form.fields[key].initial = value
|
||||
form.fields[key].required = True
|
||||
if key in settings.MESSAGES:
|
||||
form.fields[key].help_text = settings.MESSAGES[key]
|
||||
|
||||
return form
|
35
ACC/cfgs/confSelect.py
Normal file
35
ACC/cfgs/confSelect.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from django import forms
|
||||
|
||||
import os, glob, ntpath
|
||||
|
||||
from accservermanager import settings
|
||||
|
||||
|
||||
def getCfgs():
|
||||
""" Check the cfg/custom folder for available configs, return list of file names w/o suffix """
|
||||
return list(map(lambda x: os.path.splitext(ntpath.basename(x))[0],
|
||||
glob.glob('%s/*.json'%(settings.CONFIGS))))
|
||||
|
||||
|
||||
def getCfgsField(selected=None, **kwargs):
|
||||
""" A select component showing the available configs """
|
||||
return forms.TypedChoiceField(
|
||||
empty_value=None,
|
||||
choices=[(i,i) for i in getCfgs()],
|
||||
initial=None if selected is None else selected,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
class CfgsForm(forms.Form):
|
||||
""" A form with a select component showing the available configs """
|
||||
def __init__(self, selected=None):
|
||||
super().__init__()
|
||||
self.fields['cfgs'] = getCfgsField(selected)
|
||||
|
||||
|
||||
class CfgCreate(forms.Form):
|
||||
""" A form creating a new config, only holds a name field """
|
||||
name = forms.CharField(max_length=100,
|
||||
required=True,
|
||||
widget=forms.TextInput(attrs={"onkeyup":"nospaces(this)"}))
|
0
ACC/cfgs/migrations/__init__.py
Normal file
0
ACC/cfgs/migrations/__init__.py
Normal file
148
ACC/cfgs/templates/cfgs/confEdit.html
Normal file
148
ACC/cfgs/templates/cfgs/confEdit.html
Normal file
@@ -0,0 +1,148 @@
|
||||
{% extends "main.html" %}
|
||||
{% block content %}
|
||||
|
||||
<style>
|
||||
#cfg-wrapper {
|
||||
height:100%;
|
||||
position:relative;
|
||||
}
|
||||
|
||||
#cfg-inner-wrapper {
|
||||
height:100%;
|
||||
padding-top: 100px;
|
||||
}
|
||||
|
||||
#cfgs {
|
||||
height:100px;
|
||||
margin-top:-100px;
|
||||
}
|
||||
|
||||
#cfg {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
<!--margin-left: 200px;-->
|
||||
padding-left: 30px;
|
||||
<!--border-left: 1px solid #9e9e9e;-->
|
||||
}
|
||||
|
||||
sidenav {
|
||||
position: fixed!important;
|
||||
width:200px;
|
||||
padding-left:10px;
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% load django_bootstrap_breadcrumbs %}
|
||||
{% block breadcrumbs %}
|
||||
{% clear_breadcrumbs %}
|
||||
{% for b, u in path %}
|
||||
{% breadcrumb_raw_safe b u %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% load django_bootstrap5 %}
|
||||
{% bootstrap_css %}
|
||||
{% bootstrap_javascript %}
|
||||
|
||||
<div id="cfg-wrapper" class="container">
|
||||
<div class="card" style="height:100%; border:none">
|
||||
<div id="cfg-inner-wrapper">
|
||||
|
||||
<header id="cfgs">
|
||||
{% render_breadcrumbs %}
|
||||
</header>
|
||||
|
||||
{% if form is not None %}
|
||||
<div id="cfg">
|
||||
<!--<div>-->
|
||||
<!--<h6 style="font-weight:bold">{{request.path}}</h6>-->
|
||||
<form action="" method="post" style="padding-right:20px">
|
||||
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
</form>
|
||||
<!--</div>-->
|
||||
</div>
|
||||
{% endif%}
|
||||
|
||||
{% if forms is not None %}
|
||||
<div id="cfg">
|
||||
{% for frm in forms %}
|
||||
<div id="form{{forloop.counter0}}" class="card" style="display:none">
|
||||
<!--<h6 style="font-weight:bold">{{request.path}}/{{forloop.counter0}}</h6>-->
|
||||
<form action="" method="post" style="padding-right:20px">
|
||||
|
||||
{% csrf_token %}
|
||||
<!--{{ frm }}-->
|
||||
{% bootstrap_form frm %}
|
||||
<input type="hidden" name="_id" value="{{forloop.counter0}}">
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<input id="prevbutton" type="button" value="Previous" class="btn btn-primary" disabled/>
|
||||
<input id="nextbutton" type="button" value="Next" class="btn btn-primary" disabled/>
|
||||
<input id="removebutton" type="button" class="btn btn-danger" value="Remove" disabled/>
|
||||
<input id="addbutton" type="button" value="Add" class="btn btn-success" disabled/>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var form_id = 0
|
||||
|
||||
function handleButtons(new_form_id) {
|
||||
form_id = new_form_id
|
||||
$("div[id^='form']").hide();
|
||||
$("#form"+new_form_id).show();
|
||||
|
||||
if (new_form_id>0) $("#prevbutton").prop('disabled', false);
|
||||
else $("#prevbutton").prop('disabled', true);
|
||||
|
||||
if ($('#form1').length) $("#removebutton").prop('disabled', false);
|
||||
else $("#removebutton").prop('disabled', true);
|
||||
|
||||
if ($('#form'+(new_form_id+1)).length) {
|
||||
$("#nextbutton").prop('disabled', false);
|
||||
$("#addbutton").prop('disabled', true);
|
||||
} else {
|
||||
$("#nextbutton").prop('disabled', true);
|
||||
$("#addbutton").prop('disabled', false);
|
||||
}
|
||||
}
|
||||
|
||||
$("#prevbutton").click(function (){handleButtons(form_id-1)})
|
||||
$("#nextbutton").click(function (){handleButtons(form_id+1)})
|
||||
$("#addbutton").click(function() {
|
||||
window.location = '{{request.path}}'+'/add';
|
||||
})
|
||||
$("#removebutton").click(function() {
|
||||
window.location = '{{request.path}}'+'/remove/'+form_id;
|
||||
})
|
||||
|
||||
handleButtons(form_id)
|
||||
</script>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
<script>
|
||||
$(function() { $(":input").change(function() {
|
||||
$.post( '', $(this.form).serialize(), function(data) {
|
||||
alert('server response'+data);
|
||||
}, 'json' );
|
||||
})
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
88
ACC/cfgs/templates/cfgs/confSelect.html
Normal file
88
ACC/cfgs/templates/cfgs/confSelect.html
Normal file
@@ -0,0 +1,88 @@
|
||||
{% extends "main.html" %}
|
||||
{% block content %}
|
||||
|
||||
<style>
|
||||
tr:hover {
|
||||
background-color: #d3d3d3;
|
||||
}
|
||||
.container {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
{% load django_bootstrap5 %}
|
||||
{% bootstrap_css %}
|
||||
{% bootstrap_javascript %}
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h6 style="font-weight:bold">Create new config</h6>
|
||||
<form action="/cfgs/create" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<input type="submit" value="Create" class="btn btn-success">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h6 style="font-weight:bold">Existing configs</h6>
|
||||
<table>
|
||||
<tr><td style="font-weight:bold">Name</td><td style="font-weight:bold">Actions</td><td></td><td></td></tr>
|
||||
{% for cfg in cfgs %}
|
||||
<tr>
|
||||
<td style="width: 200px">
|
||||
<form id="cfgform{{forloop.counter0}}" style="margin: 0" action="/cfgs/rename" method="post">{% csrf_token %}
|
||||
<input hidden name="cfg" value="{{cfg}}">
|
||||
<i id="rename{{forloop.counter0}}" type="button"
|
||||
style="vertical-align: middle; color: blue"
|
||||
title="Rename" class="material-icons">edit</i>
|
||||
<input id="cfgname{{forloop.counter0}}" name="cfgname"
|
||||
oldvalue={{cfg}} value={{cfg}} style="width: 150px" readonly>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form style="margin: 0" action="/cfgs/clone" method="post">{% csrf_token %}
|
||||
<input hidden name="cfg" value="{{cfg}}">
|
||||
<a href="/cfgs/{{cfg}}" class="btn btn-primary">Edit</a>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form style="margin: 0" action="/cfgs/clone" method="post">{% csrf_token %}
|
||||
<input hidden name="cfg" value="{{cfg}}">
|
||||
<input type="submit" value="Clone" class="btn btn-success">
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form style="margin: 0" action="/cfgs/delete" method="post">{% csrf_token %}
|
||||
<input hidden name="cfg" value="{{cfg}}">
|
||||
<input type="submit" value="Delete" class="btn btn-danger">
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<script>
|
||||
$("#rename{{forloop.counter0}}").click(function (){
|
||||
var cfg = $("#cfgname{{forloop.counter0}}");
|
||||
var ro = cfg.prop('readonly');
|
||||
cfg.prop('readonly', !ro);
|
||||
if (ro) {
|
||||
$(this).css('color','red');
|
||||
cfg.prop('oldvalue', cfg.val());
|
||||
cfg.prop('type', 'text');
|
||||
cfg.focus()
|
||||
} else {
|
||||
$(this).css('color','blue');
|
||||
if (cfg.prop('oldvalue') != cfg.val()) {
|
||||
$("#cfgform{{forloop.counter0}}").submit();
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
12
ACC/cfgs/urls.py
Normal file
12
ACC/cfgs/urls.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.urls import path, re_path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path(r'', views.confSelect, name='confSelect'),
|
||||
path(r'delete', views.confDelete, name='confDelete'),
|
||||
path(r'create', views.confCreate, name='confCreate'),
|
||||
path(r'rename', views.confRename, name='confRename'),
|
||||
path(r'clone', views.confClone, name='confClone'),
|
||||
re_path(r'^([^/]+)/?(.*)', views.formForKey, name='formForKey'),
|
||||
]
|
161
ACC/cfgs/views.py
Normal file
161
ACC/cfgs/views.py
Normal file
@@ -0,0 +1,161 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
|
||||
import os, json, shutil, glob
|
||||
from pathlib import Path
|
||||
|
||||
from accservermanager import settings
|
||||
from accservermanager.settings import SESSION_TEMPLATE
|
||||
from cfgs.confEdit import createLabel, createForm
|
||||
from cfgs.confSelect import CfgsForm, getCfgs, CfgCreate
|
||||
|
||||
|
||||
@login_required
|
||||
def confCreate(request):
|
||||
""" Create a new config based on the backuped origin custom.json """
|
||||
_base = os.path.join(settings.ACCSERVER,'cfg','event.json')
|
||||
_f = os.path.join(settings.CONFIGS, request.POST['name']+'.json')
|
||||
if not os.path.exists(_f): shutil.copy(_base, _f)
|
||||
return HttpResponseRedirect('/cfgs')
|
||||
|
||||
|
||||
@login_required
|
||||
def confClone(request):
|
||||
""" Clone a config file """
|
||||
_f = os.path.join(settings.CONFIGS, request.POST['cfg']+'.json')
|
||||
_n = os.path.join(settings.CONFIGS, request.POST['cfg']+'_clone.json')
|
||||
if not os.path.exists(_n): shutil.copy(_f, _n)
|
||||
return HttpResponseRedirect('/cfgs')
|
||||
|
||||
|
||||
@login_required
|
||||
def confRename(request):
|
||||
""" Rename a config file """
|
||||
_o = request.POST['cfg']
|
||||
_n = request.POST['cfgname']
|
||||
_old = os.path.join(settings.CONFIGS, _o+'.json')
|
||||
_new = os.path.join(settings.CONFIGS, _n+'.json')
|
||||
if _o != _n and os.path.exists(_old) and not os.path.exists(_new):
|
||||
# create a copy of the config with the new name
|
||||
shutil.copy(_old, _new)
|
||||
# change the link of instances using the old config
|
||||
from instances.views import executors
|
||||
for inst_dir in glob.glob(os.path.join(settings.INSTANCES, '*')):
|
||||
inst_name = os.path.split(inst_dir)[-1]
|
||||
if inst_name in executors:
|
||||
executors[inst_name].config = _n+'.json'
|
||||
|
||||
cfg = os.path.join(inst_dir, 'cfg', 'event.json')
|
||||
if Path(cfg).resolve().name == _o+'.json':
|
||||
os.remove(cfg)
|
||||
os.symlink(_new, cfg)
|
||||
|
||||
# finally, remove the original config
|
||||
os.remove(_old)
|
||||
|
||||
return HttpResponseRedirect('/cfgs')
|
||||
|
||||
|
||||
@login_required
|
||||
def confDelete(request):
|
||||
""" Delete a config file """
|
||||
_f = os.path.join(settings.CONFIGS, request.POST['cfg']+'.json')
|
||||
if os.path.exists(_f):
|
||||
# check that no executor references the cfg
|
||||
# TODO: give some feedback to the UI!
|
||||
for inst_dir in glob.glob(os.path.join(settings.INSTANCES, '*')):
|
||||
cfg = os.path.join(inst_dir, 'cfg', 'event.json')
|
||||
if Path(cfg).resolve().name == request.POST['cfg']+'.json':
|
||||
return HttpResponseRedirect('/cfgs')
|
||||
os.remove(_f)
|
||||
return HttpResponseRedirect('/cfgs')
|
||||
|
||||
|
||||
@login_required
|
||||
def confSelect(request):
|
||||
""" Show available configs and form to create a new config """
|
||||
context = {
|
||||
'form' : CfgCreate(),
|
||||
'cfgs' : getCfgs(),
|
||||
}
|
||||
return render(request, 'cfgs/confSelect.html', context)
|
||||
|
||||
|
||||
def formForKey(request, config, *args):
|
||||
""" Read the select config file and display the selected portion of the json object """
|
||||
cfg_path = os.path.join(settings.CONFIGS, config+'.json')
|
||||
cfg = json.load(open(cfg_path, 'r', encoding='utf-16'))
|
||||
if len(cfg['sessions']) == 0:
|
||||
cfg['sessions'] = [SESSION_TEMPLATE]
|
||||
args = args[0]
|
||||
|
||||
# drill down into the json object to the selected part
|
||||
obj = cfg
|
||||
path = ['cfgs',config]
|
||||
if len(args)>0:
|
||||
for arg in args.split('/'):
|
||||
if isinstance(obj, list):
|
||||
# handle add/remove requests for list objects
|
||||
if arg in ['add','remove']:
|
||||
# copy the last object and add to the list
|
||||
if arg=='add': obj.append(obj[-1])
|
||||
# remove selected element
|
||||
elif arg=='remove':
|
||||
obj.remove(obj[int(args.split('/')[-1])])
|
||||
|
||||
json.dump(cfg, open(cfg_path, 'w', encoding='utf-16'))
|
||||
return HttpResponseRedirect('/'+'/'.join(path))
|
||||
|
||||
# select specific element of the list object
|
||||
arg = int(arg)
|
||||
# if the element is not in the list, redirect to the current path
|
||||
# (happens eg when jumping to another config which doesn't have the same
|
||||
# number of events)
|
||||
if arg>=len(obj): return HttpResponseRedirect('/'+'/'.join(path))
|
||||
|
||||
obj = obj[arg]
|
||||
path.append(str(arg))
|
||||
|
||||
if request.method == 'POST':
|
||||
# another config was selected
|
||||
if 'cfgs' in request.POST.keys():
|
||||
return HttpResponseRedirect('/cfgs/%s/%s'%(request.POST['cfgs'],args))
|
||||
|
||||
# the form was submitted, update the values in the json obj and dump it to file
|
||||
if isinstance(obj, list): obj = obj[int(request.POST['_id'])]
|
||||
for key, value in request.POST.items():
|
||||
if key in ['csrfmiddlewaretoken', '_id']: continue
|
||||
|
||||
if isinstance(obj[key], list): continue
|
||||
if isinstance(obj[key], int): value = int(value)
|
||||
elif isinstance(obj[key], float): value = float(value)
|
||||
elif not isinstance(obj[key], str):
|
||||
print('Unknown type',type(obj[key]), type(value))
|
||||
obj[key] = value
|
||||
|
||||
json.dump(cfg, open(cfg_path, 'w', encoding='utf-16'))
|
||||
return HttpResponseRedirect(request.path)
|
||||
|
||||
_form, _forms = None, None
|
||||
_form = createForm(obj, path)
|
||||
|
||||
# ugly switch to handle list-values (like events)
|
||||
if isinstance(_form, list):
|
||||
_forms = _form
|
||||
_form = None
|
||||
|
||||
# extract first level keys of the json object, they are displayed in the navigation sidebar
|
||||
# only if they are not emptylists
|
||||
emptyList = lambda x: not (isinstance(cfg[x], list) and len(cfg[x])==0)
|
||||
keys = sorted(filter(emptyList, cfg.keys()))
|
||||
|
||||
context = {
|
||||
# 'keys': [(k, createLabel(k)) for k in keys],
|
||||
'cfgs': CfgsForm(config),
|
||||
'cfg': config,
|
||||
'path': [(j, '/'+'/'.join(path[:i+1])) for i,j in enumerate(path)],
|
||||
'forms': _forms,
|
||||
'form': _form
|
||||
}
|
||||
return render(request, 'cfgs/confEdit.html', context)
|
Reference in New Issue
Block a user