views.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. import os
  2. import io
  3. import re
  4. from functools import wraps
  5. from nova import app, db, login_manager, fs, logic, memtar, tasks, models
  6. from nova.models import (User, Collection, Dataset, SampleScan, Genus, Family,
  7. Order, Access, Notification, Process)
  8. from flask import (Response, render_template, request, flash, redirect,
  9. url_for, jsonify, send_from_directory)
  10. from flask_login import login_user, logout_user, current_user
  11. from flask_wtf import Form
  12. from flask_sqlalchemy import Pagination
  13. from wtforms import StringField, BooleanField
  14. from wtforms.validators import DataRequired
  15. def login_required(admin=False):
  16. def wrapper(func):
  17. @wraps(func)
  18. def decorated_view(*args, **kwargs):
  19. if not current_user.is_authenticated:
  20. return login_manager.unauthorized()
  21. if admin and not current_user.is_admin:
  22. return login_manager.unauthorized()
  23. return func(*args, **kwargs)
  24. return decorated_view
  25. return wrapper
  26. class LoginForm(Form):
  27. name = StringField('name', validators=[DataRequired()])
  28. password = StringField('password', validators=[DataRequired()])
  29. class SignupForm(Form):
  30. name = StringField('name', validators=[DataRequired()])
  31. fullname = StringField('fullname', validators=[DataRequired()])
  32. email = StringField('email', validators=[DataRequired()])
  33. password = StringField('password', validators=[DataRequired()])
  34. is_admin = BooleanField('is_admin')
  35. class CreateForm(Form):
  36. name = StringField('name', validators=[DataRequired()])
  37. description = StringField('description')
  38. class CreateCollectionForm(Form):
  39. name = StringField('name', validators=[DataRequired()])
  40. description = StringField('description')
  41. class RunCommandForm(Form):
  42. name = StringField('name', validators=[DataRequired()])
  43. command_line = StringField('command-line', validators=[DataRequired()])
  44. input = StringField('input', validators=[DataRequired()])
  45. output = StringField('output', validators=[DataRequired()])
  46. class SearchForm(Form):
  47. query = StringField('query', validators=[DataRequired()])
  48. class ImportForm(Form):
  49. path_template = StringField('template', validators=[DataRequired()])
  50. class InvalidUsage(Exception):
  51. status_code = 400
  52. def __init__(self, message, status_code=None, payload=None):
  53. Exception.__init__(self)
  54. self.message = message
  55. self.payload = payload
  56. self.status_code = status_code or self.status_code
  57. def to_dict(self):
  58. rv = dict(self.payload or ())
  59. rv['message'] = self.message
  60. return rv
  61. @app.errorhandler(InvalidUsage)
  62. def handle_invalid_usage(error):
  63. response = jsonify(error.to_dict())
  64. response.status_code = error.status_code
  65. return response
  66. @login_manager.user_loader
  67. def load_user(user_id):
  68. return db.session.query(User).filter(User.name == user_id).first()
  69. @app.route('/login', methods=['GET', 'POST'])
  70. def login():
  71. form = LoginForm()
  72. if form.validate_on_submit():
  73. user = db.session.query(User).filter(User.name == form.name.data).first()
  74. if user.password == form.password.data:
  75. login_user(user)
  76. flash('Logged in successfully')
  77. return redirect(url_for('index'))
  78. else:
  79. return render_template('user/login.html', form=form, failed=True), 401
  80. return render_template('user/login.html', form=form, failed=False)
  81. @app.route('/logout')
  82. @login_required(admin=False)
  83. def logout():
  84. logout_user()
  85. return redirect(url_for('index'))
  86. @app.route('/')
  87. @app.route('/<int:page>')
  88. @login_required(admin=False)
  89. def index(page=1):
  90. if current_user.first_time:
  91. current_user.first_time = False
  92. db.session.commit()
  93. return render_template('index/welcome.html', user=current_user)
  94. pagination = Collection.query.paginate(page=page, per_page=16)
  95. notifications = Notification.query.filter(Notification.user == current_user).all()
  96. for notification in notifications:
  97. db.session.delete(notification)
  98. shared = db.session.query(Dataset, Access).\
  99. filter(Access.user == current_user).\
  100. filter(Access.dataset_id == Dataset.id).\
  101. filter(Access.owner == False).\
  102. filter(Access.seen == False).\
  103. all()
  104. shared, shared_accesses = zip(*shared) if shared else ([], [])
  105. for access in shared_accesses:
  106. access.seen = True
  107. db.session.commit()
  108. return render_template('index/index.html', notifications=notifications, pagination=pagination)
  109. @app.route('/settings')
  110. @login_required(admin=True)
  111. def admin():
  112. users = db.session.query(User).all()
  113. return render_template('user/admin.html', users=users)
  114. @app.route('/token/generate')
  115. @login_required(admin=False)
  116. def generate_token():
  117. current_user.generate_token()
  118. return redirect('user/{}'.format(current_user.name))
  119. @app.route('/token/revoke')
  120. @login_required(admin=False)
  121. def revoke_token():
  122. current_user.token = None
  123. db.session.commit()
  124. return redirect('user/{}'.format(current_user.name))
  125. @app.route('/signup', methods=['GET', 'POST'])
  126. @login_required(admin=True)
  127. def signup():
  128. form = SignupForm()
  129. if form.validate_on_submit():
  130. user = User(name=form.name.data, fullname=form.fullname.data,
  131. email=form.email.data, password=form.password.data,
  132. is_admin=form.is_admin.data)
  133. db.session.add(user)
  134. db.session.commit()
  135. return redirect(url_for('admin'))
  136. return render_template('user/signup.html', form=form)
  137. @app.route('/user/<name>')
  138. @app.route('/user/<name>/<int:page>')
  139. @login_required(admin=False)
  140. def profile(name, page=1):
  141. user = db.session.query(User).filter(User.name == name).first()
  142. pagination = Collection.query.\
  143. filter(Collection.user == user).\
  144. paginate(page=page, per_page=8)
  145. return render_template('user/profile.html', user=user, pagination=pagination)
  146. @app.route('/create/<collection_name>', methods=['GET', 'POST'])
  147. @login_required(admin=False)
  148. def create_dataset(collection_name):
  149. form = CreateForm()
  150. collection = Collection.query.\
  151. filter(Collection.name == collection_name).\
  152. filter(Collection.user == current_user).\
  153. first()
  154. if form.validate_on_submit():
  155. logic.create_dataset(models.SampleScan, form.name.data, current_user,
  156. collection, description=form.description.data)
  157. return redirect(url_for('index'))
  158. return render_template('dataset/create.html', form=form, collection=collection)
  159. @app.route('/foo', methods=['GET', 'POST'])
  160. @login_required(admin=False)
  161. def create_collection():
  162. form = CreateCollectionForm()
  163. if form.validate_on_submit():
  164. logic.create_collection(form.name.data, current_user, form.description.data)
  165. return redirect(url_for('index'))
  166. return render_template('collection/create.html', form=form)
  167. @app.route('/import', methods=['POST'])
  168. @login_required(admin=False)
  169. def import_submission():
  170. form = ImportForm()
  171. # FIXME: again this is not working
  172. # if form.validate_on_submit():
  173. # pass
  174. template = request.form['template']
  175. # XXX: incredible danger zone!
  176. for entry in (e for e in os.listdir(template) if os.path.isdir(os.path.join(template, e))):
  177. path = os.path.join(template, entry)
  178. app.logger.info("Importing {}".format(entry))
  179. logic.import_sample_scan(entry, current_user, path)
  180. return redirect(url_for('index'))
  181. @app.route('/update', methods=['POST'])
  182. @login_required(admin=False)
  183. def update():
  184. import csv
  185. # XXX: more danger zone!
  186. genuses = {x.name: x for x in db.session.query(Genus).all()}
  187. families = {x.name: x for x in db.session.query(Family).all()}
  188. orders = {x.name: x for x in db.session.query(Order).all()}
  189. scans = {x.name: x for x in db.session.query(SampleScan).all()}
  190. with open(request.form['csv'], 'rb') as f:
  191. reader = csv.reader(f)
  192. for name, _, genus, family, order in reader:
  193. if name in scans:
  194. scan = scans[name]
  195. if genus and not scan.genus:
  196. if genus in genuses:
  197. scan.genus = genuses[genus]
  198. else:
  199. g = Genus(name=genus)
  200. db.session.add(g)
  201. genuses[genus] = g
  202. scan.genus = g
  203. if family and not scan.family:
  204. if family in families:
  205. scan.family = families[family]
  206. else:
  207. f = Family(name=family)
  208. db.session.add(f)
  209. families[family] = f
  210. scan.family = f
  211. if order and not scan.order:
  212. if order in orders:
  213. scan.order = orders[order]
  214. else:
  215. o = Order(name=order)
  216. db.session.add(o)
  217. orders[order] = o
  218. scan.order = o
  219. db.session.commit()
  220. return redirect(url_for('index'))
  221. @app.route('/close/<int:dataset_id>')
  222. @login_required(admin=False)
  223. def close(dataset_id):
  224. dataset, access = db.session.query(Dataset, Access).\
  225. filter(Dataset.id == dataset_id).\
  226. filter(Access.user == current_user).\
  227. filter(Access.dataset_id == dataset_id).first()
  228. if not access.owner:
  229. return redirect(url_for('index'))
  230. dataset.closed = True
  231. db.session.commit()
  232. return redirect(url_for('index'))
  233. @app.route('/open/<int:dataset_id>')
  234. @login_required(admin=False)
  235. def open_dataset(dataset_id):
  236. dataset, access = db.session.query(Dataset, Access).\
  237. filter(Dataset.id == dataset_id).\
  238. filter(Access.user == current_user).\
  239. filter(Access.dataset_id == dataset_id).first()
  240. if not access.owner:
  241. return redirect(url_for('index'))
  242. dataset.closed = False
  243. db.session.commit()
  244. return redirect(url_for('index'))
  245. @app.route('/search', methods=['GET', 'POST'])
  246. @app.route('/search/<int:page>', methods=['GET', 'POST'])
  247. @login_required(admin=False)
  248. def search(page=1):
  249. # form = SearchForm()
  250. # XXX: for some reason this does not validate?
  251. # if form.validate_on_submit():
  252. # pass
  253. if request.method == 'POST':
  254. query = request.form['query']
  255. datasets = Dataset.query.whoosh_search(query).all()
  256. users = User.query.whoosh_search(query).all()
  257. # FIXME: this is a slow abomination, fix ASAP
  258. accesses = [a for a in db.session.query(Access).all()
  259. if a.dataset in datasets or a.user in users]
  260. return render_template('index/index.html', accesses=accesses)
  261. samples = Access.query.join(SampleScan)
  262. search_terms = {x: request.args[x] for x in ('genus', 'family', 'order') if x in request.args}
  263. # XXX: this is lame, please abstract somehow ...
  264. if 'genus' in search_terms:
  265. samples = samples.filter(SampleScan.genus_id == search_terms['genus'])
  266. if 'family' in search_terms:
  267. samples = samples.filter(SampleScan.family_id == search_terms['family'])
  268. if 'order' in search_terms:
  269. samples = samples.filter(SampleScan.order_id == search_terms['order'])
  270. pagination = samples.paginate(page=page, per_page=16)
  271. return render_template('index/search.html', pagination=pagination, search_terms=search_terms)
  272. @app.route('/share/<int:dataset_id>')
  273. @app.route('/share/<int:dataset_id>/<int:user_id>')
  274. @login_required(admin=False)
  275. def share(dataset_id, user_id=None):
  276. if not user_id:
  277. users = db.session.query(User).filter(User.id != current_user.id).all()
  278. return render_template('dataset/share.html', users=users, dataset_id=dataset_id)
  279. user = db.session.query(User).filter(User.id == user_id).first()
  280. dataset, access = db.session.query(Dataset, Access).\
  281. filter(Access.dataset_id == dataset_id).\
  282. filter(Access.owner == True).\
  283. filter(Dataset.id == dataset_id).\
  284. first()
  285. # Do not share again with the owner of the dataset
  286. if access.user != user:
  287. access = Access(user=user, dataset=dataset, owner=False, writable=False)
  288. db.session.add(access)
  289. db.session.commit()
  290. return redirect(url_for('index'))
  291. @app.route('/process/<int:dataset_id>', methods=['POST'])
  292. @login_required(admin=False)
  293. def process(dataset_id):
  294. parent = Dataset.query.filter(Dataset.id == dataset_id).first()
  295. child = logic.create_dataset(models.Volume, request.form['name'], current_user, parent.collection, slices=request.form['outname'])
  296. flats = request.form['flats']
  297. darks = request.form['darks']
  298. projections = request.form['projections']
  299. output = request.form['outname']
  300. result = tasks.reconstruct.delay(current_user.token, child.id, parent.id, flats, darks, projections, output)
  301. db.session.add(models.Reconstruction(source=parent, destination=child, task_uuid=result.id,
  302. flats=flats, darks=darks, projections=projections, output=output))
  303. db.session.commit()
  304. return redirect(url_for('index'))
  305. @app.route('/user/<name>/<collection_name>')
  306. @login_required(admin=False)
  307. def show_collection(name, collection_name):
  308. collection = Collection.query.filter(Collection.name == collection_name).first()
  309. if len(collection.datasets) != 1:
  310. return render_template('collection/list.html', collection=collection)
  311. dataset = collection.datasets[0]
  312. return redirect(url_for('show_dataset', name=name, collection_name=collection_name, dataset_name=dataset.name))
  313. @app.route('/user/<name>/<collection_name>/<dataset_name>')
  314. @app.route('/user/<name>/<collection_name>/<dataset_name>/<path:path>')
  315. @login_required(admin=False)
  316. def show_dataset(name, collection_name, dataset_name, path=''):
  317. collection = Collection.query.\
  318. filter(Collection.name == collection_name).first()
  319. dataset = Dataset.query.join(Collection).\
  320. filter(Collection.name == collection_name).\
  321. filter(Dataset.name == dataset_name).first()
  322. if path:
  323. filepath = os.path.join(dataset.path, path)
  324. if os.path.isfile(filepath):
  325. filename = os.path.basename(filepath)
  326. directory = os.path.dirname(filepath)
  327. return send_from_directory(directory, filename)
  328. # FIXME: check access rights
  329. # FIXME: scream if no dataset found
  330. parents = Dataset.query.join(Process.source).\
  331. filter(Process.destination_id == dataset.id).all()
  332. children = Dataset.query.join(Process.destination).\
  333. filter(Process.source_id == dataset.id).all()
  334. print parents
  335. parts = path.split('/')
  336. subpaths = []
  337. list_files = app.config['NOVA_ENABLE_FILE_LISTING']
  338. dirs = fs.get_dirs(dataset, path) if list_files else None
  339. files = sorted(fs.get_files(dataset, path)) if list_files else None
  340. if list_files:
  341. for part in parts:
  342. if subpaths:
  343. subpaths.append((part, os.path.join(subpaths[-1][1], part)))
  344. else:
  345. subpaths.append((part, part))
  346. params = dict(collection=collection, dataset=dataset,
  347. parents=parents, children=children,
  348. path=path, list_files=list_files,
  349. subpaths=subpaths, files=files, dirs=dirs, origin=[])
  350. return render_template('dataset/detail.html', **params)
  351. @app.route('/delete/<int:dataset_id>')
  352. @login_required(admin=False)
  353. def delete(dataset_id=None):
  354. dataset, access = db.session.query(Dataset, Access).\
  355. filter(Dataset.id == dataset_id).\
  356. filter(Access.user == current_user).\
  357. filter(Access.dataset_id == dataset_id).first()
  358. if not access.owner:
  359. return redirect(url_for('index'))
  360. shared_with = db.session.query(Access).\
  361. filter(Access.user != current_user).\
  362. filter(Access.dataset_id == dataset_id).all()
  363. for access in shared_with:
  364. db.session.add(Notification(user=access.user, message="{} has been deleted.".format(dataset.name)))
  365. db.session.commit()
  366. if dataset:
  367. path = fs.path_of(dataset)
  368. db.session.delete(dataset)
  369. db.session.commit()
  370. tasks.rmtree.delay(path)
  371. return redirect(url_for('index'))
  372. @app.route('/upload/<int:dataset_id>', methods=['POST'])
  373. def upload(dataset_id):
  374. user = logic.check_token(request.args.get('token'))
  375. dataset = db.session.query(Dataset).\
  376. filter(Access.user == user).\
  377. filter(Access.dataset_id == dataset_id).\
  378. filter(Dataset.id == dataset_id).first()
  379. if dataset is None:
  380. raise InvalidUsage('Dataset not found', status_code=404)
  381. if dataset.closed:
  382. return 'Dataset closed', 423
  383. f = io.BytesIO(request.data)
  384. memtar.extract_tar(f, fs.path_of(dataset))
  385. return ''
  386. @app.route('/clone/<int:dataset_id>')
  387. def clone(dataset_id):
  388. user = logic.check_token(request.args.get('token'))
  389. dataset = db.session.query(Dataset).\
  390. filter(Access.user == user).\
  391. filter(Access.dataset_id == dataset_id).\
  392. filter(Dataset.id == dataset_id).first()
  393. fileobj = memtar.create_tar(fs.path_of(dataset))
  394. fileobj.seek(0)
  395. def generate():
  396. while True:
  397. data = fileobj.read(4096)
  398. if not data:
  399. break
  400. yield data
  401. return Response(generate(), mimetype='application/gzip')