Example #1
0
def _newTraitCategorical(sess, request, trt):
    capKeys = [key for key in request.form.keys() if key.startswith("caption_")]
    for key in capKeys:
        caption = request.form.get(key)
        value = request.form.get(key.replace("caption_", "value_"))

        # If image provided we need to store it
        imageURL = None
        try:
            imageURLFile = request.files[key.replace("caption_", "imgfile_")]
            if imageURLFile:
                sentFilename = secure_filename(imageURLFile.filename)
                if allowed_file(sentFilename):
                    # Note, file are stored under a configured category image folder,
                    # and then with path <userName>/<trait id>/<value>.<original file extension>
                    subpath = os.path.join(app.config['CATEGORY_IMAGE_FOLDER'], sess.getProjectName(), str(trt.id))
                    if not os.path.exists(subpath):
                        os.makedirs(subpath)
                    fileExt = sentFilename.rsplit('.', 1)[1]
                    newFilename = '{0}.{1}'.format(value, fileExt)
                    imageURLFile.save(subpath +  "/" + newFilename)
                    imageURL = app.config['CATEGORY_IMAGE_URL_BASE'] + "/" + sess.getProjectName() + "/" + str(trt.id) + "/" + newFilename
                else:
                    util.flog("_newTraitCategorical bad file name")
        except Exception, e:
            util.flog("Exception in _newTraitCategorical: {0}".format(str(e)))

        # Determine if this is a new category or an existing one.
        # Trait categories are identified by the value (within a trait).
        tcat = trt.getCategory(value)
        newCat = tcat is None
        if newCat:
            # Add new trait category:
            tcat = models.TraitCategory()
            tcat.value = value
            tcat.caption = caption
            tcat.trait_id = trt.id
            tcat.imageURL = imageURL
            sess.db().add(tcat)
        else:
            util.flog("Existing category: cap:{0} value:{1}".format(caption, value))
            # This is an existing category, update caption, or image URL if necessary:
            if tcat.caption != caption:
                tcat.caption = caption
            # (Re)Set the image if necessary. There is a difficulty here.
            # NB If an image was already set, and user doesn't change it, it will come back empty.
            # And we don't want to set it to nothing in this case. The problem remains, however, as
            # to how can the user explicitly set it to no image?
            if imageURL is not None:
                tcat.imageURL = imageURL
def urlAppCoolUploadCrashReport():
    #-------------------------------------------------------------------------------------------------
    # check file size?
    # Handle a crash report upload from the app.
    cfile = request.files.get('uploadedfile')
    util.flog('urlAppCoolUploadCrashReport: filename {0}'.format(
        cfile.filename))
    if cfile and allowed_file(cfile.filename):
        sentFilename = secure_filename(cfile.filename)
        saveName = sentFilename
        try:
            # Need to check if file exists, if so postfix copy num to name so as not to overwrite:
            fullPath = current_app.config[
                'CRASH_REPORT_UPLOAD_FOLDER'] + saveName
            cfile.save(fullPath)
            return successResponse()
        except Exception, e:
            util.flog('failed save {0}'.format(
                current_app.config['CRASH_REPORT_UPLOAD_FOLDER'] + saveName))
            util.flog(e.__doc__)
            util.flog(e.message)
            return serverErrorResponse('Failed crash report upload : can'
                                       't save')
def urlAppUploadTrialData(username, trial, dbc, token):
    #-------------------------------------------------------------------------------------------------
    # This version should return JSON!
    # NB historical peculiarities here, this attow only used for upload nodes, or notes.
    # And the format of the response differs between these two. Probably would be better
    # with separate urls.
    #
    # Note used to have old_version, where the token was in the json not the url, see svn history
    # for this code if necessary. Only relevant if old clients are still out there, but I think
    # not. URLs sent and stored on client when this function was
    # used should still be able to access this func (since the URL will match). But now
    # we send out the URL for the new version.
    jtrial = request.json
    if not jtrial:
        return Response('Bad or missing JSON')
    util.flog("upload_trial:\n" + json.dumps(jtrial))

    # Probably need a 'command' or 'type' field.
    # different types will need different responses.
    # Note old client would not have it, so perhaps below must stay

    # Old clients may just send 'notes', we process that here in the manner they expect:
    # MFK - so what do new clients do different? Nothing attow.
    if 'notes' in jtrial:  # We really should put these JSON names in a set of string constants somehow..
        err = trial.addNodeNotes(token, jtrial[jTrialUpload['notes']])
        if err is not None:
            util.flog('addNodeNotes fail:{0}'.format(err))
            return Response(err)

        # All done, return success indicator:
        return successResponse()

    #
    # Created Nodes:
    # Process nodes created on the client. We need to create them on the server,
    # and send back the ids of the new server versions. This needs to be idempotent,
    # i.e. if a client sends a node more than once, it should only be created once
    # on the server. This is managed by recording the token and the client node id
    # for each created node (in the database).
    #
    if JTRL_NODES_ARRAY in jtrial:
        # MFK - make this an array of objects, preferably same format as sent server to client.
        clientLocalIds = jtrial[JTRL_NODES_ARRAY]
        serverIds = []
        serverRows = []
        serverCols = []
        # We have to return array of server ids to replace the passed in local ids.
        # We need to record the local ids so as to be idempotent.
        tokenObj = dal.Token.getToken(dbc, token, trial.id)
        if tokenObj is None:
            util.flog('urlAppUploadTrialData: invalid token')
            return Response('invalid token')
        for newid in clientLocalIds:
            #print 'new id: {0}'.format(newid)
            # Create node, or get it if it already exists:
            node = dal.TokenNode.getOrCreateClientNode(dbc, tokenObj.id, newid,
                                                       trial.id)
            serverIds.append(node.getId())
            serverRows.append(node.getRow())
            serverCols.append(node.getCol())
        returnObj = {
            'nodeIds': serverIds,
            'nodeRows': serverRows,
            'nodeCols': serverCols
        }
        return Response(json.dumps(returnObj),
                        mimetype='application/json')  # prob need ob

    # All done, return success indicator:
    return successResponse()
def urlAppUploadPhoto(username, trial, dbc, traitid, token):
    #-------------------------------------------------------------------------------------------------
    # Handle a photo upload from the app.
    # These are uniquely identified by dbusername/trial/trait/token/seqNum/sampleNum.
    # These are all provided in the url except for seqNum and sampleNum which come
    # (out-of-band) as form parameters. In addition the score metadata is also provided
    # in the form parameters. This enables us to create the datum record in this function.
    # Note originally we only saved the photo here, and the client would separately upload
    # the traitInstance containing all the meta data as for a non-photo traitInstance.
    # This was problematic because until both the metadata, and the photo itself are both
    # uploaded we have not successfully uploaded. Hence it is better done in a single
    # transaction (this func). Currently, the client will first upload a trait instance
    # WITHOUT any datums, to ensure the traitInstance is created on the server, and to
    # set its creation date. If we didn't do this we would have to upload the creation
    # date with every photo (or at least the first one) so as to allow us to create the
    # traitInstance record in this function (if it was not already present).
    #
    # UPDATE - Attow the creation date is sent with each photo, and the client does NOT
    # first create an empty ti.
    #
    # Note the node Id is uploaded in the file name (this should be same as server node id).
    # But in addition it is (now) also uploaded as a form parameter, this reduces our
    # dependence on it being in the file name (which may change).
    # The photos are saved in the PHOTO_UPLOAD_FOLDER folder, with the name encoding
    # all the relevant info:
    # '{0}_{1}_{2}_{3}_{4}_{5}_{6}.jpg'.format(dbusername, trialId, traitId, nodeId, token, seqNum, sampNum)
    #
    seqNum = request.args.get(TI_SEQNUM, '')
    sampNum = request.args.get(TI_SAMPNUM, '')
    timestamp = request.args.get(DM_TIMESTAMP, '')
    userid = request.args.get(DM_USERID, '')
    gpslat = request.args.get(DM_GPS_LAT, '')
    gpslong = request.args.get(DM_GPS_LONG, '')
    nodeId = request.args.get(DM_NODE_ID_CLIENT_VERSION, '')
    dayCreated = request.args.get(TI_DAYCREATED, '')

    file = request.files.get('uploadedfile')
    util.flog(
        'urlAppUploadPhoto:node {0}, seq {1} samp {2} filename {3}'.format(
            nodeId, seqNum, sampNum, file.filename))
    if file and allowed_file(file.filename):
        sentFilename = secure_filename(file.filename)
        saveName = dal.photoFileName(username, trial.id, traitid, int(nodeId),
                                     token, seqNum, sampNum)
        try:
            # Need to check if file exists, if so postfix copy num to name so as not to overwrite:
            fullPath = current_app.config['PHOTO_UPLOAD_FOLDER'] + saveName
            print 'fullPath {}'.format(
                current_app.config['PHOTO_UPLOAD_FOLDER'] + saveName)
            base = os.path.splitext(fullPath)[0]
            ext = os.path.splitext(fullPath)[1]
            tryAgain = os.path.isfile(fullPath)
            i = 1
            while tryAgain:
                fullPath = '{0}_c{1}{2}'.format(base, i, ext)
                i += 1
                tryAgain = os.path.isfile(fullPath)

            file.save(fullPath)
        except Exception, e:
            util.flog('failed save {0}'.format(
                current_app.config['PHOTO_UPLOAD_FOLDER'] + saveName))
            util.flog(e.__doc__)
            util.flog(e.message)
            return serverErrorResponse('Failed photo upload : can' 't save')

        # Now save datum record:
        # get TI - this should already exist, which is why we can pass in 0 for dayCreated
        # MFK note this outer if below is to support old versions of the app, to allow them to
        # upload their photos in the old way. It should be removed eventually..
        if nodeId is not None and len(nodeId) > 0:
            dbTi = dal.getOrCreateTraitInstance(dbc, traitid, trial.id, seqNum,
                                                sampNum, dayCreated, token)
            if dbTi is None:
                return serverErrorResponse(
                    'Failed photo upload : no trait instance')
            res = dal.AddTraitInstanceDatum(dbc, dbTi.id, dbTi.trait.datatype,
                                            nodeId, timestamp, userid,
                                            gpslat, gpslong,
                                            os.path.basename(fullPath))
            if res is None:
                return successResponse()
            else:
                return serverErrorResponse(
                    'Failed photo upload : datum create fail')
        else:
            util.flog('urlAppUploadPhoto: no nodeId, presumed old app version')
            return successResponse()
def serverErrorResponse(msg):
    util.flog(msg)
    response = Response(msg)
    response.status = '500 {0}'.format(msg)
    return response
    # Get json fields:
    try:
        dayCreated = jti["dayCreated"]
        seqNum = jti["seqNum"]
        sampleNum = jti["sampleNum"]
    except Exception, e:
        return Response('Missing required traitInstance field: ' + e.args[0] +
                        trial.name)
    try:
        aData = jti["data"]
    except Exception, e:
        aData = None

    # Log upload, but don't output json.dumps(jti), as it can be big:
    util.flog(
        "urlAppUploadTraitData from {0}: dc:{1}, seq:{2}, samp:{3}".format(
            token, dayCreated, seqNum, sampleNum,
            "None" if aData is None else len(aData)))

    # MFK: A problem here in that ideally we don't want to create empty scoresets.
    # The photo upload code, relies on being able to create an empty scoreset on the
    # server prior to uploading the pictures. Actually, that's now not the case, but
    # there may be versions of the app out there for a while that do require an empty
    # set to be created. So for the moment, we'll limit this to photo traits only,
    # and then remove that when we're confident all apps are updated:
    if (aData is None or len(aData) <= 0) and dal.getTrait(
            dbc, traitid).datatype != T_PHOTO:
        return successResponse()
    # Get/Create trait instance:
    dbTi = dal.getOrCreateTraitInstance(dbc, traitid, trial.id, seqNum,
                                        sampleNum, dayCreated, token)
    if dbTi is None:
Example #7
0
def traitDetailsPageHandler(sess, request, trial, trialId, traitId):
#===========================================================================
# Handles both GET and POST for page to display/modify details for a trait.
#
# MFK Note overlap with code from trait creation.
# Needs rework, most models calls here should be from trial object, not statics
#
    projId = sess.getProjectId()
    trt = models.getTrait(sess.db(), traitId)
    if trt is None:
        util.flog("No trait in traitDetailsPageHandler, trialId: {}, traitId: {}".format(trialId, traitId))
        return errorScreenInSession('Cannot find trait')
    trlTrt = models.getTrialTrait(sess.db(), trialId, traitId)
    if trlTrt is None:
        util.flog("No trialTrait in traitDetailsPageHandler, trialId: {}, traitId: {}".format(trialId, traitId))
        return errorScreenInSession('Cannot find trial trait')
    title = 'Trial: ' + trial.name + ', Trait: ' + trt.caption
    comparatorCodes = [
        ["gt", "Greater Than", 1],
        ["ge", "Greater Than or Equal to", 2],
        ["lt", "Less Than", 3],
        ["le", "Less Than or Equal to", 4]
    ]

    if request.method == 'GET':
        ###########################################################################
        # Form fields applicable to all traits:
        ###
        formh = fpUtil.htmlLabelValue('Trait', trt.caption) + '<br>'
        formh += fpUtil.htmlLabelValue('Type', TRAIT_TYPE_NAMES[trt.datatype])
        formh += fpUtil.htmlHorizontalRule()
        formh += fpUtil.htmlLabelValue('Description',
            '<input type="text" size=96 name="description" value="{0}">'.format(trt.description))

        # Trait barcode selection:
        # Note it doesn't matter if a sysTrait, since the barcode is stored in trialTrait
        attSelector = '<select name="bcAttribute" id="bcAttribute">'
        attSelector += '<option value="none">&lt;Choose Attribute&gt;</option>'
        atts = trial.getAttributes()
        for att in atts:
            attSelector += '<option value="{0}" {2}>{1}</option>'.format(
                att.id, att.name, "selected='selected'" if att.id == trlTrt.barcodeAtt_id else "")
        attSelector += '</select>'
        formh += '<br>' + fpUtil.htmlLabelValue('Barcode for Scoring', attSelector)

        # Vars that may be set by trait specifics, to be included in output:
        preform = ''
        onsubmit = ''
        if trt.datatype == T_CATEGORICAL:
            # Note the intended policy: Users may modify the caption or image of an existing
            # category, but not change the numeric value. They may add new categories.
            # The reasoning is that they should remove existing categories (as identified
            # by the value) because there may be data collected already with the values
            # which would then become undefined, or perhaps ambiguous. We could have a look
            # up and allow removal if there is no data, but we cannot be sure data will
            # not come in in the future referencing current values (unless we know trial
            # has never been downloaded). In any case this limited functionality is better
            # than none.

            #####
            ## Setup javascript to manage the display/modification of the categories.
            #####

            # Retrieve the categories from the database and make of them a javascript literal:
            catRecs = trt.categories
            catObs = ''
            for cat in catRecs:
                if catObs: catObs += ','
                catObs += '{{caption:"{0}", imageURL:"{1}", value:{2}}}'.format(cat.caption, cat.imageURL, cat.value)
            jsRecDec = '[{0}]'.format(catObs)
            formh += '<div id="traitTypeSpecificsDiv"></div>\n'
            formh += '<script src="{0}"></script>\n'.format(url_for('static', filename='newTrait.js'))
            formh += """<script type="text/javascript">
                jQuery(function(){{fpTrait.setTraitFormElements('traitTypeSpecificsDiv', '3', {0});}});
            </script>""".format(jsRecDec)

            onsubmit = "return fpTrait.validateTraitTypeSpecific('traitTypeSpecificsDiv', '{0}')".format(trt.datatype)
        elif trt.datatype == T_INTEGER or trt.datatype == T_DECIMAL:
            #
            # Generate form on the fly. Could use template but there's lots of variables.
            # Make this a separate function to generate html form, so can be used from
            # trait creation page. MFK - maybe not relevant now for trait creation page,
            # as we don't put the min/max or validation options there (in case it's a sys
            # trait, and these things should be able to vary between instances of the trait).
            #
            ttn = models.GetTrialTraitNumericDetails(sess.db(), traitId, trialId)

            # Min and Max:
            # need to get decimal version if decimal. Maybe make ttn type have getMin/getMax func and use for both types
            minText = ""
            if ttn and ttn.min is not None:
                minText = "value='{:f}'".format(ttn.getMin())
            maxText = ""
            if ttn and ttn.max is not None:
                maxText = "value='{:f}'".format(ttn.getMax())
            minMaxBounds = "<p>Minimum: <input type='text' name='min' id=tdMin {0}>".format(minText)
            minMaxBounds += "<p>Maximum: <input type='text' name='max' id=tdMax {0}><br>".format(maxText);

            # Parse condition string, if present, to retrieve comparator and attribute.
            # Format of the string is: ^. <2_char_comparator_code> att:<attribute_id>$
            # The only supported comparison at present is comparing the score to a
            # single attribute.
            # NB, this format needs to be in sync with the version on the app. I.e. what
            # we save here, must be understood on the app.
            # MFK note attribute id seems to be stored as text in cond string, will seems
            # not ideal. Probably should be a field in the table trialTraitNumeric.
            # Note that the same issue applies in the app database There is one advantage
            # I see to having a string is that we can change what is stored without requiring
            # a database structure change. And db structure changes on the app require
            # a database replace on the app.
            atId = -1
            op = ""
            if ttn and ttn.cond is not None:
                tokens = ttn.cond.split()  # [["gt", "Greater than", 0?], ["ge"...]]?
                if len(tokens) != 3:
                    return "bad condition: " + ttn.cond
                op = tokens[1]
                atClump = tokens[2]
                atId = int(atClump[4:])

            # Show available comparison operators:
            valOp = '<select name="validationOp" id="tdCompOp">'
            valOp += '<option value="0">&lt;Choose Comparator&gt;</option>'
            for c in comparatorCodes:
                valOp += '<option value="{0}" {2}>{1}</option>'.format(
                    c[2], c[1], 'selected="selected"' if op == c[0] else "")
            valOp += '</select>'

            # Attribute list:
            attListHtml = '<select name="attributeList" id="tdAttribute">'
            attListHtml += '<option value="0">&lt;Choose Attribute&gt;</option>'
            for att in atts:
                if att.datatype == T_DECIMAL or att.datatype == T_INTEGER:
                    attListHtml += '<option value="{0}" {2}>{1}</option>'.format(
                        att.id, att.name, "selected='selected'" if att.id == atId else "")
            attListHtml += '</select>'

            # javascript form validation
            # NEED TO Check that min and max are valid int or decimal
            # Check that if one of comp and att chosen both are
            # Note this is the same validation for integer and decimal. So integer
            # will allow decimal min/max. Could be made strict, but I'm not sure this is bad.
            script = """
                <script>
                function isValidDecimal(inputtxt) {
                    var decPat =  /^[+-]?[0-9]+(?:\.[0-9]+)?$/g;
                    return inputtxt.match(decPat);
                }

                function validateTraitDetails() {
                    // Check min and max fields:
                    /* It should be OK to have no min or max:
                    if (!isValidDecimal(document.getElementById("tdMin").value)) {
                        alert('Invalid value for minimum');
                        return false;
                    }
                    if (!isValidDecimal(document.getElementById("tdMax").value)) {
                        alert('Invalid value for maximum');
                        return false;
                    }
                    */

                    // Check attribute/comparator fields, either both or neither present:
                    var att = document.getElementById("tdAttribute").value;
                    var comp = document.getElementById("tdCompOp").value;
                    var attPresent = (att !== null && att !== "0");
                    var compPresent = (comp !== null && comp !== "0");
                    if (attPresent && !compPresent) {
                        alert("Attribute selected with no comparator specified, please fix.");
                        return false;
                    }
                    if (!attPresent && compPresent) {
                        alert("Comparator selected with no attribute specified, please fix.");
                        return false;
                    }
                    return true;
                }
                </script>
            """
            formh += minMaxBounds
            formh += '<p>Integer traits can be validated by comparison with an attribute:'
            formh += '<br>Trait value should be ' + valOp + attListHtml
            preform = script
            onsubmit ='return validateTraitDetails()'
        elif trt.datatype == T_STRING:
            tts = models.getTraitString(sess.db(), traitId, trialId)
            patText = "value='{0}'".format(tts.pattern) if tts is not None else ""
            formh += "<p>Pattern: <input type='text' name='pattern' id=tdMin {0}>".format(patText)

        formh += ('\n<p><input type="button" value="Cancel"' +
            ' onclick="location.href=\'{0}\';">'.format(fpUtil.fpUrl('urlTrial', sess, trialId=trialId)))
        formh += '\n<input type="submit" value="Submit">'
        return dp.dataPage( \
                    content=preform + fpUtil.htmlForm(formh, post=True, onsubmit=onsubmit, multipart=True),
                    title='Trait Validation', trialId=trialId)

    ##################################################################################################
    if request.method == 'POST':
        ### Form fields applicable to all traits:
        # Trait barcode selection:
        #MFK sys traits? barcode field is an nodeAttribute id but this is associated with a trial
        # we either have to move it to trialTrait, or make all trial traits non system traits.
        barcodeAttId = request.form.get('bcAttribute')  # value should be valid attribute ID
        if barcodeAttId == 'none':
            trlTrt.barcodeAtt_id = None
        else:
            trlTrt.barcodeAtt_id = barcodeAttId
        trt.description = request.form.get('description')

        #
        # Trait type specific stuff:
        #
        if trt.datatype == T_CATEGORICAL:
            _newTraitCategorical(sess, request, trt)
        elif trt.datatype == T_INTEGER or trt.datatype == T_DECIMAL: # clone of above remove above when integer works with numeric
            op = request.form.get('validationOp')  # value should be [1-4], see comparatorCodes
            if not re.match('[0-4]', op):
                return "Invalid operation {0}".format(op) # should be some function to show error page..
            at = request.form.get('attributeList') # value should be valid attribute ID

            # Check min/max:
            vmin = request.form.get('min')
            if len(vmin) == 0:
                vmin = None
            vmax = request.form.get('max')
            if len(vmax) == 0:
                vmax = None

            # Get existing trialTraitNumeric, or create new one if none:
            ttn = models.GetTrialTraitNumericDetails(sess.db(), traitId, trialId)
            newTTN = ttn is None
            if newTTN:
                ttn = models.TrialTraitNumeric()
            ttn.trial_id = trialId
            ttn.trait_id = traitId
            ttn.min = vmin
            ttn.max = vmax
            if int(op) > 0 and int(at) > 0:
                ttn.cond = ". " + comparatorCodes[int(op)-1][0] + ' att:' + at
            if newTTN:
                sess.db().add(ttn)
        elif trt.datatype == T_STRING:
            newPat = request.form.get('pattern')
            tts = models.getTraitString(sess.db(), traitId, trialId)
            if len(newPat) == 0:
                newPat = None
            # delete tts if not needed:
            if not newPat:
                if tts:
                    sess.db().delete(tts)
            else:
                if tts:
                    tts.pattern = newPat
                else:
                    tts = models.TraitString(trait_id=traitId, trial_id=trialId, pattern=newPat)
                    sess.db().add(tts)

        sess.db().commit()

        return redirect(fpUtil.fpUrl('urlTrial', sess, trialId=trialId))