Attention une mise à jour du service Gitlab va être effectuée le mardi 14 décembre entre 13h30 et 14h00. Cette mise à jour va générer une interruption du service dont nous ne maîtrisons pas complètement la durée mais qui ne devrait pas excéder quelques minutes.

default.py 23.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
# -*- coding: utf-8 -*-
# this file is released under public domain and you can use without limitations

#########################################################################
## This is a sample controller
## - index is the default action of any application
## - user is required for authentication and authorization
## - download is for downloading files uploaded in the db (does streaming)
## - call exposes all registered services (none by default)
#########################################################################

12
import defs
13
import vidjil_utils
14
import logging
15
from controller_utils import error_message
16

17
import gluon.contrib.simplejson, time, datetime
18
if request.env.http_origin:
19
    response.headers['Access-Control-Allow-Origin'] = request.env.http_origin
20 21
    response.headers['Access-Control-Allow-Credentials'] = 'true'
    response.headers['Access-Control-Max-Age'] = 86400
22

23
#########################################################################
24
##return the default index page for vidjil (redirect to the browser)
25 26 27
def index():
    return dict(message=T('hello world'))

28 29
#########################################################################
##return the view default/help.html
Marc Duez's avatar
Marc Duez committed
30 31 32
def help():
    return dict(message=T('help i\'m lost'))

33 34 35 36 37
#########################################################################
## default home page
def home():
    res = {"redirect" : URL('admin' if auth.is_admin() else 'patient', 'index', scheme=True, host=True)}
    return gluon.contrib.simplejson.dumps(res, separators=(',',':'))
38 39 40 41 42 43 44 45

def logger():
    '''Log to the server'''
    res = {"success" : "false",
           "message" : "/client/: %s" % request.vars['msg']}

    try:
        lvl = int(request.vars['lvl'])
46
    except:
47 48 49
        lvl = logging.INFO
    log.log(lvl, res)

Marc Duez's avatar
Marc Duez committed
50
def init_db(force=False):
51
    if (force) or (db(db.auth_user.id > 0).count() == 0) : 
Marc Duez's avatar
Marc Duez committed
52 53 54
        for table in db :
            table.truncate()
        
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
        id_first_user=""

        ## création du premier user
        id_first_user=db.auth_user.insert(
            password = db.auth_user.password.validate('1234')[0],
            email = 'plop@plop.com',
            first_name = 'System',
            last_name = 'Administrator'
        )

        ## création des groupes de base
        id_admin_group=db.auth_group.insert(role='admin')
        id_sa_group=db.auth_group.insert(role='user_1')
        db.auth_group.insert(role="public")

        db.auth_membership.insert(user_id=id_first_user, group_id=id_admin_group)
        db.auth_membership.insert(user_id=id_first_user, group_id=id_sa_group)

        ##création des configs de base
        id_config_TRG = db.config.insert(
            name = 'TRG',
            command = '-c clones -z 100 -R 1 -r 1 -G germline/TRG ',
            info = 'default trg config'
        )

        id_config_IGH = db.config.insert(
            name = 'IGH',
            command = '-c clones -d -z 100 -R 1 -r 1 -G germline/IGH ',
            info = 'default igh config'
        )

        ## permission
        ## system admin have admin/read/create rights on all patients, groups and configs
        auth.add_permission(id_admin_group, 'admin', db.patient, 0)
        auth.add_permission(id_admin_group, 'admin', db.auth_group, 0)
        auth.add_permission(id_admin_group, 'admin', db.config, 0)
        auth.add_permission(id_admin_group, 'read', db.patient, 0)
        auth.add_permission(id_admin_group, 'read', db.auth_group, 0)
        auth.add_permission(id_admin_group, 'read', db.config, 0)
        auth.add_permission(id_admin_group, 'create', db.patient, 0)
        auth.add_permission(id_admin_group, 'create', db.auth_group, 0)
        auth.add_permission(id_admin_group, 'create', db.config, 0)
Marc Duez's avatar
Marc Duez committed
97
        auth.add_permission(id_admin_group, 'impersonate', db.auth_user, 0)
98 99 100

def init_from_csv():
    if db(db.auth_user.id > 0).count() == 0:
101 102 103 104
        res = {"success" : "true", "message" : "Importing " + defs.DB_BACKUP_FILE}
        log.info(res)

        try:
105
            db.import_from_csv_file(open(defs.DB_BACKUP_FILE, 'rb'))
106 107
            # db.scheduler_task.truncate()
            # db.scheduler_run.truncate()
108 109 110 111 112 113 114
        except Exception as e:
            res = {"success": "false", "message": "!" + str(e)}
            log.error(res)
            raise

        res = {"success" : "true", "message" : "coucou"}
        log.info(res)
115

116
#########################################################################
117
## add a scheduller task to run vidjil on a specific sequence file
118
# need sequence_file_id, config_id
119
# need patient admin permission
120
def run_request():
121 122
    error = ""

123
    ##TODO check
124 125 126 127
    if not "sequence_file_id" in request.vars :
        error += "id sequence file needed, "
    if not "config_id" in request.vars:
        error += "id config needed, "
Mathieu Giraud's avatar
Mathieu Giraud committed
128 129 130
        id_config = None
    else:
        id_config = request.vars["config_id"]
131
    if not auth.can_process_file():
132
        error += "permission needed"
133

134
    id_sample_set = request.vars["sample_set_id"]
135

136 137 138 139 140
    if "grep_reads" in request.vars:
        grep_reads = request.vars["grep_reads"]
    else:
        grep_reads = None

141 142
    if not auth.can_modify_sample_set(id_sample_set) :
        error += "you do not have permission to launch process for this sample_set ("+str(id_sample_set)+"), "
143

Mathieu Giraud's avatar
Mathieu Giraud committed
144
    if id_config:
145 146
        if not auth.can_use_config(id_config) :
            error += "you do not have permission to launch process for this config ("+str(id_config)+"), "
147

148
    if error == "" :
149
        res = schedule_run(request.vars["sequence_file_id"], id_sample_set, id_config, grep_reads)
150 151 152
        return gluon.contrib.simplejson.dumps(res, separators=(',',':'))

    else :
153
        res = {"success" : "false",
154
               "message" : "default/run_request : " + error}
155
        log.error(res)
156
        return gluon.contrib.simplejson.dumps(res, separators=(',',':'))
157

158 159 160 161 162 163 164 165 166 167 168 169
def run_contamination():
    task = scheduler.queue_task('compute_contamination', pvars=dict(sequence_file_id=request.vars["sequence_file_id"],
                                                                    results_file_id=request.vars["results_file_id"],
                                                                    config_id=request.vars["config_id"]),
             repeats = 1, timeout = defs.TASK_TIMEOUT,immediate=True)
    
    res = {"success" : "true",
           "processId" : task.id}
    log.error(res)
    return gluon.contrib.simplejson.dumps(res, separators=(',',':'))


170

171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
def checkProcess():
    task = db.scheduler_task[request.vars["processId"]]
    
    if task.status == "COMPLETED" :
        run = db( db.scheduler_run.task_id == task.id ).select()[0]
    
        res = {"success" : "true",
               "status" : task.status,
               "data" : run.run_result,
               "processId" : task.id}
    else :
        res = {"success" : "true",
               "status" : task.status,
               "processId" : task.id}
        
    log.error(res)
    return gluon.contrib.simplejson.dumps(res, separators=(',',':'))

189

190 191
#########################################################################
## return .data file
192
# need sample_set/patient, config
193
# need sample_set admin or read permission
194
def get_data():
195
    from subprocess import Popen, PIPE, STDOUT
196
    if not auth.user :
197 198
        res = {"redirect" : URL('default', 'user', args='login', scheme=True, host=True,
                            vars=dict(_next=URL('default', 'get_data', scheme=True, host=True,
199
                                                vars=dict(sample_set_id = request.vars["sample_set_id"],
200 201 202 203
                                                          config =request.vars["config"]))
                                      )
                            )}
        return gluon.contrib.simplejson.dumps(res, separators=(',',':'))
204

205
    error = ""
206 207 208
    
    if "patient" in request.vars :
        request.vars["sample_set_id"] = db.patient[request.vars["patient"]].sample_set_id
209

210 211 212
    if "run" in request.vars :
        request.vars["sample_set_id"] = db.run[request.vars["run"]].sample_set_id
    
213 214 215 216 217
    if not "sample_set_id" in request.vars :
        error += "id sampleset file needed, "
    else : 
        if not auth.can_view_sample_set(request.vars["sample_set_id"]):
            error += "you do not have permission to consult this sample_set ("+str(request.vars["sample_set_id"])+")"
218
    if not "config" in request.vars:
219
        error += "id config needed, "
220

221 222 223 224

    sample_set = db.sample_set[request.vars["sample_set_id"]]
    
    query = db( ( db.fused_file.sample_set_id == request.vars["sample_set_id"])
225
               & ( db.fused_file.config_id == request.vars["config"] )
226
               ).select(db.fused_file.ALL)
227
    for row in query :
228
        fused_file = defs.DIR_RESULTS+'/'+row.fused_file
229
        sequence_file_list = row.sequence_file_list
230

231 232
    if not 'fused_file' in locals():
        error += "file not found"
233

234
    if error == "" :
235

236
        f = open(fused_file, "r")
237
        data = gluon.contrib.simplejson.loads(f.read())
238
        f.close()
239
        
240 241
        patient_name = ""
        run_name = ""
242
        config_name = db.config[request.vars["config"]].name
243
        command = db.config[request.vars["config"]].command
244
        
245 246 247 248 249
        if (sample_set.sample_type == "patient") :
            for row in db( db.patient.sample_set_id == request.vars["sample_set_id"] ).select() :
                patient_name = vidjil_utils.anon_ids(row.id)
                data["dataFileName"] = patient_name + " (" + config_name + ")"
                data["info"] = db.patient[row.id].info
250 251
                data["patient_id"] = row.id
                data["patient_name"] = patient_name
252 253

        if (sample_set.sample_type == "run") :
254
            for row in db( db.run.sample_set_id == request.vars["sample_set_id"] ).select() :
255 256
                run_name = db.run[row.id].name
                data["dataFileName"] = run_name + " (" + config_name + ")"
257 258 259
                data["info"] = db.run[row.id].info
                data["run_id"] = row.id
                data["run_name"] = run_name
260

261
        ## récupération des infos stockées sur la base de données
262 263
        query = db(  ( db.sample_set.id == request.vars["sample_set_id"] )
                   & ( db.sample_set.id == db.sample_set_membership.sample_set_id )
264
                   & ( db.sequence_file.id == db.sample_set_membership.sequence_file_id)
265 266
                   & ( db.results_file.sequence_file_id == db.sequence_file.id )
                   & ( db.results_file.config_id == request.vars["config"]  )
267 268 269 270 271 272 273 274 275
                   ).select(db.sequence_file.ALL,db.results_file.ALL, db.sample_set.id, orderby=db.sequence_file.id|~db.results_file.run_date)

        query2 = []
        sequence_file_id = 0
        for row in query : 
            if row.sequence_file.id != sequence_file_id :
                query2.append(row)
                sequence_file_id = row.sequence_file.id
        
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
        data["sample_set_id"] = sample_set.id

        data["config_name"] = config_name
        data["samples"]["info"] = []
        data["samples"]["timestamp"] = []
        data["samples"]["sequence_file_id"] = []
        data["samples"]["results_file_id"] = []
        data["samples"]["config_id"] = []
        data["samples"]["names"] = []
        data["samples"]["db_key"] = []
        data["samples"]["ids"] = []
        for i in range(len(data["samples"]["original_names"])) :
            row = query2[i]
            data["samples"]["original_names"][i] = data["samples"]["original_names"][i].split('/')[-1]
            data["samples"]["info"].append(row.sequence_file.info)
            data["samples"]["timestamp"].append(str(row.sequence_file.sampling_date))
            data["samples"]["sequence_file_id"].append(row.sequence_file.id)
            data["samples"]["results_file_id"].append(row.results_file.id)
            data["samples"]["config_id"].append(request.vars['config'])
            data["samples"]["names"].append(row.sequence_file.filename.split('.')[0])
            data["samples"]["db_key"].append('')
            data["samples"]["ids"].append(row.sequence_file.id)
            data["samples"]["commandline"].append(command)

300
        log.debug("get_data (%s) c%s -> %s" % (request.vars["sample_set_id"], request.vars["config"], fused_file))
301
        return gluon.contrib.simplejson.dumps(data, separators=(',',':'))
302

303 304
    else :
        res = {"success" : "false",
305
               "message" : "get_data (%s) c%s : %s " % (request.vars["sample_set_id"], request.vars["config"], error)}
306
        log.error(res)
307
        return gluon.contrib.simplejson.dumps(res, separators=(',',':'))
308 309 310 311 312 313 314 315 316
    
#########################################################################
def get_custom_data():
    from subprocess import Popen, PIPE, STDOUT
    if not auth.user :
        res = {"redirect" : URL('default', 'user', args='login', scheme=True, host=True)} #TODO _next
        return gluon.contrib.simplejson.dumps(res, separators=(',',':'))

    error = ""
317

318 319 320
    if not "custom" in request.vars :
        error += "no file selected, "
    else:
321 322 323 324 325 326
        if type(request.vars['custom']) is not list or len(request.vars['custom']) < 2:
            error += "you must select several files."
        else:
            for id in request.vars["custom"] :
                log.debug("id = '%s'" % str(id))
                sequence_file_id = db.results_file[id].sequence_file_id
327 328 329
                patient_id = db((db.sample_set_membership.sequence_file_id == sequence_file_id)
                            & (db.patient.sample_set_id == db.sample_set_membership.sample_set_id)
                ).select(db.patient.id).first()
330 331
                if not auth.can_view_patient(patient_id):
                    error += "you do not have permission to consult this patient ("+str(patient_id)+")"
332 333
            
    if error == "" :
334 335 336 337
        try:
            data = custom_fuse(request.vars["custom"])
        except IOError, error:
            return error_message(str(error))
338
        
339 340
        data["dataFileName"] = "Compare patients"
        data["info"] = "Compare patients"
341 342 343
        data["samples"]["original_names"] = []
        data["samples"]["timestamp"] = []
        data["samples"]["info"] = []
344
        data["samples"]["commandline"] = []
345 346 347
        
        for id in request.vars["custom"] :
            sequence_file_id = db.results_file[id].sequence_file_id
348 349 350 351
            patient_id = db((db.sequence_file.id == sequence_file_id)
                            & (db.sample_set_membership.sequence_file_id == db.sequence_file.id)
                            & (db.sample_set.id == db.sample_set_membership.sample_set_id)
                            & (db.patient.sample_set_id == db.sample_set.id)
352
                            ).select(db.patient.id).first().id
353

354
            config_id = db.results_file[id].config_id
355
            patient_name = vidjil_utils.anon_ids(patient_id)
356
            filename = db.sequence_file[sequence_file_id].filename
357
            data["samples"]["original_names"].append(patient_name + "_" + filename)
358 359
            data["samples"]["timestamp"].append(str(db.sequence_file[sequence_file_id].sampling_date))
            data["samples"]["info"].append(db.sequence_file[sequence_file_id].info)
360
            data["samples"]["commandline"].append(db.config[config_id].command)
361 362 363 364 365 366 367 368 369

        return gluon.contrib.simplejson.dumps(data, separators=(',',':'))

    else :
        res = {"success" : "false",
               "message" : "default/get_custom_data : " + error}
        log.error(res)
        return gluon.contrib.simplejson.dumps(res, separators=(',',':'))
    
370 371
#########################################################################
## return .analysis file
372
# need patient_id
373
# need patient admin or read permission
374 375 376
def get_analysis():
    error = ""

377 378 379 380
    if "custom" in request.vars :
        res = {"success" : "true"}
        return gluon.contrib.simplejson.dumps(res, separators=(',',':'))

381 382 383 384 385 386
    if "patient" in request.vars :
        request.vars["sample_set_id"] = db.patient[request.vars["patient"]].sample_set_id

    if "run" in request.vars :
        request.vars["sample_set_id"] = db.run[request.vars["run"]].sample_set_id
    
387 388 389 390
    if not "sample_set_id" in request.vars :
        error += "id sample_set file needed, "
    if not auth.can_view_sample_set(request.vars["sample_set_id"]):
        error += "you do not have permission to consult this sample_set ("+str(request.vars["sample_set_id"])+")"
391

392
    if "custom" in request.vars :
393
        return gluon.contrib.simplejson.dumps(get_default_analysis(), separators=(',',':'))
394
    
395
    if error == "" :
396
        
397
        ## récupération des infos se trouvant dans le fichier .analysis
398
        analysis_data = get_analysis_data(request.vars['sample_set_id'])
399
        #analysis_data["info_patient"] = db.patient[request.vars["patient"]].info
400
        return gluon.contrib.simplejson.dumps(analysis_data, separators=(',',':'))
401

402 403 404
    else :
        res = {"success" : "false",
               "message" : "default/get_analysis : " + error}
405
        log.error(res)
406
        return gluon.contrib.simplejson.dumps(res, separators=(',',':'))
407 408


409 410
#########################################################################
## upload .analysis file and store it on the database
411
# need patient_id, fileToUpload
412
# need patient admin permission
413 414
def save_analysis():
    error = ""
415 416 417 418 419
    if "patient" in request.vars :
        request.vars["sample_set_id"] = db.patient[request.vars["patient"]].sample_set_id

    if "run" in request.vars :
        request.vars["sample_set_id"] = db.run[request.vars["run"]].sample_set_id
420

421
    if not "sample_set_id" in request.vars :
422 423
        error += "It is currently not possible to save an analysis on a comparison of samples, "
    elif not auth.can_modify_sample_set(request.vars['sample_set_id']) :
424
        error += "you do not have permission to save changes on this sample set"
425

426 427
    if error == "" :
        f = request.vars['fileToUpload']
428
        ts = time.time()
429
        
430
        sample_set_id = request.vars['sample_set_id']
431
        
432
        analysis_id = db.analysis_file.insert(analysis_file = db.analysis_file.analysis_file.store(f.file, f.filename),
433
                                              sample_set_id = sample_set_id,
434 435
                                              analyze_date = datetime.datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S')
                                              )
436

437
        sample_type = db.sample_set[sample_set_id].sample_type
438 439 440 441 442 443 444 445 446 447
        if (request.vars['info'] is not None):
            if (sample_type == "patient") :
                db(db.patient.sample_set_id == sample_set_id).update(info = request.vars['info']);
            
            if (sample_type == "run") :
                db(db.run.sample_set_id == sample_set_id).update(info = request.vars['info']);

        if (request.vars['samples_id'] is not None and request.vars['samples_info'] is not None):
	    ids = request.vars['samples_id'].split(',')
	    infos = request.vars['samples_info'].split(',')
448 449
        
        
450 451 452 453
            # TODO find way to remove loop ?
            for i in range(0, len(ids)):
                if(len(ids[i]) > 0):
                    db(db.sequence_file.id == int(ids[i])).update(info = infos[i])
454

455
        #patient_name = db.patient[request.vars['patient']].first_name + " " + db.patient[request.vars['patient']].last_name
456

457
        res = {"success" : "true",
458
               "message" : "(%s): analysis saved" % (sample_set_id)}
459
        log.info(res)
460 461
        return gluon.contrib.simplejson.dumps(res, separators=(',',':'))
    else :
462 463
        res = {"success" : "false",
               "message" : error}
464
        log.error(res)
465
        return gluon.contrib.simplejson.dumps(res, separators=(',',':'))
466 467


468

Vidjil Team's avatar
Vidjil Team committed
469
#########################################################################
470
def error():
Vidjil Team's avatar
Vidjil Team committed
471 472 473 474
    """
    Custom error handler that returns correct status codes,
    adapted from http://www.web2pyslices.com/slice/show/1529/custom-error-routing
    """
475

Vidjil Team's avatar
Vidjil Team committed
476 477
    code = request.vars.code
    request_url = request.vars.request_url
478
    requested_uri = request.vars.requested_uri
Vidjil Team's avatar
Vidjil Team committed
479
    ticket = request.vars.ticket
480
    response.status = int(code)
481

482
    assert(response.status == 500 and request_url != request.url) # avoid infinite loop
Vidjil Team's avatar
Vidjil Team committed
483

484 485 486
    ticket_url = '<a href="https://%(host)s/admin/default/ticket/%(ticket)s">%(ticket)s</a>' % { 'host':request.env.http_host,
                                                                                                 'ticket':ticket }
    log.error("Server error // %s" % ticket_url)
487

488
    user_str, x = log.process('', None)
489
    user_str = user_str.replace('<','').replace('>','').strip()
490

491 492 493
    mail.send(to=defs.ADMIN_EMAILS,
              subject="[Vidjil] Server error - %s" % user_str,
              message="<html>Ticket: %s<br/>At: %s<br />User: %s</html>" % (ticket_url, requested_uri, user_str))
Vidjil Team's avatar
Vidjil Team committed
494 495

    return "Server error"
496

Marc Duez's avatar
Marc Duez committed
497
    
498 499 500 501 502 503 504 505 506 507 508 509 510 511 512
def user():
    """
    exposes:
    http://..../[app]/default/user/login
    http://..../[app]/default/user/logout
    http://..../[app]/default/user/register
    http://..../[app]/default/user/profile
    http://..../[app]/default/user/retrieve_password
    http://..../[app]/default/user/change_password
    http://..../[app]/default/user/manage_users (requires membership in
    use @auth.requires_login()
        @auth.requires_membership('group name')
        @auth.requires_permission('read','table name',record_id)
    to decorate functions that need access control
    """
513

Marc Duez's avatar
Marc Duez committed
514
    #redirect already logged user 
515
    if auth.user and request.args[0] == 'login' :
516
        res = {"redirect" : URL('default', 'home', scheme=True, host=True)}
517
        return gluon.contrib.simplejson.dumps(res, separators=(',',':'))
Marc Duez's avatar
Marc Duez committed
518 519 520 521 522 523 524 525 526 527 528 529 530 531 532
    
    #only authentified admin user can access register view
    if auth.user and request.args[0] == 'register' :
        #save admin session (the registering will automatically login the new user in order to initialize its default values)
        admin_auth = session.auth
        auth.is_logged_in = lambda: False
        
        def post_register(form):
            #default values for new user
            group_id = db(db.auth_group.role == 'public' ).select()[0].id
            db.auth_membership.insert(user_id = auth.user.id, group_id = group_id)
            #restore admin session after register
            session.auth = admin_auth
            auth.user = session.auth.user
        auth.settings.register_onaccept = post_register
533 534 535 536 537
        
        #redirect to the last added user view
        auth.settings.logged_url = URL('user', 'info')
        auth.settings.login_next = URL('user', 'info')
        
Marc Duez's avatar
Marc Duez committed
538 539 540 541 542 543 544
        return dict(form=auth.register())
    
    #reject others
    if request.args[0] == 'register' :
        res = {"message": "you need to be admin and logged to add new users"}
        return gluon.contrib.simplejson.dumps(res, separators=(',',':'))
    
545
    return dict(form=auth())
546

Marc Duez's avatar
Marc Duez committed
547 548 549 550
def impersonate() :
    if auth.is_impersonating() :
        stop_impersonate()
    if request.vars["id"] != 0 :
551
        log.debug({"success" : "true", "message" : "impersonate >> %s" % request.vars["id"]})
Marc Duez's avatar
Marc Duez committed
552
        auth.impersonate(request.vars["id"]) 
553
        log.debug({"success" : "true", "message" : "impersonated"})
554 555 556 557
    if not 'admin' in request.vars['next']:
        res = {"redirect": "reload"}
    else:
        res = {"redirect" : URL('patient', 'index', scheme=True, host=True)}
Marc Duez's avatar
Marc Duez committed
558 559 560 561
    return gluon.contrib.simplejson.dumps(res, separators=(',',':'))

def stop_impersonate() :
    if auth.is_impersonating() :
562
        log.debug({"success" : "true", "message" : "impersonate << stop"})
Marc Duez's avatar
Marc Duez committed
563 564 565 566
        auth.impersonate(0) 
        # force clean login (default impersonate don't restore everything :/ )
        auth.login_user(db.auth_user(auth.user.id))

567
    res = {"redirect" : "reload"}
Marc Duez's avatar
Marc Duez committed
568 569 570 571
    return gluon.contrib.simplejson.dumps(res, separators=(',',':'))



572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588
## TODO make custom download for .data et .analysis
@cache.action()
def download():
    """
    allows downloading of uploaded files
    http://..../[app]/default/download/[filename]
    """
    return response.download(request, db, download_filename=request.vars.filename)

def download_data():

    file = "test"
    return response.stream( file, chunk_size=4096, filename=request.vars.filename)



#########################################################################