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.

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