You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

375 lines
12 KiB

2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
  1. #!/usr/bin/env python3
  2. import os
  3. from flask import Flask, render_template, session, request, abort, redirect, url_for, jsonify
  4. from flask_sqlalchemy import SQLAlchemy
  5. from sqlalchemy import inspect, and_
  6. from flask_wtf import FlaskForm
  7. import bcrypt
  8. from wtforms_alchemy import model_form_factory
  9. from flask_migrate import Migrate
  10. from uuid import uuid4
  11. import csv
  12. import validate
  13. import secrets
  14. from dotenv import load_dotenv
  15. load_dotenv()
  16. # from datetime import date
  17. app = Flask(__name__)
  18. app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///project.db"
  19. app.secret_key = os.getenv('SECRET_KEY', secrets.token_hex(16))
  20. db: SQLAlchemy = SQLAlchemy()
  21. migrate = Migrate()
  22. db.init_app(app)
  23. migrate.init_app(app, db)
  24. BaseModelForm = model_form_factory(FlaskForm)
  25. class ModelForm(BaseModelForm):
  26. @classmethod
  27. def get_session(cls):
  28. return db.session
  29. class Admin(db.Model):
  30. id = db.Column(db.Integer, primary_key=True)
  31. username = db.Column(db.String, unique=True, nullable=False)
  32. password = db.Column(db.String, nullable=False)
  33. query: db.Query
  34. @classmethod
  35. def generate_password(cls, pw: str):
  36. return bcrypt.hashpw(pw, bcrypt.gensalt(12))
  37. @classmethod
  38. def authenticate(cls, username: str, pw: str):
  39. user = Admin.query.filter_by(username=username).one_or_none()
  40. if user and bcrypt.checkpw(pw, user.password):
  41. session['admin'] = user.username
  42. return user
  43. else:
  44. return None
  45. @classmethod
  46. def exists(cls):
  47. user = Admin.query.one_or_none()
  48. return True if user else False
  49. @classmethod
  50. def authorize(cls):
  51. if "admin" not in session:
  52. return redirect(url_for("admin_login"))
  53. else:
  54. return None
  55. def object_as_dict(obj):
  56. return {c.key: getattr(obj, c.key)
  57. for c in inspect(obj).mapper.column_attrs}
  58. class Chemical(db.Model):
  59. query: db.Query
  60. id = db.Column(db.Integer, primary_key=True)
  61. person_name = db.Column(db.String, nullable=False)
  62. standard_grp = db.Column(db.String, nullable=False)
  63. # all fields after here are included in the database
  64. chemical_db_id = db.Column(db.String)
  65. library = db.Column(db.String)
  66. # important fields
  67. metabolite_name = db.Column(db.String, nullable=False)
  68. formula = db.Column(db.String, nullable=False)
  69. mass = db.Column(db.Float, nullable=False)
  70. pubchem_cid = db.Column(db.Integer)
  71. pubmed_refcount = db.Column(db.Integer)
  72. standard_class = db.Column(db.String)
  73. inchikey = db.Column(db.String, nullable=False)
  74. inchikey14 = db.Column(db.String)
  75. final_mz = db.Column(db.Float, nullable=False)
  76. final_rt = db.Column(db.Float, nullable=False)
  77. final_adduct = db.Column(db.String, nullable=False)
  78. adduct = db.Column(db.String)
  79. detected_adducts = db.Column(db.String)
  80. adduct_calc_mz = db.Column(db.String)
  81. msms_detected = db.Column(db.Boolean, nullable=False)
  82. msms_purity = db.Column(db.Float)
  83. # serialized into datetime.date
  84. createdAt = db.Column(db.Date)
  85. class ChemicalForm(ModelForm):
  86. class Meta:
  87. csrf = False
  88. model = Chemical
  89. # Error Handlers
  90. @app.errorhandler(404)
  91. def handler_404(msg):
  92. return render_template("errors/404.html")
  93. @app.errorhandler(403)
  94. def handler_403(msg):
  95. return render_template("errors/403.html")
  96. # Admin routes
  97. @app.route('/admin')
  98. def admin_root():
  99. if login := Admin.authorize():
  100. return login
  101. return render_template("admin.html", user=session.get("admin"))
  102. @app.route('/admin/create', methods=['GET', 'POST'])
  103. def admin_create():
  104. if Admin.exists():
  105. if login := Admin.authorize():
  106. return login
  107. if request.method == "GET":
  108. return render_template("register.html")
  109. else:
  110. username, pw = request.form.get(
  111. 'username'), request.form.get('password')
  112. if username is None or pw is None:
  113. return render_template("register.html", fail="Invalid Input.")
  114. elif db.session.execute(db.select(Admin).filter_by(username=username)).fetchone():
  115. return render_template("register.html", fail="Username already exists.")
  116. else:
  117. db.session.add(
  118. Admin(username=username, password=Admin.generate_password(pw)))
  119. db.session.commit()
  120. return render_template("register.html", success=True)
  121. @app.route('/admin/login', methods=['GET', 'POST'])
  122. def admin_login():
  123. if request.method == "POST":
  124. username, pw = request.form.get(
  125. 'username', ''), request.form.get('password', '')
  126. if Admin.authenticate(username, pw):
  127. return render_template("login.html", success=True)
  128. else:
  129. return render_template("login.html", fail="Could not authenticate.")
  130. else:
  131. return render_template("login.html")
  132. @app.route('/admin/logout', methods=['GET'])
  133. def admin_logout():
  134. if "admin" in session:
  135. session.pop('admin')
  136. return redirect(url_for('home'))
  137. @app.route("/")
  138. def home():
  139. if Admin.exists():
  140. return render_template("index.html")
  141. else:
  142. return redirect(url_for("admin_create"))
  143. # Routes for CRUD operations on chemicals
  144. @app.route("/chemical/create", methods=['GET', 'POST'])
  145. def chemical_create():
  146. if not session.get('admin'):
  147. abort(403)
  148. if request.method == "POST":
  149. form = ChemicalForm(**request.form)
  150. if form.validate():
  151. new_chemical = Chemical(**form.data)
  152. db.session.add(new_chemical)
  153. db.session.commit()
  154. return render_template("create_chemical.html", form=ChemicalForm(), success=True)
  155. else:
  156. return render_template("create_chemical.html", form=form, invalid=True), 400
  157. else:
  158. form = ChemicalForm()
  159. return render_template("create_chemical.html", form=form)
  160. @app.route("/chemical/<int:id>/update", methods=['GET', 'POST'])
  161. def chemical_update(id: int):
  162. if not session.get('admin'):
  163. abort(403)
  164. current_chemical: Chemical = Chemical.query.filter_by(id=id).one_or_404()
  165. dct = object_as_dict(current_chemical)
  166. if request.method == "POST":
  167. form = ChemicalForm(**request.form)
  168. if form.validate():
  169. # take the row with id and update it.
  170. for k in form.data:
  171. setattr(current_chemical, k, form.data[k])
  172. db.session.commit()
  173. return render_template("create_chemical.html", form=form, success=True, id=id)
  174. else:
  175. form = ChemicalForm(**dct)
  176. return render_template("create_chemical.html", form=form, invalid=True, id=id), 400
  177. else:
  178. form = ChemicalForm(**dct)
  179. return render_template("create_chemical.html", form=form, id=id)
  180. @app.route("/chemical/<int:id>/delete")
  181. def chemical_delete(id: int):
  182. if not session.get('admin'):
  183. abort(403)
  184. current_chemical: Chemical = Chemical.query.filter_by(id=id).one_or_404()
  185. db.session.delete(current_chemical)
  186. db.session.commit()
  187. return render_template("delete_chemical.html", id=id)
  188. @app.route("/chemical/<int:id>/view")
  189. def chemical_view(id: int):
  190. current_chemical: Chemical = Chemical.query.filter_by(id=id).one_or_404()
  191. dct = object_as_dict(current_chemical)
  192. return render_template("view_chemical.html", id=id, chemical=dct)
  193. @app.route("/chemical/all")
  194. def chemical_all():
  195. if not session.get('admin'):
  196. abort(403)
  197. result: list[Chemical] = Chemical.query.all()
  198. data = []
  199. for x in result:
  200. data.append({c.name: getattr(x, c.name) for c in x.__table__.columns})
  201. return jsonify(data)
  202. @app.route("/chemical/search", methods=["POST"])
  203. def search_api():
  204. query = request.json
  205. if query is None:
  206. return jsonify([])
  207. for field in query:
  208. query[field] = float(query[field])
  209. mz_min, mz_max = query.get('mz_min'), query.get('mz_max')
  210. rt_min, rt_max = query.get('rt_min'), query.get('rt_max')
  211. year_max, month_max, day_max = int(query.get(
  212. 'year_max')), int(query.get('month_max')), int(query.get('day_max'))
  213. try:
  214. mz_filter = and_(mz_max > Chemical.final_mz,
  215. Chemical.final_mz > mz_min)
  216. rt_filter = and_(rt_max > Chemical.final_rt,
  217. Chemical.final_rt > rt_min)
  218. # date_filter = date(year_max, month_max, day_max) >= Chemical.createdAt
  219. except ValueError as e:
  220. return jsonify({"error": str(e)}), 400
  221. result = Chemical.query.filter(
  222. and_(mz_filter, rt_filter)
  223. ).limit(20).all()
  224. data = []
  225. for x in result:
  226. data.append({"url": url_for("chemical_view", id=x.id),
  227. "name": x.metabolite_name, "mz": x.final_mz, "rt": x.final_rt})
  228. return jsonify(data)
  229. # Utilities for doing add and search operations in batch
  230. # no file over 3MB is allowed.
  231. app.config['MAX_CONTENT_LENGTH'] = 3 * 1000 * 1000
  232. @app.route("/chemical/batchadd", methods=["GET", "POST"])
  233. def batch_add_request():
  234. if not session.get('admin'):
  235. abort(403)
  236. if request.method == "POST":
  237. if "input" not in request.files or request.files["input"].filename == '':
  238. return render_template("batchadd.html", invalid="Blank file included")
  239. # save the file to RAM
  240. file = request.files["input"]
  241. os.makedirs("/tmp/walkerdb", exist_ok=True)
  242. filename = os.path.join("/tmp/walkerdb", str(uuid4()))
  243. file.save(filename)
  244. # perform cleanup regardless of what happens.
  245. def cleanup(): return os.remove(filename)
  246. # read it as a csv
  247. with open(filename, "r") as csvfile:
  248. reader = csv.DictReader(csvfile, delimiter="\t")
  249. results, error = validate.validate_insertion_csv_fields(reader)
  250. if error:
  251. cleanup()
  252. return render_template("batchadd.html", invalid=error)
  253. else:
  254. chemicals = [Chemical(**result) for result in results]
  255. db.session.add_all(chemicals)
  256. db.session.commit()
  257. cleanup()
  258. return render_template("batchadd.html", success=True)
  259. else:
  260. return render_template("batchadd.html")
  261. @app.route("/chemical/batch", methods=["GET", "POST"])
  262. def batch_query_request():
  263. if not session.get('admin'):
  264. abort(403)
  265. if request.method == "POST":
  266. if "input" not in request.files or request.files["input"].filename == '':
  267. return render_template("batchadd.html", invalid="Blank file included")
  268. # save the file to RAM
  269. file = request.files["input"]
  270. os.makedirs("/tmp/walkerdb", exist_ok=True)
  271. filename = os.path.join("/tmp/walkerdb", str(uuid4()))
  272. file.save(filename)
  273. # perform cleanup regardless of what happens.
  274. def cleanup(): return os.remove(filename)
  275. # read it as a csv
  276. with open(filename, "r") as csvfile:
  277. reader = csv.DictReader(csvfile, delimiter="\t")
  278. queries, error = validate.validate_query_csv_fields(reader)
  279. if error:
  280. cleanup()
  281. return render_template("batchquery.html", invalid=error)
  282. else:
  283. # generate the queries here.
  284. data = []
  285. for query in queries:
  286. mz_filter = and_(query["mz_max"] > Chemical.final_mz,
  287. Chemical.final_mz > query["mz_min"])
  288. rt_filter = and_(query["rt_max"] > Chemical.final_rt,
  289. Chemical.final_rt > query["rt_min"])
  290. # date_filter = query["date"] >= Chemical.createdAt
  291. result = Chemical.query.filter(
  292. and_(mz_filter, rt_filter)
  293. ).limit(5).all()
  294. hits = []
  295. for x in result:
  296. hits.append({"url": url_for("chemical_view", id=x.id),
  297. "name": x.metabolite_name, "mz": x.final_mz, "rt": x.final_rt})
  298. data.append(dict(
  299. query=query,
  300. hits=hits,
  301. ))
  302. cleanup()
  303. return render_template("batchquery.html", success=True, data=data)
  304. return render_template("batchquery.html")
  305. @app.route("/search")
  306. def search():
  307. return render_template("search.html")
  308. if __name__ == "__main__":
  309. with app.app_context():
  310. db.create_all()
  311. app.run(debug=True)