Merge pull request 'wilhelm-usereditor' (#2) from wilhelm-usereditor into master

Reviewed-on: #2
pull/4/head
willd 1 year ago
commit baed641f31

@ -20,6 +20,7 @@ db_engine = {}
db_metadata = {}
parts = {}
octopartURL =""
baseURL = "/parts"
def getContainers():
query = "select id, name from containers order by UPPER(name);"
@ -65,21 +66,21 @@ def serveImage(img):
img_io.seek(0)
return send_file(img_io, mimetype='image/png')
@app.route('/parts', strict_slashes=True)
@app.route(baseURL, strict_slashes=True)
def index():
return render_template('partsearch.html', containers=getContainers())
return render_template('partsearch.html', containers=getContainers(), baseURL=baseURL)
@app.route('/parts/getlocationsInContainer/<containerID>')
@app.route(baseURL+'/getlocationsInContainer/<containerID>')
def get_locations_in_container(containerID):
s = 'select id, name from locations where container_id = :id order by name;'
r = db_engine.execute(text(s), id=containerID)
l={}
for row in r:
l[row[0]]= row[1];
l[str(row[0])]= row[1];
r.close()
return json.dumps(l)
@app.route('/parts/getlocationURL/<locationID>')
@app.route(baseURL+'/getlocationURL/<locationID>')
def get_locationURL(locationID):
s = 'select map from locations where id = :id;'
r = db_engine.execute(text(s), id=locationID)
@ -89,7 +90,7 @@ def get_locationURL(locationID):
r.close()
return l[0][0]
@app.route('/parts/locationEditor')
@app.route(baseURL+'/locationEditor')
def locationEditor():
query = 'select c.name as container, l.name as name, l.id, c.id as container_id from locations as l inner join containers as c on l.container_id = c.id order by container, name;'
r = db_engine.execute(text(query))
@ -97,9 +98,20 @@ def locationEditor():
for row in r:
locations.append(dict(row))
r.close()
return render_template('locationEditor.html', locations=locations, containers=getContainers())
return render_template('locationEditor.html', locations=locations, containers=getContainers(),baseURL=baseURL)
@app.route(baseURL+'/userEditor')
def userEditor():
query = 'select c.name as container, l.name as name, l.id, c.id as container_id from locations as l inner join containers as c on l.container_id = c.id order by container, name;'
query = 'select id, username from users;'
r = db_engine.execute(text(query))
users = []
for row in r:
users.append(dict(row))
r.close()
return render_template('userEditor.html', users=users, containers=getContainers(),baseURL=baseURL)
@app.route('/parts/alterLocation/<locationID>', methods=['POST'])
@app.route(baseURL+'/alterLocation/<locationID>', methods=['POST'])
@requires_auth
def alterLocation(locationID):
locationID = int(locationID)
@ -121,8 +133,30 @@ def alterLocation(locationID):
r = db_engine.execute(s, name=request.form['name'],container=request.form['container'],locationID=locationID);
r.close()
return '{"status":"ok"}'
@app.route(baseURL+'/alterUser/<userID>', methods=['POST'])
@requires_auth
def alterUser(userID):
userID = int(userID)
s = ''
if userID < 0:
# New entry
s = 'insert into users (username, password) '
s += 'values (:name, :password);'
s = text(s)
r = db_engine.execute(s,username=request.form['name'],password=request.form['password']);
r.close()
return '{"status":"ok"}'
else:
# Modify entry
s = 'update users '
s += 'set username=:name, password=:password '
s += 'where id=:userID;'
s = text(s)
r = db_engine.execute(s, name=request.form['name'],password=request.form['password'],userID=userID);
r.close()
return '{"status":"ok"}'
@app.route('/parts/getpartinfo/<partID>')
@app.route(baseURL+'/getpartinfo/<partID>')
def get_part_info(partID):
s = 'select p.id,partno,description,notes, c.name || l.name as location_descriptor, location_id, container_id, datasheet from parts as p inner join locations as l on p.location_id = l.id inner join containers as c on l.container_id = c.id where p.id = :id;'
r = db_engine.execute(text(s), id=partID)
@ -132,7 +166,7 @@ def get_part_info(partID):
r.close()
return json.dumps(l[0])
@app.route('/parts/query/<filter_dummy>/<query>') # TODO: maybe change AND to OR or maybe not
@app.route(baseURL+'/query/<filter_dummy>/<query>') # TODO: maybe change AND to OR or maybe not
def query(filter_dummy, query):
filter = request.args.to_dict()
keywords = query.split() # Default splits with spaces
@ -171,7 +205,7 @@ def query(filter_dummy, query):
r.close()
return json.dumps(l)
@app.route('/parts/map/<containerID>')
@app.route(baseURL+'/map/<containerID>')
def getMap(containerID):
s = 'select map, overlay from containers where id = :id;'
r = db_engine.execute(text(s), id=containerID)
@ -196,14 +230,14 @@ def getMap(containerID):
return serveImage(mapimage)
@app.route('/parts/getfile/<filename>')
@app.route(baseURL+'/getfile/<filename>')
def getfile(filename):
if(re.match('^[\w\-_]+.[p|P][d|D][f|F]$', filename) == None):
return 'No injections pls.'
return send_from_directory('/srv/datasheets/', filename)
@app.route('/parts/alter/<partID>', methods=['POST'])
@app.route(baseURL+'/alter/<partID>', methods=['POST'])
@requires_auth
def alter(partID):
partID = int(partID)
@ -276,7 +310,7 @@ def alter(partID):
r.close()
return '{"status":"ok", "part_id" : ' + str(new_id) + '}'
@app.route('/parts/delete/<partID>')
@app.route(baseURL+'/delete/<partID>')
@requires_auth
def delete(partID):
if int(partID) < 0:
@ -285,7 +319,7 @@ def delete(partID):
r = db_engine.execute(s, id=partID)
return '{"status":"ok"}'
@app.route('/parts/deleteLocation/<locationID>')
@app.route(baseURL+'/deleteLocation/<locationID>')
@requires_auth
def deleteLocation(locationID):
if int(locationID) < 0:
@ -293,8 +327,16 @@ def deleteLocation(locationID):
s = text('delete from locations where id=:id;')
r = db_engine.execute(s, id=locationID)
return '{"status":"ok"}'
@app.route(baseURL+'/deleteUser/<userID>')
@requires_auth
def deleteUser(userID):
if int(userID) < 0:
abort(400)
s = text('delete from users where id=:id;')
r = db_engine.execute(s, id=userID)
return '{"status":"ok"}'
@app.route('/parts/fetchOctopartSnippet/<searchTerm>')
@app.route(baseURL+'/fetchOctopartSnippet/<searchTerm>')
def fetchOctopartSnippet(searchTerm):
if octopartURL == '':
return '{"result":"octopart integration not enabled"}'
@ -334,7 +376,7 @@ if __name__ == '__main__':
app.config['SESSION_TYPE'] = 'memcached'
with open('admin.json') as f:
postgres_credentials = json.load(f)
db_engine, db_metadata = connect(postgres_credentials['username'], postgres_credentials['password'], 'parts_v2')
db_engine, db_metadata = connect(postgres_credentials['username'], postgres_credentials['password'], 'parts_v3')
parts = sqlalchemy.Table('parts', db_metadata)
try:
with open('octopartAPIkey.json') as f:
@ -350,4 +392,4 @@ if __name__ == '__main__':
'''s = select([parts]).where(parts.c.notes != '')
for row in db_engine.execute(s):
print row'''
app.run('0.0.0.0')
app.run(host='127.0.0.1', port='5000', debug=False)

@ -213,6 +213,7 @@ function show_part_info(partID) {
}).fail(function() {
console.log( "Fetching part info failed" );
});
// update URL and history
var query = $('.search-bar').val();
window.history.pushState('searching', '', 'parts?q=' + query + '&p=' + partID)
@ -248,7 +249,7 @@ function perform_query() {
$('#no-results').animate({opacity:1},2000);
console.log( "Query failed" );
});
// update URL and history
window.history.pushState('searching', '', 'parts?q=' + query)
}
@ -306,6 +307,24 @@ $(document).ready(function() {
show_part_info(search_params.get('p'));
}
//For linking to a search query or specific part
const search_params = new URLSearchParams(window.location.search);
if(search_params.has('q')){
$('.search-bar').val(search_params.get('q')); perform_query();
}
if(search_params.has('p')){
show_part_info(search_params.get('p'));
}
//For linking to a search query or specific part
const search_params = new URLSearchParams(window.location.search);
if(search_params.has('q')){
$('.search-bar').val(search_params.get('q')); perform_query();
}
if(search_params.has('p')){
show_part_info(search_params.get('p'));
}
// $('#partno-input').bind('blur',function() {
// if($('#partno-input').val().length > 0){
// octopartFetch();

@ -0,0 +1,118 @@
function new_user_entry() {
}
function init_User_edit(userID, name) {
$('#save-button').attr("onclick", "saveUser(" + userID + ")");
$('#delete-button').attr("onclick", "deleteUser(" + userID + ")");
$('#user-name-input').val(name);
$('#user-password-input').val('');
overlay_in();
}
function saveUser(userID) {
var name_v = $('#user-name-input').val();
var password_v = $('#user-password-input').val();
if(name_v.length > 100) {
alert('Name too long (max 100 characters).')
return;
}
var data = new FormData();
data.append('password', password_v);
data.append('name', name_v)
$.ajax({
url: rootURL + 'alterUser/' + userID,
type: 'POST',
data: data,
cache: false,
contentType: false,
processData: false,
success: function(data) {
alert("k.");
},
error: function() {
alert("Couldn't update the user information. Please retry.");
}
});
end_edit();
$('#edit-button').click(function() {
init_edit(userID);
});
}
function deleteUser(userID) {
if (userID < 0){
alert('Congratulations! You found the secret UI bug easter egg! This button should not be here, yet I left it specifically because I wanted an easter egg and not because of any other reasons! Good for you!')
return;
}
if (!confirm('Delete the selected user? ')){
return;
}
$.ajax({
url: rootURL + 'deleteUser/' + userID,
type: 'GET',
cache: false,
contentType: false,
processData: false,
success: function() {
alert("User removed.");
overlay_out();
location.reload();
},
fail: function() {
console.log('An error occurred while deleting the entry');
},
});
}
function end_edit(){
//intentionally left blank
}
function show_user_info(userID) {
$.getJSON(rootURL + 'getpartinfo/' + partID, function(data) {
$('table#details tr#location td p').text(text_filter(data.location_descriptor)); // name is the location friendly name
$('#location-dropdown').val(data.location_id);
$('#container-dropdown').val(data.container_id);
$('table#details tr#partno td p').text(text_filter(data.partno));
$('table#details tr#partno td input').val(text_filter(data.partno));
$('table#details tr#description td p').text(text_filter(data.description));
$('table#details tr#description td input').val(text_filter(data.description));
container_onchange();
if (data.datasheet != null) {
$('tr#datasheet-head').html($('<td>DATASHEET: <a href="parts/getfile/' + data.datasheet + '"><i class="fa fa-file-text" aria-hidden="true"></i></a></td>'));
$('#datasheet-input').val(data.datasheet);
}
else
$('tr#datasheet-head td').text('DATASHEET: ');
$('#edit-button').click(function() {
init_edit(partID);
});
$('#delete-button').click(function() {
delete_entry(partID);
});
overlay_in();
}).fail(function() {
console.log( "Fetching part info failed" );
});
}
function placeMarker(locationID){
//temporarily not used
var $img = $('#clickablemap');
var currentClickPosX = event.pageX - $img.offset().left;
var currentClickPosY = event.pageY - $img.offset().top;
var correctX = (($img.prop('naturalWidth') / $img.width()) * currentClickPosX).toFixed(0);
var correctY = (($img.prop('naturalHeight') / $img.height()) * currentClickPosY).toFixed(0);
// $("#mapURL").html("elab.png?x=" + correctX + "&y=" + correctY);
$("#clickablemap").attr("src", "map/" + $("#mapfile-input").val() + "?x=" + correctX + "&y=" + correctY);
}

@ -8,10 +8,10 @@
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
crossorigin="anonymous"></script>
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="/parts/static/style.css">
<link rel="stylesheet" type="text/css" href="{{ baseURL }}/static/style.css">
<script src="https://use.fontawesome.com/2fef7be393.js"></script>
<script type="text/javascript" src="/parts/static/common.js"></script>
<script type="text/javascript" src="/parts/static/locationEditorScript.js"></script>
<script type="text/javascript" src="{{ baseURL }}/static/common.js"></script>
<script type="text/javascript" src="{{ baseURL }}/static/locationEditorScript.js"></script>
</head>
<body>
<h1>LOCATION EDITOR</h1>

@ -9,13 +9,13 @@
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
crossorigin="anonymous"></script>
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="parts/static/style.css" media="screen and (min-width:500px)">
<link rel="stylesheet" type="text/css" href="parts/static/style-m.css" media="screen and (max-width:500px)">
<link rel="apple-touch-icon-precomposed" href="parts/static/apple-touch-icon.png">
<link rel="stylesheet" type="text/css" href="{{ baseURL }}/static/style.css" media="screen and (min-width:500px)">
<link rel="stylesheet" type="text/css" href="{{ baseURL }}/static/style-m.css" media="screen and (max-width:500px)">
<link rel="apple-touch-icon-precomposed" href="{{ baseURL }}/parts/static/apple-touch-icon.png">
<script src="https://use.fontawesome.com/2fef7be393.js"></script>
<script type="text/javascript" src="parts/static/common.js"></script>
<script type="text/javascript" src="parts/static/script.js"></script>
<script type="text/javascript" src="{{ baseURL }}/static/common.js"></script>
<script type="text/javascript" src="{{ baseURL }}/static/script.js"></script>
</head>
<body>
<h1>ELAB Part Search Engine</h1>

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>ELAB Part Search Engine - User editor</title>
<script
src="https://code.jquery.com/jquery-3.2.1.min.js"
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
crossorigin="anonymous"></script>
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="{{ baseURL }}/static/style.css">
<script src="https://use.fontawesome.com/2fef7be393.js"></script>
<script type="text/javascript" src="{{ baseURL }}/static/common.js"></script>
<script type="text/javascript" src="{{ baseURL }}/static/userEditorScript.js"></script>
</head>
<body>
<h1>USER EDITOR</h1>
<p>Get in! Get out! Get your fresh users here! <a href=".">back to parts</a></p>
<table id="results" class="results-locations">
<tr>
<th>ID</th>
<th>Username</th>
</tr>
{% for user in users %}
<tr onclick="init_User_edit({{user['id']}}, '{{user['username']}}');">
<td>{{user['id']}}</td>
<td>{{user['username']}}</td>
</tr>
{% endfor %}
</table>
<div class="bottom-spacer"></div>
<div class="shadow" onclick="overlay_out()"></div>
<div class="overlay">
<h2>User Details</h2>
<table id="details">
<tr><td>Adding a new user</td></tr>
<tr><td>Name and password for new user<br />Example: "noob"</td></tr>
<tr><td><p></p><input type="text" name="user-name-input" id="user-name-input" placeholder="Name" class="pinfo-input nothidden"/></td></tr>
<tr><td><p></p><input type="password" name="user-password-input" id="user-password-input" placeholder="Password" class="pinfo-input nothidden"/></td></tr>
</table>
<div class="round-button-left"><a href="#" id="save-button"><i class="fa fa-check" aria-hidden="true"></i></a></div>
<div class="round-button"><a href="#" id="delete-button"><i class="fa fa-trash" aria-hidden="true"></i></a></div>
<div class="small-square-button"><a href="#" onclick="overlay_out()"><i class="fa fa-times" aria-hidden="true"></i></a></div>
</div>
<div class="round-floating-button"><a href="#" onclick="init_User_edit(-1, '', 0)"><i class="fa fa-plus" aria-hidden="true"></i></a></div>
</body>
</html>

@ -5,14 +5,19 @@ ELABs custom inventory system for locating components.
The system allows searching for parts in ELAB by part name or a description text and returns the location of the part - a drawer or other compartment name and a map illustration. Finding obscure, outdated ICs was never this easy!
## Requirements
Python, and the following packages:
##Requirements (Debian-based systems)
$ sudo apt install python3 python3-pip libpq-dev postgresql-13 nginx git
Python packages:
* flask
* requests
* sqlalchemy
* werkzeug
* pillow
* psycopg2
## Adding a part
@ -49,4 +54,4 @@ AND DONE! Congratulations! You have added a part to the database! Only 563945679
* The Notes field is for notes.
## Work in progress
The system is currently being worked on. No touchy!
The system is currently being worked on. No touchy!

Loading…
Cancel
Save