diff --git a/SmearcarDB/.gitignore b/SmearcarDB/.gitignore index a4853d7..fef690a 100644 --- a/SmearcarDB/.gitignore +++ b/SmearcarDB/.gitignore @@ -112,3 +112,5 @@ venv.bak/ # Other *.p +*.txt +*.db diff --git a/SmearcarDB/server.py b/SmearcarDB/server.py index 0c035d7..8a532d7 100644 --- a/SmearcarDB/server.py +++ b/SmearcarDB/server.py @@ -1,72 +1,219 @@ -import pickle from flask import Flask from flask import render_template, jsonify, request -import ulid +from flask_sqlalchemy import SQLAlchemy +import time app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data.db' app.config.update( DEBUG=True, TEMPLATES_AUTO_RELOAD=True ) +db = SQLAlchemy(app) -try: - with open("save.p", "rb") as f: - database = pickle.load(f) - # Can be commented out after non-id languages are all converted - for item in database['values']: - if not 'id' in item: - item['id'] = ulid.new().str -except (FileNotFoundError) as e: - database = {'languages': [], - 'phonemes': [], - 'values': []} -def saveDatabase(): - # Save copy under separate name - with open("newestsave.p", "wb") as f: - pickle.dump(database, f) +class Frequency(db.Model): + language_id = db.Column(db.Integer, db.ForeignKey('language.id'), primary_key=True) + phoneme_id = db.Column(db.Integer, db.ForeignKey('phoneme.id'), primary_key=True) + value = db.Column(db.Float(6), nullable=False) + phoneme = db.relationship('Phoneme') + + +class Language(db.Model): + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + name = db.Column(db.String(100), nullable=False) + source = db.Column(db.LargeBinary) + phonemes = db.relationship('Frequency') + + +class Phoneme(db.Model): + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + name = db.Column(db.String(5), nullable=False, unique=True) + + +class Update(db.Model): + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + author = db.Column(db.String(30), nullable=False) + title = db.Column(db.String(100), nullable=False) + content = db.Column(db.Text, nullable=False) + date = db.Column(db.BigInteger, nullable=False, + default=int(time.time()*1000)) + + +def generate_key(): + pass + + +class Editor(db.Model): + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + description = db.Column(db.String(75), nullable=False) + authority = db.Column(db.Integer, nullable=False, default=1) + # 0: Full Access + # 1: Edit values and Add files + # 2: Edit values + # 3: No Access + + token = db.Column(db.String(32), nullable=False, default=generate_key) + date = db.Column(db.BigInteger, nullable=False, + default=int(time.time()*1000)) + + +def database(): + final = {'values': []} + final['languages'] = [f.name for f in Language.query.all()] + final['phonemes'] = [f.name for f in Phoneme.query.all()] + for language in Language.query.all(): + languageobject = {'id': language.id, + 'name': language.name, + 'source': language.source, + 'phonemes': {}} + for frequency in language.phonemes: + languageobject['phonemes'][frequency.phoneme.name] = frequency.value + final['values'].append(languageobject) + return final + +def phoneme_add(info): + """Add or edit value associated with phoneme.""" + # info = { + # language_id: language_id, + # phoneme: phoneme_name, + # value: phoneme_value + # } + phoneme = Phoneme.query.filter_by(name=info['phoneme']).first() + language = Language.query.filter_by(id=info['language_id']).first() + if phoneme: + link = Frequency.query.filter_by( + language_id=language.id, + phoneme_id=phoneme.id).first() + link.value = info['value'] + else: + phoneme = Phoneme(name=info['phoneme']) + link = Frequency(value=info['value']) + link.phoneme = phoneme + language.phonemes.append(link) + db.session.add_all([phoneme, link]) + + +def phoneme_remove(info): + """Remove a phoneme from a language.""" + # info = { + # language_id: language_id, + # phoneme_id: phoneme_id + # } + phoneme = Phoneme.query.filter_by(id=info['phoneme_id']).first() + language = Language.query.filter_by(id=info['language_id']).first() + frequency = Frequency.query.filter_by( + phoneme_id=info['phoneme_id'], language_id=info['language_id']).first() + + if Frequency.query.filter_by(phoneme_id=info['phoneme_id']).count() == 1: + # Delete phoneme + db.session.delete(phoneme) + + language.phonemes = [frequency for frequency in language.phonemes + if frequency.phoneme_id != info['phoneme_id']] + db.session.delete(frequency) + + +def language_name_edit(info): + """Edit the name of a Language.""" + # info = { + # language_id: language_id, + # language_name: name + # } + language = Language.query.filter_by(id=info['language_id']).first() + language.name = info['language_name'] + + +def language_source_add(info): + """Add or replace a source""" + # info = { + # language_id: language_id, + # language_source = source + # } + language = Language.query.filter_by(id=info['language_id']).first() + language.source = info['language_source'] + + +patch_functions = { + "phoneme_add": phoneme_add, # Add and edit value + "phoneme_remove": phoneme_remove, # Remove association and/or phoneme + "language_name_edit": language_name_edit, # Change language name + "language_source_add": language_source_add # Add/edit source +} + # Render the client at the default URL @app.route("/") def initial(): return render_template('index.html') + +# GET method for files +@app.route("/server/") +def file_return(lang_id): + return Language.query.filter_by(id=lang_id).first().source + + # Place for client to communicate with the server @app.route("/server", methods=["GET", "POST", "PATCH"]) -# TODO add more methods def backend(): - # GET method returns the latest database - if request.method == "GET": - return jsonify(database) + # # GET method returns the latest database + # if request.method == "GET": + # return jsonify(database()) # POST method appends input to database['values'] - elif request.method == "POST": - newlanguage = request.get_json() - newlanguage['id'] = ulid.new().str - database['values'].append(newlanguage) + if request.method == "POST": + received = request.get_json() + language = Language(name=received['name'], source=received['source']) + db.session.add(language) - # Add new phonemes - newphonemes = list(newlanguage[ 'phonemes' ]) - uniquephonemes = list(set(newphonemes) - set(database[ 'phonemes' ])) - database['phonemes'] = database['phonemes'] + uniquephonemes - - # Add new language - newlangname = {newlanguage['name']} - uniquelanguages = list(newlangname - set(database['languages'])) - database['languages'] = database['languages'] + uniquelanguages - - saveDatabase() - return jsonify(database) + for phoneme, value in received['phonemes'].items(): + with db.session.no_autoflush: + search = Phoneme.query.filter_by(name=phoneme).first() + if not search: + search = Phoneme(name=phoneme) + db.session.add(search) + link = Frequency(value=value, phoneme=search) + language.phonemes.append(link) + db.session.add(link) + db.session.commit() + # return jsonify(database()) # PATCH method inputs edited language and returns updated database elif request.method == "PATCH": - newlanguage = request.get_json() - database['values'] = [newlanguage if language['id'] == newlanguage['id'] else language for language in database['values']] - saveDatabase() - return jsonify(database) + received = request.get_json() + patch_functions[received['action']](received['data']) + db.session.commit() + + return jsonify(database()) + + +# Manipulate Updates +@app.route("/updates", methods=["GET", "POST", "PATCH"]) +def updates(): + + if request.method == "POST": + received = request.get_json() + update = Update(author=received['author'], + title=received['title'], + content=received['content']) + db.session.add(update) + + elif request.method == "PATCH": + received = request.get_json() + update = Update.query.filter_by(id=received['id']).first() + update.name = received['author'] + update.title = received['title'] + update.content = received['content'] + + db.session.commit() + return jsonify([{"author": update.name, + "id": update.id, + "title": update.title, + "content": update.content, + "date": update.date} + for update in Update.query.all()]) - else: - return if __name__ == "__main__": app.run(host="0.0.0.0") diff --git a/SmearcarDB/static/index.css b/SmearcarDB/static/index.css index 51591f7..cd35a2c 100644 --- a/SmearcarDB/static/index.css +++ b/SmearcarDB/static/index.css @@ -203,11 +203,6 @@ a { grid-gap: 2vh; } - #dataValues { - grid-template-columns: 1fr 1fr; - grid-template-rows: 2fr 5fr 2fr; -} - #home { display: block; @@ -265,6 +260,25 @@ a { grid-row: 2; } +.row { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 100%; + grid-gap: 2vh; +} + +#dataValues > div { + margin-bottom: 2vh; +} + +#dataValues .row { + height: 20%; +} + +#dataValues #dataTable { + height: 42%; +} + #langSelect { grid-column: 1; grid-row: 1; @@ -288,11 +302,6 @@ a { margin: auto 0 auto 4vh; } -#dataTable { - grid-column: 1 / 3; - grid-row: 2; -} - #dataTableCont { display: grid; font-weight: 300; @@ -313,12 +322,21 @@ a { padding: 0 5% 0 5%; } -#dataGraph1 { - grid-column: 1; - grid-row: 3; +#dataTableCont input { + border: 1px solid rgba(0,0,0,0.3); + font-size: 100%; + width: 60%; + padding: 0; + font-family: 'Saira Condensed', sans-serif; + font-weight: 300; + } -#dataGraph2 { - grid-column: 2; - grid-row: 3; +#langGraph { + display: block; +} + +#langGraph canvas { + grid-column: 1; + grid-row: 1; } \ No newline at end of file diff --git a/SmearcarDB/static/index.js b/SmearcarDB/static/index.js index 2f607ed..7791890 100644 --- a/SmearcarDB/static/index.js +++ b/SmearcarDB/static/index.js @@ -1,6 +1,8 @@ var navSelect = "home"; var serverURL = window.location.origin; var data; +var languageChart; +var dataOpen = false; // var trelloInfo = {}; @@ -22,6 +24,28 @@ var dropOp = { var dropOpStore = {}; +// Left This so that the post function can be reused + +// function temporary(data) { +// for(var i = 0; i < data.length; i++) { +// $.ajax({ +// url: serverURL + '/server', +// type: 'POST', +// data: JSON.stringify(data[i]), +// dataType: "json", +// contentType: 'application/json;charset=UTF-8' +// }) +// .then( +// function success(data) { +// console.log(data); +// }, +// function error(e) { +// console.log(e); +// } +// ); +// } +// } + function createNav() { for (var i = 0; i < navi.length; i++) { // Create navigation tabs. var side = document.getElementById("sidebar"); @@ -50,11 +74,7 @@ function updateMain(op) { // Updates the actual page. setTimeout(function() { console.log(op); document.getElementById(navSelect).style.display = "none"; - if(op === "home") { - document.getElementById(op).style.display = "block"; - } else { - document.getElementById(op).style.display = "grid"; - } + document.getElementById(op).style.display = "block"; setTimeout(function() { document.getElementById(op).style.opacity = "1"; }, 30); @@ -113,8 +133,10 @@ function generateDropOp() { // For options that change based on data. var langInfo = language(dropOpStore["langSelect"]); var info = document.getElementById("langInfoCont"); var dataBox = document.getElementById("dataTableCont"); + var graph = document.querySelectorAll("#langGraph > canvas")[0]; info.style.opacity = "0"; dataBox.style.opacity = "0"; + graph.style.opacity = "0"; setTimeout(function() { while (info.firstChild) { info.removeChild(info.firstChild); @@ -124,16 +146,17 @@ function generateDropOp() { // For options that change based on data. var a = document.createElement("a"); p.appendChild(document.createTextNode("Type: " + (langInfo.type || "N/A"))); p2.appendChild(document.createTextNode("Source: ")); - if(langInfo.source.length > 0) { + if(langInfo.source === null) { + p2.appendChild(document.createTextNode("N/A")); + } else if(langInfo.source.length > 0) { a.href = langInfo.source; srcText = (langInfo.source.length > 60) ? langInfo.source.substring(0, 57) + "..." : langInfo.source; a.appendChild(document.createTextNode(srcText)); p2.appendChild(a); - } else { - p2.appendChild(document.createTextNode("N/A")); } info.appendChild(p); info.appendChild(p2); + // Generate data box material. var phonemes = Object.keys(langInfo.phonemes); @@ -165,16 +188,134 @@ function generateDropOp() { // For options that change based on data. dataBox.children[tableNum].appendChild(pT2); } p1.appendChild(document.createTextNode(phonemes[i])); - p2.appendChild(document.createTextNode(langInfo.phonemes[phonemes[i]])); + p2.appendChild(document.createTextNode(langInfo.phonemes[phonemes[i]])); + p2.className = "dataEdit"; + p2.onclick = function(event) { + if(this === event.target) return; + closeEditInput(); + dataOpen = true; + var input = document.createElement("input"); + var value = this.childNodes[0].nodeValue; + this.removeChild(this.childNodes[0]); + this.appendChild(input); + input.value = value; + input.id = "dataOpen"; + input.focus(); + } dataBox.children[tableNum].appendChild(p1); dataBox.children[tableNum].appendChild(p2); } + var graphData = Object.entries(langInfo.phonemes).sort(function(a,b) { + return b[1] - a[1]; + }); + graphData = [graphData.map(function(a,b) { + return a[0]; + }), graphData.map(function(a,b) { + return a[1]; + })]; + // Generate graphs. + var ctx = graph.getContext("2d"); + try { + languageChart.destroy(); + } catch(err) {} + languageChart = new Chart(ctx, { + type: 'bar', + data: { + labels: graphData[0], + datasets: [{ + label: "Phoneme Prevalence", + data: graphData[1], + backgroundColor: 'rgba(244, 121, 34, 0.7)', + borderColor: 'rgba(246, 112, 18, 1)', + borderWidth: 2 + }] + }, + options: { + legend: { + labels: { + fontFamily: "'Open Sans Condensed', sans-serif", + fontSize: 20 + } + }, + scales: { + yAxes: [{ + scaleLabel: { + display: true, + labelString: "Phoneme (%)", + fontFamily: "'Open Sans Condensed', sans-serif", + fontSize: 20, + padding: 4 + }, + ticks: { + fontFamily: "'Open Sans Condensed', sans-serif", + fontSize: 20, + callback: function(value) { + return value + "%"; + } + } + }], + xAxes: [{ + scaleLabel: { + display: true, + labelString: "Percent Prevalence", + fontFamily: "'Open Sans Condensed', sans-serif", + fontSize: 20, + padding: 4 + }, + ticks: { + fontFamily: "'Open Sans Condensed', sans-serif", + fontSize: 20 + } + }], + + } + } + }); info.style.opacity = "1"; dataBox.style.opacity = "1"; + graph.style.opacity = "1"; }, 300); }].concat(["Select language..."].concat(data.languages)); } +function closeEditInput() { + try { + var input = document.getElementById("dataOpen"); + var p = input.parentNode; + var patchData = { + action: 'phoneme_add', + data: { + language_id: language(dropOpStore["langSelect"]).id, + phoneme: p.previousSibling.innerText, + value: input.value + } + }; + $.ajax({ + url: serverURL + '/server', + type: 'PATCH', + dataType: "json", + contentType: 'application/json;charset=UTF-8', + data: JSON.stringify(patchData) + }) + .then( + function success(incoming) { + p.appendChild(document.createTextNode(input.value)); + p.removeChild(input); + }, + function error(e) { + console.log(e); + } + ); + dataOpen = false; + } catch(err) {} +} + +document.addEventListener("click", function(event) { + if(event.target.className !== "dataEdit") { + closeEditInput(); + } +}); + function createDrop() { var dropButtons = document.getElementsByClassName("dropdown"); for (var i = 0; i < dropButtons.length; i++) { diff --git a/SmearcarDB/templates/index.html b/SmearcarDB/templates/index.html index 00ac2a1..3d2cd56 100644 --- a/SmearcarDB/templates/index.html +++ b/SmearcarDB/templates/index.html @@ -27,14 +27,16 @@
-
-

Language

-