diff --git a/parts/server.py b/parts/server.py old mode 100644 new mode 100755 index 54c43e1..fde1e58 --- a/parts/server.py +++ b/parts/server.py @@ -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/') +@app.route(baseURL+'/getlocationsInContainer/') 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/') +@app.route(baseURL+'/getlocationURL/') 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/', methods=['POST']) +@app.route(baseURL+'/alterLocation/', 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/', 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/') +@app.route(baseURL+'/getpartinfo/') 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//') # TODO: maybe change AND to OR or maybe not +@app.route(baseURL+'/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/') +@app.route(baseURL+'/map/') 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/') +@app.route(baseURL+'/getfile/') 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/', methods=['POST']) +@app.route(baseURL+'/alter/', 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/') +@app.route(baseURL+'/delete/') @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/') +@app.route(baseURL+'/deleteLocation/') @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/') +@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/') +@app.route(baseURL+'/fetchOctopartSnippet/') 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) diff --git a/parts/static/locationEditorScript.js b/parts/static/locationEditorScript.js old mode 100644 new mode 100755 diff --git a/parts/static/script.js b/parts/static/script.js old mode 100644 new mode 100755 index 859bf02..f575f92 --- a/parts/static/script.js +++ b/parts/static/script.js @@ -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(); diff --git a/parts/static/style-m.css b/parts/static/style-m.css old mode 100644 new mode 100755 diff --git a/parts/static/style.css b/parts/static/style.css old mode 100644 new mode 100755 diff --git a/parts/static/userEditorScript.js b/parts/static/userEditorScript.js new file mode 100755 index 0000000..9e4d1d8 --- /dev/null +++ b/parts/static/userEditorScript.js @@ -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($('DATASHEET: ')); + $('#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); +} diff --git a/parts/templates/locationEditor.html b/parts/templates/locationEditor.html old mode 100644 new mode 100755 index a32dfac..688185c --- a/parts/templates/locationEditor.html +++ b/parts/templates/locationEditor.html @@ -8,10 +8,10 @@ integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"> - + - - + +

LOCATION EDITOR

diff --git a/parts/templates/partsearch.html b/parts/templates/partsearch.html old mode 100644 new mode 100755 index 594fd4a..60f5b88 --- a/parts/templates/partsearch.html +++ b/parts/templates/partsearch.html @@ -9,13 +9,13 @@ integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"> - - - + + + - - + +

ELAB Part Search Engine

diff --git a/parts/templates/userEditor.html b/parts/templates/userEditor.html new file mode 100755 index 0000000..2a9d652 --- /dev/null +++ b/parts/templates/userEditor.html @@ -0,0 +1,53 @@ + + + + + ELAB Part Search Engine - User editor + + + + + + + + +

USER EDITOR

+

Get in! Get out! Get your fresh users here! back to parts

+ + + + + + {% for user in users %} + + + + + {% endfor %} +
IDUsername
{{user['id']}}{{user['username']}}
+
+ +
+
+

User Details

+ + + + + + + + + + +
Adding a new user
Name and password for new user
Example: "noob"

+
+
+
+
+
+ + diff --git a/readme.md b/readme.md index 595a007..2ba8336 100644 --- a/readme.md +++ b/readme.md @@ -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! \ No newline at end of file +The system is currently being worked on. No touchy!