Commit 1b0fc6f9 authored by Romain Loth's avatar Romain Loth

finish fundamental login and profile scenarios

parent 008c9ab5
......@@ -326,11 +326,14 @@ def get_full_scholar(uid):
return urow_dict
def save_scholar(uid, date, safe_recs, reg_db, uactive = True):
def save_scholar(uid, date, safe_recs, reg_db, uactive=True, update_flag=False):
"""
Useful for new registration:
For new registration:
-> add to *scholars* table
For profile change (just toggle update_flag to True)
-> *update* scholars table
see also COLS variable and doc/table_specifications.md
"""
......@@ -346,8 +349,8 @@ def save_scholar(uid, date, safe_recs, reg_db, uactive = True):
# => But currently bug in MySQLdb for binary values)
# (see also MySQLdb.converters)
# => So for now we buid the values string ourselves in db_qstrvals instead
# ------------- -----------
# => So for now we build the values string ourselves in db_qstrvals instead
# ------------- -----------
# and then we execute(full_statmt) :-)
......@@ -377,19 +380,33 @@ def save_scholar(uid, date, safe_recs, reg_db, uactive = True):
db_tgtcols.append('record_status')
db_qstrvals.append('"active"')
# expected colnames "(doors_uid, last_modified_date, email, ...)"
db_tgtcols_str = ','.join(db_tgtcols)
reg_db_c = reg_db.cursor()
# fields converted to sql syntax
db_vals_str = ','.join(db_qstrvals)
if not update_flag:
# expected colnames "(doors_uid, last_modified_date, email, ...)"
db_tgtcols_str = ','.join(db_tgtcols)
reg_db_c = reg_db.cursor()
# fields converted to sql syntax
db_vals_str = ','.join(db_qstrvals)
# INSERT: full_statement with formated values
full_statmt = 'INSERT INTO scholars (%s) VALUES (%s)' % (
db_tgtcols_str,
db_vals_str
)
else:
# we won't change the ID now
db_tgtcols.pop(0)
db_qstrvals.pop(0)
set_full_str = ','.join([db_tgtcols[i] + '=' + db_qstrvals[i] for i in range(len(db_tgtcols))])
# UPDATE: full_statement with formated values
full_statmt = 'UPDATE scholars SET %s WHERE doors_uid = "%s"' % (
set_full_str,
uid
)
print(full_statmt)
# full_statement with formated values
full_statmt = 'INSERT INTO scholars (%s) VALUES (%s)' % (
db_tgtcols_str,
db_vals_str
)
reg_db_c.execute(full_statmt)
reg_db.commit()
......@@ -407,6 +424,16 @@ def save_pairs_sch_kw(pairings_list, comex_db):
mlog("DEBUG", "Keywords: saved %s pair" % str(id_pair))
def delete_pairs_sch_kw(uid, comex_db):
"""
Simply deletes all pairings (uid, *) in the table
"""
db_cursor = comex_db.cursor()
n = db_cursor.execute('DELETE FROM sch_kw WHERE uid = "%s"' % uid)
comex_db.commit()
mlog("DEBUG", "Keywords: DELETED %i pairings for %s" % (n, str(uid)))
def get_or_create_keywords(kw_list, comex_db):
"""
kw_str -> lookup/add to *keywords* table -> kw_id
......
......@@ -38,7 +38,7 @@ if __package__ == 'services':
from services.user import User, login_manager, doors_login, UCACHE
from services.text import keywords
from services.tools import restparse, mlog, re_hash, REALCONFIG
from services.db import connect_db, get_or_create_keywords, save_pairs_sch_kw, get_or_create_affiliation, save_scholar, get_field_aggs
from services.db import connect_db, get_or_create_keywords, save_pairs_sch_kw, delete_pairs_sch_kw, get_or_create_affiliation, save_scholar, get_field_aggs
from services.db_to_tina_api.extractDataCustom import MyExtractor as MySQL
else:
# when this script is run directly
......@@ -46,7 +46,7 @@ else:
from user import User, login_manager, doors_login, UCACHE
from text import keywords
from tools import restparse, mlog, re_hash, REALCONFIG
from db import connect_db, get_or_create_keywords, save_pairs_sch_kw, get_or_create_affiliation, save_scholar, get_field_aggs
from db import connect_db, get_or_create_keywords, save_pairs_sch_kw, delete_pairs_sch_kw, get_or_create_affiliation, save_scholar, get_field_aggs
from db_to_tina_api.extractDataCustom import MyExtractor as MySQL
# ============= read config ============
......@@ -194,53 +194,72 @@ def login():
doors_connect = config['DOORS_HOST']+':'+config['DOORS_PORT']
)
elif request.method == 'POST':
# TODO check captcha
# TODO sanitize
email = request.form['email']
pwd = request.form['password']
# testing the captcha answer
captcha_userinput = request.form['my-captcha']
captcha_userhash = re_hash(captcha_userinput)
captcha_verifhash = int(request.form['my-captchaHash'])
# we do our doors request here server-side to avoid MiM attack on result
uid = doors_login(email, pwd, config)
# dbg
mlog("DEBUG", "login captcha verif", str(captcha_verifhash))
mlog("DEBUG", "login captcha user", str(captcha_userhash))
if uid:
if captcha_userhash != captcha_verifhash:
mlog("WARNING", "pb captcha rejected")
return render_template(
"message.html",
message = """
We're sorry the "captcha" information you entered was wrong!
<br/>
<strong><a href="%s">Retry login here</a></strong>.
""" % url_for('login', _external=True)
)
else:
# OK captcha accepted
email = request.form['email']
pwd = request.form['password']
login_ok = login_user(User(uid))
# we do our doors request here server-side to avoid MiM attack on result
uid = doors_login(email, pwd, config)
# login_ok = login_user(User(uid), remember=True)
# -------------
# creates REMEMBER_COOKIE_NAME
# which is itself bound to session cookie
if uid:
login_ok = login_user(User(uid))
if login_ok:
# normal user
return redirect(url_for('profile', _external=True))
# POSS "next" request.args (useful when we'll have more pages)
# ---
# TODO check cookie
# login_ok = login_user(User(uid), remember=True)
# -------------
# creates REMEMBER_COOKIE_NAME
# which is itself bound to session cookie
else:
# user exists in doors but has no comex profile yet
# => TODO
# => we add him
# => status = "fresh_profile"
# => empty profile
# return redirect(url_for('fresh_profile', _external=True))
return redirect(url_for('register', _external=True))
if login_ok:
# normal user
return redirect(url_for('profile', _external=True))
# POSS "next" request.args (useful when we'll have more pages)
# ---
else:
# user doesn't exist in doors nor comex_db
# (shouldn't happen since client-side blocks submit and displays same message, but still possible if user tweaks the js)
return render_template(
"message.html",
message = """
We're sorry but you don't exist in our database yet!
<br/>
However you can easily <strong><a href="%s">register here</a></strong>.
""" % url_for('register', _external=True)
)
else:
# user exists in doors but has no comex profile yet
# => TODO
# => we add him
# => status = "fresh_profile"
# => empty profile
# return redirect(url_for('fresh_profile', _external=True))
return redirect(url_for('register', _external=True))
else:
# user doesn't exist in doors nor comex_db
# (shouldn't happen since client-side blocks submit and displays same message, but still possible if user tweaks the js)
return render_template(
"message.html",
message = """
We're sorry but you don't exist in our database yet!
<br/>
However you can easily <strong><a href="%s">register here</a></strong>.
""" % url_for('register', _external=True)
)
# /services/user/profile/
@app.route(config['PREFIX'] + config['USR_ROUTE'] + '/profile/', methods=['GET'])
@app.route(config['PREFIX'] + config['USR_ROUTE'] + '/profile/', methods=['GET', 'POST'])
@fresh_login_required
def profile():
"""
......@@ -248,28 +267,54 @@ def profile():
@login_required uses flask_login to relay User object current_user
"""
if request.method == 'GET':
# login provides us current_user
if current_user.empty:
mlog("INFO", "PROFILE: empty current_user %s" % current_user.uid)
else:
mlog("INFO", "PROFILE: current_user %s\n -" % current_user.uid
+ '\n -'.join([current_user.info['email'],
current_user.info['initials'],
str(current_user.info['keywords']),
current_user.info['country']]
)
)
# debug session cookies
# print("[k for k in session.keys()]",[k for k in session.keys()])
mlog("DEBUG", "PROFILE view with flag session.new = ", session.new)
# login provides us current_user
if current_user.empty:
mlog("INFO", "PROFILE: empty current_user %s" % current_user.uid)
else:
mlog("INFO", "PROFILE: current_user %s\n -" % current_user.uid
+ '\n -'.join([current_user.info['email'],
current_user.info['initials'],
str(current_user.info['keywords']),
current_user.info['country']]
)
)
# debug session cookies
# print("[k for k in session.keys()]",[k for k in session.keys()])
mlog("DEBUG", "PROFILE view with flag session.new = ", session.new)
return render_template(
"profile.html",
return render_template(
"profile.html"
# NB we also got user info in {{current_user.info}}
# and {{current_user.json_info}}
)
# doors info only for link
doors_connect=config['DOORS_HOST']+':'+config['DOORS_PORT']
# NB we also got user info in {{current_user.info}}
# and {{current_user.json_info}}
)
elif request.method == 'POST':
try:
save_form(
request.form,
request.files if hasattr(request, "files") else {},
update_flag = True
)
except Exception as perr:
return render_template("thank_you.html",
form_accepted = False,
backend_error = True,
message = ("ERROR ("+str(perr.__doc__)+"):<br/>"
+ ("<br/>".join(format_tb(perr.__traceback__)+[repr(perr)]))
)
)
return render_template("thank_you.html",
debug_records = (clean_records if app.config['DEBUG'] else {}),
form_accepted = True,
backend_error = False,
message = "")
# /services/user/register/
......@@ -305,49 +350,14 @@ def register():
mlog("INFO", "ok form accepted")
form_accepted = True
# only safe values
clean_records = {}
kw_array = []
# 1) handles all the inputs from form, no matter what target table
(duuid, rdate, kw_array, clean_records) = read_record(request.form)
# 2) handles the pic_file if present
if hasattr(request, "files") and 'pic_file' in request.files:
# type: werkzeug.datastructures.FileStorage.stream
pic_blob = request.files['pic_file'].stream.read()
if len(pic_blob) != 0:
clean_records['pic_file'] = pic_blob
# 3) save to DB
try:
# A) a new DB connection
reg_db = connect_db(config)
# B) read/fill the affiliation table to get associated id
clean_records['affiliation_id'] = get_or_create_affiliation(clean_records, reg_db)
# C) create record into the primary user table
# ---------------------------------------------
# TODO class User method !!
save_scholar(duuid, rdate, clean_records, reg_db)
# D) read/fill each keyword and save the (uid <=> kwid) pairings
kwids = get_or_create_keywords(kw_array, reg_db)
# TODO class User method !!
save_pairs_sch_kw([(duuid, kwid) for kwid in kwids], reg_db)
# clear cache concerning this scholar
# TODO class User method !!
if duuid in UCACHE: UCACHE.pop(duuid)
# E) end connection
reg_db.close()
clean_records = save_form(
request.form,
request.files if hasattr(request, "files") else {}
)
except Exception as perr:
return render_template("thank_you.html",
debug_records = clean_records,
form_accepted = False,
backend_error = True,
message = ("ERROR ("+str(perr.__doc__)+"):<br/>"
......@@ -363,6 +373,55 @@ def register():
########### SUBS ###########
def save_form(request_form, request_files, update_flag=False):
"""
wrapper function for save profile/register form actions
"""
# only safe values
clean_records = {}
kw_array = []
# 1) handles all the inputs from form, no matter what target table
(duuid, rdate, kw_array, clean_records) = read_record(request_form)
# 2) handles the pic_file if present
if 'pic_file' in request_files:
# type: werkzeug.datastructures.FileStorage.stream
pic_blob = request_files['pic_file'].stream.read()
if len(pic_blob) != 0:
clean_records['pic_file'] = pic_blob
# 3) save to DB
# A) a new DB connection
reg_db = connect_db(config)
# B) read/fill the affiliation table to get associated id
clean_records['affiliation_id'] = get_or_create_affiliation(clean_records, reg_db)
# C) create record into the primary user table
# ---------------------------------------------
# TODO class User method !!
save_scholar(duuid, rdate, clean_records, reg_db, update_flag=update_flag)
# D) read/fill each keyword and save the (uid <=> kwid) pairings
kwids = get_or_create_keywords(kw_array, reg_db)
# TODO class User method !!
# POSS selective delete ?
if update_flag:
delete_pairs_sch_kw(duuid, reg_db)
save_pairs_sch_kw([(duuid, kwid) for kwid in kwids], reg_db)
# clear cache concerning this scholar
# TODO class User method !!
if duuid in UCACHE: UCACHE.pop(duuid)
# E) end connection
reg_db.close()
return clean_records
def read_record(incoming_data):
"""
......
......@@ -2,6 +2,7 @@
.red { color:#910 ; }
.green { color:#161 ; }
.grey { color:#554 ; }
.orange { color:#F96 ; }
/* not used at present but could be useful for autompleted inputs */
.autocomp {
......@@ -46,6 +47,11 @@
display: none;
}
input.readonly {
font-weight: bold;
background-color: #CCC !important;
}
#cnil_warning {
/*text-align: center;*/
}
......@@ -143,10 +149,15 @@ h3.formcatfirst {
/* the main validation message (a special legend) */
#main_validation_message {
font-size: 200%;
#main_message {
color: white ;
font-size: 150%;
text-align:center;
/*color: #554 ;*/
background-color: #988 ;
border-radius: 2em;
padding: 1em 3em;
margin-bottom: 2em;
}
......@@ -154,6 +165,8 @@ ul.minilabels {
padding-top: .5em;
list-style-type: none;
font-size: 75% ;
text-align: left;
margin-left: 32%;
}
li.minilabel {
......
......@@ -14,22 +14,37 @@
*/
// initialize form controllers
cmxClt.uform.initialize("comex_profile_form", completionAsYouGo)
var isProfileComplete = false
var pleaseCompleteMessage = document.selectById("please_complete")
// main validation function
// ------------------------
function completionAsYouGo() {
cmxClt.uform.mainMessage.style.display = 'block'
cmxClt.uform.mainMessage.innerHTML = "Checking the answers..."
var missingColumns = []
var diagnostic = cmxClt.uform.testFillField(cmxClt.uform.theForm,
{'fixResidue': true})
function completionAsYouGo() {
var valid = true
var mandatoryMissingFields = []
var optionalMissingFields = []
[ valid,
mandatoryMissingFields,
optionalMissingFields
] = cmxClt.uform.testFillField(cmxClt.uform.theForm)
var valid = diagnostic[0]
var mandatoryMissingFields = diagnostic[1]
var optionalMissingFields = diagnostic[2]
if (valid) {
cmxClt.uform.mainMessage.innerHTML = "<span class='green glyphicon glyphicon-check' style='float:left;'></span>&nbsp;&nbsp;OK we have all the important fields!<br/>"
}
else {
cmxClt.uform.mainMessage.innerHTML = "<span class='red glyphicon glyphicon-warning-sign'></span>&nbsp;&nbsp;Sorry, there are some important missing fields<br/>"
}
// list of missing fields
cmxClt.uform.mainMessage.innerHTML += cmxClt.ulListFromLabelsArray(mandatoryMissingFields, ['red']) + cmxClt.ulListFromLabelsArray(optionalMissingFields, ['white'], "You may also want to fill:")
}
// run first check on existing profile data pre-filled by the template
completionAsYouGo()
console.log("profile controllers load OK")
......@@ -6,7 +6,6 @@
* + prepares DB save into cmxClt.COLS
*
* @todo
* - harmonize var names (eg 'cmxClt.uauth.email' vs 'initialsInput' are both input elts)
* - package.json
*
* @version 1
......@@ -18,27 +17,26 @@
*/
// initialize form controllers
cmxClt.uform.initialize("comex_reg_form", testAsYouGo) // our form is now in cmxClt.uform.theForm
cmxClt.uform.initialize("comex_reg_form", testAsYouGo)
// our form is now in cmxClt.uform.theForm
// initialize auth with doors
cmxClt.uauth.emailIdSupposedToExist = false
var jobLookingDateStatus = false
// done when anything in the form changes
function testAsYouGo() {
// console.log("testAsYouGo Go")
cmxClt.uauth.earlyValidate()
checkJobDateStatus()
// TODO add uform.testFillField (start checking after 3-4 filled fields)
cmxClt.uform.checkJobDateStatus()
if (cmxClt.uauth.passStatus
&& cmxClt.uauth.emailStatus
&& cmxClt.uauth.captchaStatus
&& jobLookingDateStatus) {
&& cmxClt.uform.jobLookingDateStatus) {
cmxClt.uform.submitButton.disabled = false
}
else {
......@@ -50,14 +48,13 @@ function testAsYouGo() {
var regTimestamp = document.getElementById('last_modified_date')
var subPage1Style = document.getElementById('subpage_1').style
var subPage2Style = document.getElementById('subpage_2').style
var teamCityDivStyle = document.getElementById('team_city_div').style
var otherInstDivStyle = document.getElementById('other_org_div').style
var jobLookingDivStyle = document.getElementById('job_looking_div').style
function registerDoorsAndSubmit(){
......@@ -68,11 +65,11 @@ function registerDoorsAndSubmit(){
var passValue = cmxClt.uauth.pass1.value
var wholenameValue = ""
if (mName.value != "") {
wholenameValue = lName.value + ', ' + fName.value + ' ' + mName.value
if (cmxClt.uform.mName.value != "") {
wholenameValue = cmxClt.uform.lName.value + ', ' + cmxClt.uform.fName.value + ' ' + cmxClt.uform.mName.value
}
else {
wholenameValue = lName.value + ', ' + fName.value
wholenameValue = cmxClt.uform.lName.value + ', ' + cmxClt.uform.fName.value
}
......@@ -127,12 +124,14 @@ function validateAndMsg() {
cmxClt.uform.mainMessage.style.display = 'block'
cmxClt.uform.mainMessage.innerHTML = "Validating the form..."
var valid = true
var missingFields = []
// runs field-by-field validation and highlights mandatory missing fields
var diagnostic = cmxClt.uform.testFillField(cmxClt.uform.theForm)
// +++++++++++++
[valid, missingFields] = cmxClt.uform.testFillField(cmxClt.uform.theForm)
// +++++++++++++
// RESULTS
var valid = diagnostic[0]
var missingFields = diagnostic[1]
if (valid) {
// adds the captchaCheck inside the form
ccModule.uauth.collectCaptcha()
......@@ -143,14 +142,11 @@ function validateAndMsg() {
return true
}
else {
console.warn("form is not valid")
cmxClt.uform.submitButton.disabled = false
var errorMessage = ''
// TODO highlight invalid fields
if (missingFields.length) {
errorMessage += "Please fill the missing fields: " + cmxClt.ulListFromLabelsArray(missingFields, ["red"])
errorMessage = cmxClt.ulListFromLabelsArray(missingFields, ["red"], "Please fill the missing fields: ")
}
// length is handled by each input's maxlength
......@@ -162,146 +158,6 @@ function validateAndMsg() {
}
var fileInput = document.getElementById('pic_file')
var showPicImg = document.getElementById('show_pic')
var boxShowPicImg = document.getElementById('box_show_pic')
var picMsg = document.getElementById('picture_message')
var imgReader = new FileReader();
function checkShowPic() {
// TEMPORARY initial size already 200 kB, user has to do it himself
var max_size = 204800
// TODO max source image size before resizing
// see libs or stackoverflow.com/a/24015367
// 4 MB
// always reset style and width/height calculations
boxShowPicImg.style.display = 'none'
showPicImg.style.display = ""
showPicImg.style.width = ""
showPicImg.style.height = ""
// var max_size = 4194304
if (fileInput.files) {
var theFile = fileInput.files[0]
// debug
console.log(theFile.name, "size", theFile.size, theFile.lastModifiedDate)
if (theFile.size > max_size) {
// msg pb
picMsg.innerHTML = "The picture is too big (200kB max)!"
picMsg.style.color = cmxClt.colorRed
}
else {
// msg ok
picMsg.innerHTML = "Picture ok"
picMsg.style.color = cmxClt.colorGreen
// to show the pic when readAsDataURL
imgReader.onload = function () {
showPicImg.src = imgReader.result;
// prepare max size while preserving ratio
var imgW = window.getComputedStyle(showPicImg).getPropertyValue("width")
var imgH = window.getComputedStyle(showPicImg).getPropertyValue("height")
console.log("img wid", imgW)
console.log("img hei", imgH)
if (imgW > imgH) {
showPicImg.style.width = "100%"
showPicImg.style.height = "auto"
}
else {
showPicImg.style.width = "auto"
showPicImg.style.height = "100%"
}
// now reaadjust outer box and show
boxShowPicImg.style.display = 'block'
// possible re-adjust outerbox ?
// showPicImg.style.border = "2px dashed " + cmxClient.colorGrey
}
// create fake src url & trigger the onload
imgReader.readAsDataURL(theFile);
}
}
else {
console.warn("skipping testPictureBlob called w/o picture in fileInput")
}
}
// show middlename button binding
var mnBtn = document.getElementById('btn-midname')
mnBtn.onclick= function() {
var mnDiv = document.getElementById('group-midname')
if (mnDiv.style.display == 'none') {
mnDiv.style.display = 'table'
}
else {
mnDiv.style.display = 'none'
}
}
// first, middle & last name ~~~> initials
var fName = document.getElementById('first_name')
var mName = document.getElementById('middle_name')
var lName = document.getElementById('last_name')
var initialsInput = document.getElementById('initials')
var nameInputs = [fName, mName, lName]
nameInputs.forEach ( function(nameInput) {
nameInput.onchange = function () {
var apparentInitials = ""
nameInputs.forEach ( function(nameInput) {
var txt = nameInput.value
if (txt.length) {
if(/[A-Z]/.test(txt)) {
var capsArr = txt.match(/[A-Z]/g)
for (var i in capsArr) {
apparentInitials += capsArr[i]
}
}
else {
apparentInitials += txt.charAt(0)
}
}
}) ;
// update the displayed value
initialsInput.value = apparentInitials
}
})
// jobLookingDateStatus ~~~> is job date a valid date?
var jobBool = document.getElementById('job_bool')
var jobDate = document.getElementById('job_looking_date')
var jobDateMsg = document.getElementById('job_date_message')
jobDate.onkeyup = checkJobDateStatus
jobDate.onchange = checkJobDateStatus
function checkJobDateStatus() {
jobLookingDateStatus = (jobBool.value == "No" || cmxClt.uform.validDate.test(jobDate.value))
if (!jobLookingDateStatus) {
jobDateMsg.style.color = cmxClt.colorRed
jobDateMsg.innerHTML = 'Date is not yet in the valid format YYYY/MM/DD'
}
else {
jobDateMsg.style.color = cmxClt.colorGreen
jobDateMsg.innerHTML = 'Ok valid date!'
}
}
// £TODO move autocomp data to an autocomplete module
// -> local data for countries, jobtitles
// -> use ajax aggs api for the scholars, kws and labs
......@@ -912,14 +768,14 @@ $(function() {
console.log("reg controllers load OK")
// £DEBUG autofill ----------->8------
// fName.value = "Jean"
// lName.value = "Tartampion"
// initialsInput.value="JPP"
// cmxClt.uform.fName.value = "Jean"
// cmxClt.uform.lName.value = "Tartampion"
// document.getElementById('initials').value="JPP"
// document.getElementById('country').value = "France"
// document.getElementById('position').value = "atitle"
// document.getElementById('keywords').value = "Blabla"
// document.getElementById('org').value = "CNRS"
//
// cmxClt.uauth.email.value= cmxClt.makeRandomString(7)+"@om.fr"
// cmxClt.uauth.pass1.value="123456+789"
// cmxClt.uauth.pass2.value="123456+789"
......
......@@ -23,42 +23,45 @@ var cmxClt = (function() {
ccModule.colorRed = '#910'
ccModule.colorGreen = '#161'
ccModule.colorGrey = '#554'
// the target columns in DB: tuple (name, mandatoryBool, type)
ccModule.COLS = [ ["doors_uid", true, "auto" ],
["last_modified_date", true, "auto" ],
["email", true, "plsfill"],
["country", true, "plsfill"],
["first_name", true, "plsfill"],
["middle_name", false, "plsfill"],
["last_name", true, "plsfill"],
["initials", true, "plsfill"],
["position", false, "plsfill"],
["hon_title", false, "plsfill"],
["interests_text", false, "plsfill"],
["community_hashtags", false, "plsfill"],
["gender", false, "plsfill"],
["job_looking_date", false, "pref" ],
["home_url", false, "plsfill"],
["pic_url", false, "pref" ],
["pic_file", false, "pref" ],
ccModule.colorOrange = '#F96'
// the target columns in DB: tuple (name, mandatoryBool, group, type)
ccModule.COLS = [ ["doors_uid", true, "auto" , "t"],
["last_modified_date", true, "auto" , "d"],
["email", true, "plsfill", "t"],
["country", true, "plsfill", "t"],
["first_name", true, "plsfill", "t"],
["middle_name", false, "pref", "t"],
["last_name", true, "plsfill", "t"],
["initials", true, "plsfill", "t"],
["position", true, "plsfill", "t"],
["hon_title", false, "plsfill", "t"],
["interests_text", false, "plsfill", "t"],
["community_hashtags", false, "plsfill", "at"],
["gender", false, "plsfill", "m"],
["job_looking_date", false, "pref" , "d"],
["home_url", false, "plsfill", "t"],
["pic_url", false, "pref" , "t"],
["pic_file", false, "pref" , "f"],
// ==> *scholars* table
["keywords", true, "plsfill"],
["keywords", true, "plsfill", "at"],
// ==> *keywords* table
["org", true, "plsfill"],
["org_type", true, "plsfill"],
["team_lab", false, "pref" ],
["org_city", false, "pref" ]]
["org", true, "plsfill", "t"],
["org_type", true, "plsfill", "m"],
["team_lab", false, "pref" , "t"],
["org_city", false, "pref" , "t"]]
// ==> *affiliations* table
// "type" is a complementary information to mandatory
// --------------------------------------------------
// type "auto" === filled by controllers
// type "plsfill" === filled by user, ideally needed for a complete profile
// type "pref" === filled by user but not needed at all
// group "auto" === filled by controllers
// group "plsfill" === filled by user, ideally needed for a complete profile
// group "pref" === filled by user but not needed at all
ccModule.miniSanitize = function(aString) {
return aString.replace(/[^A-z0-9, :\(\)-]/, ' ').replace(/^ +| +$/, '')
}
ccModule.makeRandomString = function (nChars) {
var rando = ""
......@@ -69,12 +72,17 @@ var cmxClt = (function() {
return rando
}
ccModule.ulListFromLabelsArray = function (strArray, ulClassList) {
ccModule.ulListFromLabelsArray = function (cplArray, ulClassList, message) {
ulClasses=["minilabels"].concat(ulClassList).join(" ")
var resultHtml = '<ul class="'+ulClasses+'">'
for (var i in strArray) {
var label = strArray[i].replace(/_/, " ")
resultHtml += '<li class="minilabel">'+label+'</li>'
var resultHtml = ""
if (message) {
resultHtml = ccModule.miniSanitize(message)
}
resultHtml += '<ul class="'+ulClasses+'">'
for (var i in cplArray) {
var fname = cplArray[i][0]
var flabel = cplArray[i][1]
resultHtml += '<li class="minilabel">'+flabel+'</li>'
}
resultHtml += '</ul>'
return resultHtml
......@@ -91,15 +99,29 @@ var cmxClt = (function() {
}
// common vars to user forms
// ===============================
// common vars to all user forms
// ===============================
// exposed functions and vars that will be used during the interaction
ccModule.uform = {}
ccModule.uform.theFormId = null
ccModule.uform.theForm = null
// vars that will be used during the interaction
ccModule.uform.submitButton = document.getElementById('formsubmit')
ccModule.uform.mainMessage = document.getElementById('main_validation_message')
ccModule.uform.initialize
ccModule.uform.testFillField
ccModule.uform.mainMessage = document.getElementById('main_message')
ccModule.uform.submitButton = document.getElementById('form_submit')
// dates up to 2049/12/31
ccModule.uform.validDate = new RegExp( /^20[0-4][0-9]\/(?:0?[1-9]|1[0-2])\/(?:0?[1-9]|[1-2][0-9]|3[0-1])$/)
// function definitions
// =====================
// initialize
// -----------
ccModule.uform.initialize = function(aFormId, aValidationFun) {
ccModule.uform.theFormId = aFormId
ccModule.uform.theForm = document.getElementById(aFormId)
......@@ -107,16 +129,14 @@ var cmxClt = (function() {
ccModule.uform.theForm.onkeyup = aValidationFun
ccModule.uform.theForm.onchange = aValidationFun
ccModule.uform.theForm.onblur = aValidationFun
}
// dates up to 2049/12/31
ccModule.uform.validDate = new RegExp( /^20[0-4][0-9]\/(?:0?[1-9]|1[0-2])\/(?:0?[1-9]|[1-2][0-9]|3[0-1])$/)
// testFillField
// --------------
// checks if mandatory fields are filled
// checks if other plsfill ones are filled
ccModule.uform.testFillField = function (aForm) {
// highlights labels of missing mandatory fields
ccModule.uform.testFillField = function (aForm, params) {
// "private" copy
var wholeFormData = new FormData(aForm)
......@@ -124,47 +144,80 @@ var cmxClt = (function() {
var valid = true
var mandatoryMissingFields = []
var otherMissingFields = []
// var toolongFields = []
// default params
if (!params) params = {}
if (params.doHighlight == undefined) params.doHighlight = true
if (params.fixResidue == undefined) params.fixResidue = false
// let's go
for (var i in ccModule.COLS) {
// console.warn("checking ccModule.COLS["+i+"]", ccModule.COLS[i])
// console.info("testFillField COLS["+i+"]", ccModule.COLS[i])
var fieldName = ccModule.COLS[i][0]
var mandatory = ccModule.COLS[i][1]
var fieldType = ccModule.COLS[i][2]
var fieldGroup = ccModule.COLS[i][2]
var fieldType = ccModule.COLS[i][3]
// skip non-plsfill elements
if (fieldName != 'plsfill') continue ;
if (fieldGroup != 'plsfill') continue ;
var actualValue = wholeFormData.get(fieldName)
// get a human-readable label
var labelElt = document.querySelector('label[for='+fieldName+']')
var fieldLabel = labelElt ? labelElt.innerText : fieldName
// alternative null values
if (actualValue == "" || actualValue == "None") {
if (actualValue == "") {
actualValue = null
}
// python residue ~~~> can correct on the fly
// POSS better strategy ?
if (params.fixResidue) {
// "None" as a string
if (actualValue == "None") {
actualValue = null
document.getElementById(fieldName).value = ""
}
// arrays of text
if (fieldType == "at" && actualValue
&& actualValue.charAt(0) == '['
&& actualValue.charAt(1) == "'") {
actualValue = actualValue.replace(/[\[\]']/g,'')
document.getElementById(fieldName).value = actualValue
}
}
// debug
// console.log(
// "cmxClt.testEachField: field", fieldName,
// "cmxClt.testFillField: field", fieldName,
// "actualValue:", actualValue
// )
// test mandatory -----------------
if (mandatory && actualValue == null) {
// todo human-readable fieldName here
mandatoryMissingFields.push(fieldName)
mandatoryMissingFields.push([fieldName, fieldLabel])
valid = false
console.log("mandatoryMissingFields", fieldName)
// console.log("mandatoryMissingFields", fieldName)
if (params.doHighlight) {
labelElt.style.backgroundColor = ccModule.colorOrange
}
}
// test benign --------------------
// test benign
// may be missing but doesn't affect valid
else if (actualValue == null) {
otherMissingFields.push(fieldName)
console.log("otherMissingField", fieldName)
otherMissingFields.push([fieldName, fieldLabel])
// console.log("otherMissingField", fieldName)
}
// --------------------------------
else if (params.doHighlight) {
labelElt.style.backgroundColor = ""
}
} // end for val in ccModule.COLS
// return full form diagnostic and field census
......@@ -173,7 +226,188 @@ var cmxClt = (function() {
otherMissingFields ]
}
// ===================================================================
// additional controllers for detailed forms like /register, /profile
// ===================================================================
// exposed functions and vars
ccModule.uform.checkShowPic
ccModule.uform.createInitials
ccModule.uform.checkJobDateStatus
ccModule.uform.fName = document.getElementById('first_name')
ccModule.uform.mName = document.getElementById('middle_name')
ccModule.uform.lName = document.getElementById('last_name')
ccModule.uform.jobLookingDateStatus = false
// function definitions, private vars and event handlers
// ======================================================
// image fileInput ~~~> display image
// ----------------------------------
var fileInput = document.getElementById('pic_file')
var showPicImg = document.getElementById('show_pic')
var boxShowPicImg = document.getElementById('box_show_pic')
var picMsg = document.getElementById('picture_message')
var imgReader = new FileReader();
ccModule.uform.checkShowPic = function (aForm, doHighlight) {
// TEMPORARY initial size already 200 kB, user has to do it himself
var max_size = 204800
// TODO max source image size before resizing
// see libs or stackoverflow.com/a/24015367
// 4 MB
// var max_size = 4194304
// always reset style and width/height calculations
boxShowPicImg.style.display = 'none'
showPicImg.style.display = ""
showPicImg.style.width = ""
showPicImg.style.height = ""
if (fileInput.files) {
var theFile = fileInput.files[0]
// debug
console.log(theFile.name, "size", theFile.size, theFile.lastModifiedDate)
if (theFile.size > max_size) {
// msg pb
picMsg.innerHTML = "The picture is too big (200kB max)!"
picMsg.style.color = cmxClt.colorRed
}
else {
// msg ok
picMsg.innerHTML = "Picture ok"
picMsg.style.color = cmxClt.colorGreen
// to show the pic when readAsDataURL
imgReader.onload = function () {
showPicImg.src = imgReader.result;
// prepare max size while preserving ratio
var realValues = window.getComputedStyle(showPicImg)
var imgW = realValues.getPropertyValue("width")
var imgH = realValues.getPropertyValue("height")
// debug
// console.log("img wid", imgW)
// console.log("img hei", imgH)
if (imgW > imgH) {
showPicImg.style.width = "100%"
showPicImg.style.height = "auto"
}
else {
showPicImg.style.width = "auto"
showPicImg.style.height = "100%"
}
// now show it
boxShowPicImg.style.display = 'block'
// possible re-adjust outerbox ?
}
// create fake src url & trigger the onload
imgReader.readAsDataURL(theFile);
}
}
else {
console.warn("skipping testPictureBlob called w/o picture in fileInput")
}
}
// first, middle & last name ~~~> initials
// ----------------------------------------
var nameInputs = [ccModule.uform.fName,
ccModule.uform.mName,
ccModule.uform.lName]
var initialsInput = document.getElementById('initials')
ccModule.uform.createInitials = function() {
var apparentInitials = ""
nameInputs.forEach ( function(nameInput) {
var txt = nameInput.value
if (txt.length) {
if(/[A-Z]/.test(txt)) {
var capsArr = txt.match(/[A-Z]/g)
for (var i in capsArr) {
apparentInitials += capsArr[i]
}
}
else {
apparentInitials += txt.charAt(0)
}
}
}) ;
// update the displayed value
initialsInput.value = apparentInitials
}
// handlers: names to initials
nameInputs.forEach ( function(nameInput) {
if (nameInput) {
nameInput.onkeyup = ccModule.uform.createInitials
nameInput.onchange = ccModule.uform.createInitials
}
})
// handler: show middlename button
var mnBtn = document.getElementById('btn-midname')
if(mnBtn) {
mnBtn.onclick= function() {
var mnDiv = document.getElementById('group-midname')
if (mnDiv.style.display == 'none') {
mnDiv.style.display = 'table'
}
else {
mnDiv.style.display = 'none'
}
}
}
// jobLookingDateStatus ~~~> is job date a valid date?
// ---------------------------------------------------
var jobBool = document.getElementById('job_bool')
var jobDate = document.getElementById('job_looking_date')
var jobDateMsg = document.getElementById('job_date_message')
var jobLookingDiv = document.getElementById('job_looking_div')
ccModule.uform.checkJobDateStatus = function () {
ccModule.uform.jobLookingDateStatus = (jobBool.value == "No" || ccModule.uform.validDate.test(jobDate.value))
if (!ccModule.uform.jobLookingDateStatus) {
jobDateMsg.style.color = cmxClt.colorRed
jobDateMsg.innerHTML = 'Date is not yet in the valid format YYYY/MM/DD'
}
else {
jobDateMsg.style.color = cmxClt.colorGreen
jobDateMsg.innerHTML = 'Ok valid date!'
}
}
// handler: show jobLookingDiv
if (jobBool && jobDate) {
jobBool.onchange = function() {
if(this.value=='Yes'){
jobLookingDiv.style.display = 'block'
}
else {
jobLookingDiv.style.display='none'
jobDate.value=''
}
}
jobDate.onkeyup = ccModule.uform.checkJobDateStatus
jobDate.onchange = ccModule.uform.checkJobDateStatus
}
// ========= end of advanced form controls ===========
return ccModule
}()) ;
console.log("shared load OK")
console.log("user shared load OK")
......@@ -96,7 +96,7 @@ cmxClt = (function(ccModule) {
// tests if email is well-formed
// TODO: better extension and allowed chars set
var emailFormatOk = /^[-A-z0-9_=.+]+@[-A-z0-9_=.+]+\.[-A-z0-9_=.+]{1,4}$/.test(emailValue)
var emailFormatOk = /^[-A-z0-9_=.+]+@[-A-z0-9_=.+]+\.[-A-z0-9_=.+]{2,4}$/.test(emailValue)
if (! emailFormatOk) {
// restore original lack of message
......@@ -149,7 +149,7 @@ cmxClt = (function(ccModule) {
ccModule.uauth.doorsMessage.style.color = ccModule.colorGreen
// label
ccModule.uauth.emailLbl.style.color = ccModule.colorGreen
ccModule.uauth.emailLbl.style.backgroundColor = ""
}
else {
var errMsg = expectExists ? "your ID isn't recognized" : "this ID is already taken"
......@@ -165,7 +165,7 @@ cmxClt = (function(ccModule) {
ccModule.uauth.doorsMessage.style.color = ccModule.colorRed
// label
ccModule.uauth.emailLbl.style.backgroundColor = ccModule.colorRed
ccModule.uauth.emailLbl.style.backgroundColor = ccModule.colorOrange
}
// to debounce re-invocations
......
......@@ -77,7 +77,7 @@
<!-- main validation message -->
<p id="main_validation_message" class="legend" style="display:none"></p>
<p id="main_message" class="legend" style="display:none"></p>
<!--pseudo captcha using realperson from http://keith-wood.name/realPerson.html -->
......@@ -93,7 +93,7 @@
<!-- normal submit button
(login to doors will happen b/w servers: no need to do it by ajax) -->
<button class="btn btn-lg btn-success" style="float:right"
id="formsubmit" disabled>
id="form_submit" disabled>
LOGIN
</button>
......
......@@ -18,27 +18,15 @@
<div id="intro">
<h2 class="oldstyle">Your Profile Info</h2>
<p class="mini-hero">
{% if current_user.is_authenticated %}
Welcome to your <strong>Community explorer</strong> profile,
{% if current_user.info.hon_title is not none %}
{{ current_user.info.hon_title }}
{% endif %}
{{ current_user.info.last_name }}!
{% endif %}
</p>
</div>
<!-- FORM COMPLETING INTRO TEXT -->
<div id="intro">
<h2 id="please_complete" class="oldstyle">Please complete your profile</h2>
<p class="mini-hero">
Take the time to complete your <strong>Community Explorer</strong> profile to generate better maps of your own socio-semantic network .
<br/>
<br/>
The <strong>keywords</strong>, <strong>affiliations</strong> and <strong>hashtags</strong> will all count as similarities to identify network neighbours.
Welcome to your profile page,
<strong>
{% if current_user.info.hon_title is not none %}
{{ current_user.info.hon_title }}
{% endif %}
{{ current_user.info.last_name }}
</strong> !
<br/>
<br/>
The <strong>home page (url)</strong>, <strong>picture (url or file)</strong> and <strong>free description</strong> will allow your neighbours to know you a little better.
Take the time to complete it to generate better maps of your socio-semantic network .
</p>
</div>
......@@ -48,6 +36,21 @@
<!-- todo onsubmit also save to cache -->
<!-- EMAIL & PASSWORD -->
<h3 class="formcat"> Login infos </h3>
<p class="mini-hero">
This email is your main ID. You may only change it via the <a href="http://{{ doors_connect }}/">Doors portal</a>.
</p>
<!-- readonly version for current profile -->
<div class="question input-group">
<label for="email" class="smlabel input-group-addon">* Email</label>
<input id="email" name="email" maxlength="255" readonly
type="text" class="form-control readonly" placeholder="email"
value="{{ current_user.info.email }}">
</div>
<!-- NAME & COUNTRY -->
<h3 class="formcatfirst"> Basic infos </h3>
......@@ -107,33 +110,13 @@
value="{{ current_user.info.country }}">
</div>
<!-- EMAIL & PASSWORD -->
<h3 class="formcat"> Login infos </h3>
<div class="question">
<p class="legend">Your email will also be your login for the ISC services.</p>
<div class="input-group">
<!-- email validation onblur/onchange is done by cmxClt.uauth in comex_user_shared_auth.js -->
<label for="email" class="smlabel input-group-addon">* Email</label>
<input id="email" name="email" maxlength="255"
type="text" class="form-control" placeholder="email"
value="{{ current_user.info.email }}">
<!-- doors return value icon -->
<div id="doors_ret_icon_msg" class="input-group-addon"
title="The email will be checked in our DB after you type and leave the box.">
<span id="doors_ret_icon"
class="glyphicon glyphicon-question-sign grey"
></span>
</div>
</div>
<!-- doors return value message -->
<p id="doors_ret_message" class="legend"></p>
</div>
<!-- JOB & INTERESTS -->
<!-- JOB, INTERESTS AND ORGANIZATION -->
<h3 class="formcat"> About your job and research </h3>
<p class="mini-hero">
The <strong>keywords</strong> and <strong>affiliation</strong> will count as similarities to identify your network neighbours.
</p>
<div class="question input-group">
<label for="hon_title" class="smlabel input-group-addon"> Title </label>
<input id="hon_title" name="hon_title" maxlength="30"
......@@ -160,9 +143,6 @@
<p class="legend">Please enter at least 5 comma-separated keywords</p>
</div>
<!-- ORGANIZATION -->
<h3 class="formcat"> About your current organization </h3>
<div class="question">
<div class="input-group">
<label for="org" class="smlabel input-group-addon">* Parent Institution</label>
......@@ -223,6 +203,11 @@
<!-- OTHER PERSONAL INFO -->
<h3 class="formcatfirst"> Complementary information </h3>
<p class="mini-hero">
The <strong>home page (url)</strong>, <strong>picture (url or file)</strong> and <strong>free description</strong> will allow your neighbours to know you a little better.
Also, our <strong>interest groups</strong> are each linked to an ISCPIF mailing list that you can subscribe to.
</p>
<div class="question">
<div class="input-group">
<label for="gender" class="smlabel input-group-addon">Gender</label>
......@@ -321,8 +306,43 @@
</div>
</div>
<!-- CNIL WARNING -->
<!-- hidden input for modification date -->
<input id="last_modified_date" name="last_modified_date" type="text" hidden>
</input>
<!-- hidden input for doors user id -->
<input id="doors_uid" name="doors_uid" type="text" hidden
value="{{ current_user.uid | safe }}">
</input>
<p> TEST UID {{ current_user.uid | safe }} </p>
<div class="row spacerrow">&nbsp;</div>
<!-- main validation message -->
<p id="main_message" class="cartouche legend" style="display:none"></p>
<!-- == S U B M I T == -->
<div style="text-align:center">
<!-- no @type => implicit @type=submit -->
<button class="btn btn-lg btn-success" id="form_submit">
Save profile
</button>
</div>
</form>
</div>
<div class="spacer col-sm-2 col-md-2">&nbsp;</div>
</div>
<!-- CNIL WARNING -->
<div class="row spacerrow">&nbsp;</div>
<div class="row spacerrow">&nbsp;</div>
<div class="row">
<div class="spacer col-sm-1 col-md-1">&nbsp;</div>
<div class="col-sm-8 col-md-8">
<h3 class="formcat"> About your data </h3>
<div class="cartouche" id="cnil_warning">
<p>Les informations recueillies à partir de ce formulaire font l’objet d’un traitement
......@@ -348,34 +368,6 @@
consultez vos droits sur le site de la CNIL</a>.
</p>
</div>
<!-- hidden input for modification date -->
<input id="last_modified_date" name="last_modified_date" type="text" hidden>
</input>
<!-- hidden input for doors user id -->
<input id="doors_uid" name="doors_uid" type="text" hidden>
</input>
<div class="row spacerrow">&nbsp;</div>
<!-- main validation message -->
<p id="main_validation_message" class="legend" style="display:none"></p>
<!-- == S U B M I T == -->
<h3 class="formcat">Save profile</h3>
<!-- button instead of input.submit to validate before real submit action -->
<button class="btn btn-lg btn-success" style="float:right"
id="formsubmit" type=button
onclick="if (validateAndMsg(event)) {registerDoorsAndSubmit()}">
Save profile
</button>
</form>
</div>
<div class="spacer col-sm-2 col-md-2">&nbsp;</div>
</div>
......
......@@ -186,7 +186,8 @@
<select id="org_type" name="org_type"
class="custom-select form-control"
onchange="if(this.value=='other'){otherInstDivStyle.display = 'block'} else {otherInstDivStyle.display='none'}">
<option selected value="university">University</option>
<option selected disabled value="">Please select</option>
<option value="university">University</option>
<option value="public R&amp;D org">Public sector R&amp;D organization</option>
<option value="public other org">Other public sector organization</option>
<option value="private org">Private sector organization</option>
......@@ -268,7 +269,7 @@
<label for="pic_file" class="smlabel input-group-addon">Picture</label>
<input type="file" id="pic_file" name="pic_file"
accept="image/png,image/jpeg" class="form-control"
onchange="checkShowPic(this)">
onchange="cmxClt.uform.checkShowPic(this)">
</div>
<p id="picture_message" class="legend red" style="font-weight:bold"></p>
</div>
......@@ -305,8 +306,7 @@
<div class="input-group">
<label for="job_bool" class="smlabel input-group-addon">Looking for a job?</label>
<select id="job_bool" name="job_bool"
class="custom-select form-control"
onchange="if(this.value=='Yes'){jobLookingDivStyle.display = 'block'} else {jobLookingDivStyle.display='none';jobDate.value=''}">
class="custom-select form-control">
<option selected value="No" onclick="jobDate.value=''">No</option>
<option value="Yes" onclick="jobLookingDivStyle.display = 'block'">Yes</option>
</select>
......@@ -382,7 +382,7 @@
<!-- main validation message -->
<p id="main_validation_message" class="legend" style="display:none"></p>
<p id="main_message" class="legend" style="display:none"></p>
<!-- CAPTCHA & SUBMIT BTN + INFOS-->
......@@ -402,7 +402,7 @@
<!-- button instead of input.submit to validate before real submit action -->
<!-- also remember stackoverflow.com/a/3315016/2489184 -->
<button class="btn btn-lg btn-success" style="float:right"
id="formsubmit" type=button disabled
id="form_submit" type=button disabled
onclick="if (validateAndMsg(event)) {registerDoorsAndSubmit()}">
Submit your form
</button>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment