1# 2# ver : 1.2 || date: Oct 17, 2018 || auth: PA Taylor 3# + separate title and text strings 4# + new warn type 5# 6# ver : 1.3 || date: Nov 1, 2018 7# + [PT] wrap_imag now includes href, so clicking on image opens it in 8# new link 9# 10# ver : 1.4 || date: Nov 1, 2018 11# + [PT] Making py3 compatible: 12# from 2to3, updating DICT.has_key(x) -> x in DICT 13# 14# ver : 1.5 || date: Feb 21, 2019 15# + [PT] adding comment to empty button sets it to "?", not "X" 16# 17#ver = '2.21' ; date = 'May 17, 2019' 18# + [PT] simplifying radcor behavior 19# 20#ver = '2.3' ; date = 'July 3, 2019' 21# + [PT] Colorbars standard widths 22# + [PT] QC block ID now in QC block titles 23# + [PT] added more help descriptions 24# 25#ver = '2.4' ; date = 'March 27, 2020' 26# [PT] remove dependency on lib_apqc_html_helps.py 27# + absorb those lahh.* functions and variables here 28# 29ver = '2.5' ; date = 'Feb 23, 2021' 30# [PT] update helps, reorder 31# 32######################################################################### 33 34 35# mostly ways to read & wrap text & images. 36import sys 37import os 38import json 39import collections as coll 40 41from afnipy import lib_apqc_html_css as lahc 42 43NULL_BTN1 = '' # empty space, used to keep button populated 44NULL_BTN0 = '|' # empty space, used to keep button populated 45 46json_cols = [ 'qcele', 'rating', 'comment' ] 47PBAR_FLAG = 'SHOW_PBAR' 48dir_img = 'media' 49 50## ========================================================================== 51## ========================================================================== 52## ========================================================================== 53## ========================================================================== 54## ========================================================================== 55 56# --------- Section on what used to be library of "apqc html helps" --------- 57 58# For single subj QC, we now identify major groupings/categories to 59# look at: QC blocks. Those will determine general organization 60# (sections, section titles, etc.) and guide usage. 61# 62# Format of each entry: 63# qc_block[ ABBREV ] = [ "SECTION HOVER TEXT", "SECTION TITLE" ] 64# 65# For organizational purposes, these quantity abbrevs are/should be 66# used in the names of functions used to generate each section. (Makes 67# it easier to see what is connected to what, even if they change.) 68 69qc_title = coll.OrderedDict() 70qc_title["Top"] = [ "Top of page for:
${subj}", 71 "afni_proc.py single subject report" ] 72 73qc_blocks = coll.OrderedDict() 74qc_blocks["vorig"] = [ "vols in orig space", 75 "Check vols in original space" ] 76 77qc_blocks["ve2a" ] = [ "vol alignment (EPI-anat)", 78 "Check vol alignment (EPI to anat)" ] 79 80qc_blocks["va2t" ] = [ "vol alignment (anat-template)", 81 "Check vol alignment (anat to template)" ] 82 83qc_blocks["vstat"] = [ "statistics vols", 84 "Check statistics vols (and effect estimates)" ] 85 86qc_blocks["mot" ] = [ "motion and outliers", 87 "Check motion and outliers" ] 88 89qc_blocks["regr" ] = [ "regressors", 90 "Check regressors, DFs and residuals" ] 91 92qc_blocks["radcor"] = [ "@radial_correlate vols", 93 "Check extent of local correlation" ] 94 95qc_blocks["warns"] = [ "all warnings from processing", 96 "Check all warnings from processing" ] 97 98qc_blocks["qsumm"] = [ "summary quantities from @ss_review_basic", 99 "Check summary quantities from @ss_review_basic" ] 100 101qc_link_final = [ "FINAL", 102 "overall subject rating" ] 103 104# ------------------------------ 105 106# a brief help string for online help 107qcb_helps = coll.OrderedDict() 108qcb_helps["vorig"] = ''' 109Volumetric mages of data (EPI and anat) in original/native space. 110 111(EPI should now be shown; anat vol will be along shortly.) 112''' 113 114qcb_helps["ve2a" ] = ''' 115Volumetric images of the alignment of the subject's anat 116(underlay/grayscale) and EPI (overlay/hot color edges) volumes. Likely 117these will be shown in the template space, if using the tlrc block. 118''' 119 120qcb_helps["va2t" ] = ''' 121Volumetric images of the alignment of the standard space template 122(underlay/grayscale) and subject's anat (overlay/hot color edges) 123volumes. 124''' 125 126qcb_helps["vstat"] = ''' 127Volumetric images of statistics results (and, where available, effect 128estimates). These images are only created for task data sets, i.e., 129where GLTs or stimuli are specified (so not for resting state data). 130 131By default, the (full) F-stat of an overall regression model is shown. 132Additionally, one can specify labels of stimuli or GLTs used in the 133afni_proc.py command, and statistical results will be shown. For 134stimuli with effect estimates, the 'Coef' vales will be displayed as 135the olay colors (preferably with the 'scale' block having been used in 136afni_proc.py, producing meaningful units of BOLD % signal change in 137the 'Coef' volumes). 138 139Colorbar ranges and thresholds are chosen from either percentile 140values within the data set (preferably from within a WB mask, 141available when the 'mask' block was used in afni_proc.py) or from 142pre-set statistical levels (like p=0.001). Each is case is described. 143 144''' 145 146qcb_helps["mot" ] = ''' 147Summary of motion and outlier information, which may each/both be 148used as censoring criteria. 149 150The 6 rigid body motion parameters (3 rotation + 3 translation) are 151combined into a single quantity: the Euclidean norm (enorm), which has 152approx. units of 'mm'. Large changes in the enorm time series show 153moments of subject motion. 154 155Separate runs are shown with the background alternating between white 156and light gray. 157 158Boxplots summarize parameter values, both before censoring (BC) and 159after censoring (AC). 160 161And a grayplot of residuals (with motion/outliers/censoring) is 162provided. The '-pvorder' is used for output, placing the time series 163in decreasing order of similarity to the top two principal components 164of the (masked) time series data. The colorbar max is set to 3.29, 165the value at which a standard normal distribution N(0,1) has a 166two-sided tail probability of 0.001. The grayplot's top row contains 167a plot of the motion enorm and outlier frac across time, for reference 168with the grayplot series. 169''' 170 171qcb_helps["regr" ] = ''' 172When processing with stimulus time series, both individual and 173combined stimulus plots are generated (with any censoring also shown). 174 175The degrees of freedom (DF) summary is also provided, so one can check 176if too many get used up during processing (careful with bandpassing!). 177 178The "corr_brain" plot shows correlation of each voxel with the errts 179average within the whole brain mask (what could be called the 'global 180signal'). 181 182Two TSNR dsets can be shown. In each case, voxelwise TSNR is shown 183throughout the full FOV, and any brain mask dset is just used for 184defining a region within which percentiles are calculated. The generic 185formula for TSNR is: 186 TSNR = average(signal) / stdev(noise) 187+ First, the TSNR of r01 after volreg is shown if the user used the 188 '-volreg_compute_tsnr yes' opt in AP. Here, the "signal" is the time 189 series and the "noise" is the detrended time series. 190+ Second, the TSNR of the combined runs after regression modeling is 191 shown. Here, the "signal" is the all_runs dset and the "noise" is 192 the errts time series. 193When a mask is present, the olay's hot colors (yellow-orange-red) are 194defined by the 5-95%ile range of TSNR in the mask. The 1-5%ile values 195within the mask are shown in light blue, and the lower values are 196shown in dark blue. In the absence of a mask, then the colorbar goes from 1970 to the 98%ile value within the whole dset. 198''' 199 200qcb_helps["radcor"] = ''' 201@radial_correlate plots (per run, per block). These can show 202scanner coil artifacts, as well as large subject motion; both factors 203can lead to large areas of very high correlation, which would be 204highlighted here. 205''' 206 207qcb_helps["warns"] = ''' 208Several AFNI programs carry out consistency checks while 209processing (e.g., pre-steady state check, regression matrix corr 210warnings, left-right flip checks). Warnings are conglomerated here. 211 212Each warning has one of the following levels: 213 {} 214 215The warning level is written, with color coding, at the top of each 216warning's text box. The QC block label 'warns' at the top of the page 217is also colored according to the maximum warning level present. 218'''.format( lahc.wlevel_str ) 219 220qcb_helps["qsumm"] = ''' 221This is the output of @ss_review_basic, which contains a loooot of 222useful information about your single subject processing. 223''' 224 225qcbh = '' 226for x in qcb_helps.keys(): 227 y = qcb_helps[x] 228 yspl = y.split('\n') 229 yind = '\n '.join(yspl) 230 qcbh+= '\n' + x 231 qcbh+= yind 232 233 234# ----------------------------------------------------------------------- 235# ----------------------------------------------------------------------- 236 237apqc_help = [ 238['HELP FILE FOR AP-QC', 239 '''afni_proc.py's single subject QC report form'''], 240['OVERVIEW', 241'''QC organization 242 The quality control (QC) is organized into thematic blocks to 243 check, such as original data acquisition, different alignments, 244 motion, regression modeling, and more. At the top of the QC page, 245 there is a navigation bar with a label for each QC block (vorig, 246 ve2a, etc.) that functions as a button to the top of that section. 247 248Rating 249 Beneath each section label is a(n initially empty) QC button, 250 which users can click to set a rating for that QC block and for 251 the overall subject rating ("FINAL"). Clicking on the QC button 252 toggles its state through good (+), bad (X) or other (?, which may 253 include 'ugly', or just a hint to revisit later). Users can also 254 use convenient 'filler buttons' at the right (A+, Ax, etc.), when 255 the ratings are constant/uniform-- one hopes for 'all good' 256 processing, but who knows... 257 258Commenting 259 Additionally, users can ctrl+click on the QC button to enter a 260 comment for that block. For example, they can write why a rating 261 was good or bad, or what question they have led them to rate it as 262 'other'. 263 264Saving 265 Clicking SAVE will let the user save the QC ratings+comments on 266 their computer for later use, such as inclusion/exclusion criteria 267 for the subject in group analysis. (NB: This action is treated 268 the same as downloading a file from online, and is subject to 269 standard limitations on simplicity due to browser security 270 settings.) 271 272See also 273 The online web tutorial: 274 https://afni.nimh.nih.gov/pub/dist/doc/htmldoc/tutorials/apqc_html/main_toc.html 275 It's more verbose and pictorial, if that's useful. 276''' 277], 278['''DEFINITIONS''', 279'''QC block 280 One thematic section of the QC form (original data, alignment 281 step, etc.). Each block has a label in the navigation bar (vorig, 282 ve2a, etc.)-- click on the label to jump to that block. Click on 283 the button below the label to provide a rating for that block. 284 285QC button 286 Below each QC block label in the navigation bar is a button 287 (initially empty after running afni_proc.py). The user can click 288 on it to toggle its state to one of three ratings (good, +; bad, 289 X; other, ?), as well as to enter a comment for that QC block (via 290 ctrl+click). 291 292'FINAL' 293 Label for the QC button to hold the user's overall/final 294 evaluation of the subject's data and processing. (Clicking on this 295 label does nothing.) 296 297filler button 298 |A+|, |Ax|, |A?|-- located in the upper right corner of the 299 navigation menu. These can be used to provide uniform ratings 300 across the QC buttons (click to fill empty buttons, double-click 301 to fill *all* buttons, regardless of state). 302 |clr|-- double-clicking on this will empty all ratings+comments 303 from the QC buttons. 304 305'SAVE' 306 Write the ratings+comments to disk. This action is done through 307 the browser, and is subject to the browser settings; probably 308 users should not have the browser set to automatically download 309 all files to the same location, for example. 310 311'HELP' 312 Button : well, how did you get here?? 313 314'BC', 'AC' 315 When using the 'pythonic' HTML style, 1D-plotting (like motion 316 enorm plots) also produces boxplots to summarize values. When 317 censoring has been applied, by default there will be two boxplots 318 made: one of values 'before censoring' (BC), and one of values 319 'after censoring' (AC). Those labels are affixed in order to the 320 titles of the boxplots. 321 322'''], 323['''SET BUTTON RATING''', 324''' 325To record an evaluation, click the button below any section label, and 326toggle through: 327 + : good, 328 X : bad, 329 ? : other (or 'revisit'). 330 331For speed, you can click 'filler button' |A+| once to fill all *empty* 332buttons with +, or doubly to fill *all* buttons with +. |Ax| behaves 333the same for X, and |A?| for ?. 334 335Double-click |clr| to clear all rating and comment values. 336 337Pro-tip: if data are mostly all in a single state like good or bad, 338just use filler buttons to save yourself click time, and then just 339click any individual buttons that are different. '''], 340['''COMMENT''', 341''' 342Use ctrl+click on a QC button to open (or close) a comment window. 343 344Save a comment with the green (left) button, or hit Enter at any point. 345Remove a comment with the pink (right) button, or hit Esc at any point. 346 347Any QC button with a comment gets a pair of quotes added, like ''+''. 348Comments are independent of rating, but adding a comment to an empty 349button changes its rating to ''?'' (which can be altered further from 350there). 351'''], 352['''SAVE FORM''', 353'''Click on the 'SAVE' button. 354 355Unfortunately, the file will not be directly saved by this, due to 356security settings on most web-browsers, and the user will be prompted 357to save the file as if downloading from the Web.''' 358], 359['''KEYBOARD NAVIGATION''', 360''' 361Use Tab to navigate the QC menu mirroring all above functionality. 362 363Hit Tab to move through the menu. Hit Enter on a section label to 364scroll the page there. 365 366On QC buttons hit Enter to toggle through the rating list. Use 367ctrl+Enter to open comments; as above, use Enter or Esc to keep or 368erase, respectively. 369 370On the filler buttons |A+|, |Ax| and |A?|, use Enter to fill empty 371QC buttons and ctrl+Enter to fill *all* buttons. 372On |clr|, ctrl+Enter clears all rating and comment values. 373'''], 374['''QC BLOCKS''', 375qcbh ] 376] 377 378# --------------------------------------------------------------------- 379 380brate_hover = '''QC BUTTON FORM 381 382NAVIGATE 383Click a label ('Top', etc.) to jump to a section. 384 385RATE + COMMENT 386Click the QC button below it to record your rating, toggling through: 387 X : bad, 388 ? : other/revisit, 389 + : good. 390Use ctrl+click on a QC button to provide a comment. Close the comment 391panel with ctrl+click or its buttons. 392 393SPEEDIFY 394There are 'filler buttons' for each rating: |A+|, |Ax|, |A?|. 395Click once to fill all *empty* buttons with that rating, or 396double click to fill *all* buttons with that rating. 397 398--- click 'HELP' at the right for more details and features ---''' 399 400# --------------------------------------------------------------------- 401 402bgood_hover = '''Speed rating with: +. 403Click once (or Enter) to fill all *empty* QC buttons; 404Double-click (or ctrl+Enter) to fill *all* QC buttons. 405''' 406 407bbad_hover = '''Speed rating with: X. 408Click once (or Enter) to fill all *empty* QC buttons; 409Double-click (or ctrl+Enter) to fill *all* QC buttons. 410''' 411 412bother_hover = '''Speed rating with: ?. 413Click once (or Enter) to fill all *empty* QC buttons; 414Double-click (or ctrl+Enter) to fill *all* QC buttons. 415''' 416 417bclear_hover = '''Clear all (!) QC buttons. 418Double-click (or ctrl+Enter) to clear *all* QC buttons. 419''' 420 421# --------------------------------------------------------------------- 422 423bsave_hover = '''Save ratings+comments. 424Click once (or Enter) to write QC form to disk. 425''' 426 427bhelp_hover = '''Open help page. 428Click once (or Enter) to open new help page. 429''' 430 431 432 433def write_help_html_file( ofile, ocss ): 434 435 # -------------- start --------------------- 436 437 ht = ''' 438 <html> 439 ''' 440 441 # -------------- head --------------------- 442 443 # use same style sheet as main pages 444 ht+= ''' 445 <head> 446 <link rel="stylesheet" type="text/css" href="{}" /> 447 '''.format( ocss ) 448 449 ht+= ''' 450 </head> 451 ''' 452 453 # -------------- body --------------------- 454 455 ht+= ''' 456 <body> 457 ''' 458 459 Nsec = len(apqc_help) 460 461 for ii in range(Nsec): 462 x = apqc_help[ii] 463 464 ht+= wrap_block_title( x[0], 465 vpad=1, 466 addclass=" class='padtop' ", 467 blockid='' ) 468 469 ht+= wrap_block_text( x[1], 470 addclass=" class='container' " ) 471 472 473 474 # -------------- finish --------------------- 475 476 ht+= '''</body>''' 477 ht+= '''</html>''' 478 479 # -------------- output --------------------- 480 481 fff = open( ofile, "w" ) 482 fff.write( ht ) 483 fff.close() 484 485# ---------------------------------------------------------------------- 486# ---------------------------------------------------------------------- 487# ---------------------------------------------------------------------- 488# ---------------------------------------------------------------------- 489# ---------------------------------------------------------------------- 490 491# -------------- functions for main HTML file generation --------------- 492 493def parse_apqc_text_fields(tt) : 494 '''Input: tt can be either a string or list of strings. 495 496 Output: a single string, concatenation of the list (if input as 497 such). Special case parsing happens if one string contains a flag 498 to include a PBAR in the string. 499 ''' 500 501 out = "" 502 503 # not all versions of python seem to have 'unicode' type 504 try: 505 if type(tt) == unicode : 506 tt = [str(tt)] 507 except: 508 if type(tt) == str : 509 tt = [tt] 510 511 Ntt = len(tt) 512 513 for ii in range(Ntt) : 514 ss = tt[ii] 515 #print(ss) 516 if ss.__contains__(PBAR_FLAG) : 517 sspbar = make_inline_pbar_str(ss) 518 out+= sspbar 519 else: 520 out+= ss 521 if ii < (Ntt - 1) : 522 out+= "\n" 523 524 return out 525 526# ------------------------------------------------------------------- 527 528def make_inline_pbar_str(ss) : 529 '''Input: ss is single string of form: {PBAR_FLAG}:{pbar.json} 530 531 Output: a single string, formatted to put pbar inline in text, and 532 possibly have a second line of thr info. 533 534 ''' 535 536 add_indent = ss.find(PBAR_FLAG) 537 if add_indent >= 0 : 538 str_indent = add_indent * ' ' 539 else: 540 sys.error("** ERROR: missing pbar flag in {}".format(ss)) 541 542 # file name of JSON should be only thing after ":" 543 fname_json = dir_img + "/" + ss.split(":")[1] 544 545 # pbar dict keys given in @chauffeur_afni 546 with open(fname_json, 'r') as fff: 547 pbar_dict = json.load(fff) 548 549 if not(pbar_dict.__contains__('pbar_for')) : 550 pbar_dict['pbar_for'] = 'olay' 551 552 out = str_indent + ''' {pbar_for}: {pbar_bot} <img class='pbar' style="display: inline;" src="media/{pbar_fname}" > {pbar_top}'''.format(**pbar_dict) 553 554 if pbar_dict['pbar_comm'] : 555 out+= ''' ({pbar_comm})'''.format(**pbar_dict) 556 557 if pbar_dict['vthr'] : 558 out+= '''\n''' 559 out+= str_indent + ''' thr : {vthr}'''.format(**pbar_dict) 560 561 if pbar_dict['vthr_comm'] : 562 out+= ''' ({vthr_comm})'''.format(**pbar_dict) 563 564 if pbar_dict['gen_comm'] : 565 out+= '''\n''' 566 out+= str_indent + '''{gen_comm}'''.format(**pbar_dict) 567 568 return out 569 570 571# ------------------------------------------------------------------- 572 573# these are the properties/fields that the incoming text might have. 574# These define what fields the jsons created by @ss_review_html (and 575# therefore, ultimately in lib_apqc_tcsh.py) can have. 576class apqc_item_info: 577 578 title = "" 579 text = "" 580 subtext = "" 581 itemtype = "" 582 itemid = "" 583 blockid = "" 584 blockid_hov = "" 585 warn_level = "" # [PT: May 21, 2019] 586 587 def set_title(self, DICT): 588 if 'title' in DICT : 589 self.title = DICT['title'] 590 591 def set_itemtype(self, DICT): 592 if 'itemtype' in DICT : 593 self.itemtype = DICT['itemtype'] 594 595 def set_itemid(self, DICT): 596 if 'itemid' in DICT : 597 self.itemid = DICT['itemid'] 598 599 def set_blockid(self, DICT): 600 if 'blockid' in DICT : 601 self.blockid = DICT['blockid'] 602 603 def set_blockid_hov(self, DICT): 604 if 'blockid_hov' in DICT : 605 self.blockid_hov = DICT['blockid_hov'] 606 607 def set_warn_level(self, DICT): 608 if 'warn_level' in DICT : 609 self.warn_level = DICT['warn_level'] 610 611 # [PT: May 16, 2019] updated to deal with parsing of PBAR stuff here 612 def add_text(self, DICT): 613 if 'text' in DICT : 614 xx = parse_apqc_text_fields(DICT['text']) 615 self.text+= xx 616# if type(DICT['text']) == list : 617# xx = '\n'.join(DICT['text']) 618# self.text+= xx 619# else: 620# self.text+= DICT['text'] 621 622 # [PT: May 16, 2019] updated to deal with parsing of PBAR stuff here 623 def add_subtext(self, DICT): 624 if 'subtext' in DICT : 625 xx = parse_apqc_text_fields(DICT['subtext']) 626 self.subtext+= xx 627# if type(DICT['subtext']) == list : 628# xx = '\n'.join(DICT['subtext']) 629# self.subtext+= xx 630# else: 631# self.subtext+= DICT['subtext'] 632 633 # this just runs through all possible things above and fills in 634 # what it can 635 def set_all_from_dict(self, DICT): 636 self.set_title(DICT) 637 self.set_itemtype(DICT) 638 self.set_itemid(DICT) 639 self.set_blockid(DICT) 640 self.set_blockid_hov(DICT) 641 self.add_text(DICT) 642 self.add_subtext(DICT) 643 self.set_warn_level(DICT) 644 645# ------------------------------------------------------------------- 646 647# these are the properties/fields that the incoming *page-title* info 648# might have. These define what fields the jsons created by 649# @ss_review_html (and therefore, ultimately in lib_apqc_tcsh.py) can 650# have. 651class apqc_title_info: 652 653 title = "" 654 subj = "" 655 taskname = "" 656 itemtype = "" 657 itemid = "" 658 blockid = "" 659 blockid_hov = "" 660 661 def set_title(self, DICT): 662 if 'title' in DICT : 663 self.title = DICT['title'] 664 665 def set_itemtype(self, DICT): 666 if 'itemtype' in DICT : 667 self.itemtype = DICT['itemtype'] 668 669 def set_itemid(self, DICT): 670 if 'itemid' in DICT : 671 self.itemid = DICT['itemid'] 672 673 def set_blockid(self, DICT): 674 if 'blockid' in DICT : 675 self.blockid = DICT['blockid'] 676 677 def set_blockid_hov(self, DICT): 678 if 'blockid_hov' in DICT : 679 self.blockid_hov = DICT['blockid_hov'] 680 681 def set_taskname(self, DICT): 682 if 'taskname' in DICT : 683 self.taskname = DICT['taskname'] 684 685 def set_subj(self, DICT): 686 if 'subj' in DICT : 687 self.subj = DICT['subj'] 688 689 # this just runs through all possible things above and fills in 690 # what it can 691 def set_all_from_dict(self, DICT): 692 self.set_title(DICT) 693 self.set_itemtype(DICT) 694 self.set_itemid(DICT) 695 self.set_blockid(DICT) 696 self.set_blockid_hov(DICT) 697 self.set_taskname(DICT) 698 self.set_subj(DICT) 699 700# ------------------------------------------------------------------- 701 702def write_json_file( ll, fname ): 703 704 olist = [] 705 olist.append( json_cols ) 706 707 # skip the first element here because it came from the title, and 708 # that doesn't have a QC button with it (it's just the 'Top' of 709 # the page). 710 for i in range(1,len(ll)): 711 x = ll[i] 712 olist.append( [x[0], "", ""] ) 713 714 # output with indentation 715 ojson = json.dumps( olist, indent=4 ) 716 fff = open( fname, "w" ) 717 fff.write( ojson ) 718 fff.close() 719 720# -------------------------------------------------------------------- 721 722# !!! UNUSED 723#def wrap_block_lab(x, vpad=0): 724# y = """<h3><center>block: """+x+"""</center></h3>""" 725# if vpad: 726# y= """\n"""+y 727# y+="""\n""" 728# return y 729 730# ------------------------------------------------------------------- 731 732def make_nav_table(llinks, max_wlevel=''): 733 # table form, not ul 734 N = len(llinks) 735 idx = 0 736 737 # ======================================================================= 738 # dummy, background nav 739 740 y = ''' 741 <div class="navbar"> 742 <table style="width: 100%"> 743 744 <tr> 745 <td style="width: 100%"> 746 <a style="text-align: left"> {0} </a> 747 </td> 748 </tr> 749 750 <tr> 751 <td style="width: 100%"> 752 <button class="button-generic button-LHS btn0" onclick=""> 753 {0} </button> 754 </td> 755 </tr> 756 757 </table> 758 </div> 759 '''.format( NULL_BTN0 ) 760 761 # ======================================================================= 762 # real, foreground nav 763 764 y+= '''\n<div class="navbar">\n''' 765 766 # ----------------------------------------------------- 767 # L-floating part: section anchors and rating buttons 768 # NB: these are fixed width 769 770 ## note about keycodes on internet explorer, might have to do 771 ## something like this for each one: 772 # https://stackoverflow.com/questions/1750223/javascript-keycode-values-are-undefined-in-internet-explorer-8 773 774 for i in range(0, N): 775 ll, hov = llinks[i][0], llinks[i][1] 776 #print(ll) 777 778 color_change = '' 779 780 # Put lines around "FINAL" element 781 if i<N-1 : 782 finaltab = '' 783 if ll == 'warns' and max_wlevel : 784 wcol = lahc.wlevel_colors[max_wlevel] 785 if lahc.wlevel_ranks[max_wlevel] > lahc.wlevel_ranks['mild'] : 786 finaltab = '''style="color: {}; '''.format("#000") 787 finaltab+= '''background-color: {};" '''.format(wcol) 788 else: 789 finaltab = '''style="color: {};" '''.format(wcol) 790 791 else: 792 finaltab = '''style="background-color: #ccc; color: #000;" ''' 793 794 # new table 795 y+= '''<table style="float: left">\n''' 796 797 # TOP ROW (blockid) 798 y+= ''' 799 <tr> 800 <td class="td1" id=td1_{0}> 801 <button class="button-generic button-LHS btn5" id="btn5_{0}" 802 onmousedown="moveToDiv(hr_{0})" title="{1}" {2} 803 onkeypress="if ( event.keyCode == 13 ) {{ moveToDiv(hr_{0}); }}"> 804 {0}</button> 805 </td> 806 </tr> 807 '''.format( ll, hov, finaltab ) 808 809 # BOT ROW (QC button) 810 y+= '''<td >''' # set boundary between QC buttons here 811 if i : 812 # NB: with button clicks, if using onkeypress with 813 # onclick, the former *also* drives the latter a second 814 # time, so get annoying behavior; hence, distinguish those 815 y+= ''' 816 <button class="button-generic button-LHS btn1" id="btn1_{0}" data-txtcomm="" 817 onmousedown="btn1Clicked(event, this)" 818 onkeypress="if ( event.keyCode == 13 ) {{ btn1Clicked(event, this); }}" 819 {1}</button> 820 </td> 821 '''.format( ll, NULL_BTN1 ) 822 else: 823 y+= ''' 824 <button class="button-generic button-LHS btn0" id="btn0_{0}" 825 onclick="" 826 title="{1}"> 827 {2}</button></td> 828 '''.format( ll, brate_hover, "FORM:" ) 829 y+= '''</tr>\n''' 830 y+= '''</table>''' 831 832 if i : 833 # ~dropdown form button 834 ## NB: the onkeydown stuff makes it that hitting "Enter" 835 ## (event.keyCode == 10 || event.keyCode == 13) inside the 836 ## text field is like submitting the text (and the 837 ## .preventDefault() means that it does NOT input a 838 ## newline): 839 ## https://stackoverflow.com/questions/155188/trigger-a-button-click-with-javascript-on-the-enter-key-in-a-text-box 840 ## https://stackoverflow.com/questions/26975349/textarea-wont-stop-making-new-line-when-enter-is-pressed 841 ## ... and hitting "Esc" (event.keyCode == 27) is like 842 ## canceling. 843 y+= ''' 844 <div class="form-popup" id="cform_{0}" > 845 <form class="form-container" onsubmit="return false;"> 846 <textarea type="text" placeholder="Enter comment" 847 rows="4" cols="40" id="comm_{0}" 848 onkeydown="if (event.keyCode == 10 || event.keyCode == 13) {{ 849 event.preventDefault(); keepFromCommentForm(comm_{0}.id, cform_{0}.id);}} 850 else if (event.keyCode == 27) {{ 851 clearCommentForm(comm_{0}.id, cform_{0}.id); }}"> 852 </textarea> 853 <button type="button" class="btn" 854 onclick="keepFromCommentForm(comm_{0}.id, cform_{0}.id)">keep+close</button> 855 <button type="button" class="btn cancel" 856 onclick="clearCommentForm(comm_{0}.id, cform_{0}.id)">clear+close</button> 857 </form> 858 </div> 859 '''.format( ll ) 860 861 # ------------------------------------------------------ 862 # R-floating part: subj ID and SAVE button 863 # NB: this is flexible width 864 bsave = 'SAVE' 865 bhelp = 'HELP' 866 bgood = 'A+' ; bgood_ind = 1 867 bbad = 'Ax' ; bbad_ind = 2 868 bother = 'A?' ; bother_ind = 0 869 bclear = 'clr' ; bclear_ind = -1 870 871 # Start right-side table 872 y+= '''<table style="float: right; margin-right: 2px">\n''' 873 874 # ROW: "all fill" buttons-- click (or Enter) fills empty QC buttons, 875 # dblclick (or ctrl+Enter) fills ALL QC buttons 876 y+= '''<tr>\n''' 877 y+= '''<td style="width: 180px; white-space:nowrap;">\n''' 878 879 y+= ''' 880<button class="button-generic button-RHS button-RHS-little btn2{0}" title="{1}" 881onmousedown="allYourBaseAreBelongToUs({2})" 882onkeydown="if (event.keyCode == 10 || event.keyCode == 13) {{ if (event.ctrlKey) {{ 883reallyAllYourBaseAreBelongToUs({2}); }} else {{ allYourBaseAreBelongToUs({2}); }} }} " 884ondblclick="reallyAllYourBaseAreBelongToUs({2})"> 885{3}</button> 886 '''.format( 'good', bgood_hover, bgood_ind, bgood ) 887 888 y+= ''' 889<button class="button-generic button-RHS button-RHS-little btn2{0}" title="{1}" 890onmousedown="allYourBaseAreBelongToUs({2})" 891onkeydown="if (event.keyCode == 10 || event.keyCode == 13) {{ if (event.ctrlKey) {{ 892reallyAllYourBaseAreBelongToUs({2}); }} else {{ allYourBaseAreBelongToUs({2}); }} }} " 893ondblclick="reallyAllYourBaseAreBelongToUs({2})"> 894{3}</button> 895 '''.format( 'bad', bbad_hover, bbad_ind, bbad ) 896 897 y+= ''' 898<button class="button-generic button-RHS button-RHS-little btn2{0}" title="{1}" 899onmousedown="allYourBaseAreBelongToUs({2})" 900onkeydown="if (event.keyCode == 10 || event.keyCode == 13) {{ if (event.ctrlKey) {{ 901reallyAllYourBaseAreBelongToUs({2}); }} else {{ allYourBaseAreBelongToUs({2}); }} }} " 902ondblclick="reallyAllYourBaseAreBelongToUs({2})"> 903{3}</button> 904 '''.format( 'other', bother_hover, bother_ind, bother ) 905 906 y+= ''' 907<button class="button-generic button-RHS button-RHS-little btn2{0}" title="{1}" 908onkeydown="if (event.keyCode == 10 || event.keyCode == 13) {{ if (event.ctrlKey) {{ 909reallyAllYourBaseAreBelongToUs({2}); }} }} " 910ondblclick="reallyAllYourBaseAreBelongToUs({2})"> 911{3}</button> 912 '''.format( 'clear', bclear_hover, bclear_ind, bclear ) 913 914 y+= '''</td>\n''' 915 y+= '''</tr>\n''' 916 917 # ROW: hyperlinks (anchors) within the page 918 y+= '''<tr>\n''' 919 y+= '''<td style="width: 180px; white-space:nowrap;" id=td3_{}>'''.format( bsave ) 920 921 y+= '''<button class="button-generic button-RHS btn3save" title="{}" '''.format( bsave_hover ) 922 y+= '''onclick="doSaveAllInfo()">''' 923 y+= '''{}</button>\n'''.format( bsave ) 924 925 y+= '''<button class="button-generic button-RHS btn3help" title="{}" '''.format( bhelp_hover ) 926 y+= '''onclick="doShowHelp()">''' 927# y+= '''href="help.html" target="_blank">''' 928# y+= '''onclick="location.href='help.html';">''' 929 y+= '''{}</button>\n'''.format( bhelp ) 930 931 y+= '''</td>\n''' 932 y+= '''</tr>\n''' 933 934 # End right-side table 935 y+= '''</table>''' 936 y+= '''</div>''' 937 938 return y 939 940# ------------------------------------------------------------------- 941# ------------------------------------------------------------------- 942# ------------------------------------------------------------------- 943 944# JAVASCRIPT script functions and variables 945 946def make_javascript_btn_func(subj ): 947 948 y = '' 949 950 y+= '''<script type="text/javascript">\n''' 951 952 y+= ''' 953 954// global vars 955var allBtn1, allTd1, allhr_sec; // selection/location IDs 956var topi, previ; // keeps our current/last location 957var jsonfile = "apqc_{0}.json"; // json file: apqc_SUBJ.json 958var qcjson; // I/O json of QC button responses 959var nb_offset = 66-1; // navbar height: needed for scroll/jump 960 961// properties of QC buttons that get toggled through 962const bkgds = [ "#fff" , "#67a9cf", "#d7191c" ]; 963const valeurs = [ "?" , "+" , "X" ]; 964const tcols = [ "#777" , "#FFF" , "#000" ]; 965 966'''.format( subj ) 967 968 # --------------- load/reload initialize ----------------- 969 970 # gets run at loadtime 971 y+= ''' 972//window.onload = function() { 973function RunAtStart() { 974 975 // initialize arrays and location 976 initializeParams(); 977 978 // read in JSON 979 loadJSON(jsonfile, function(unparsed_json) { 980 // give the global variable values 981 window.qcjson = JSON.parse(unparsed_json); 982 }); 983 984 CheckJsonfileMatchesQcbuttons(); 985 986 ApplyJsonfileToQcbuttons(); 987}; 988 989''' 990 991 # OFF AT HTE MOMENT, but a guard for reloading page 992 y+= ''' 993 window.onbeforeunload = function(event) 994 { 995 return confirm(); 996 }; 997''' 998 999 y+= ''' 1000// This function gets run when page loads ("onload"). 1001function initializeParams() { 1002 allBtn1 = document.getElementsByClassName("btn1"); // btn1_vepi, btn1_* 1003 allTd1 = document.getElementsByClassName("td1"); // td1_vepi, td1_* 1004 allhr_sec = document.getElementsByClassName("hr_sec"); // hr_vepi, hr_* 1005 1006 topi = findTopSectionIdx() // idx of current loc 1007 previ = topi; // init "previous" idx 1008 setTd1Border(topi, "#ffea00"); //"yellow"); // show location 1009} 1010''' 1011 1012 # read in JSON file, from: 1013 # https://codepen.io/KryptoniteDove/post/load-json-file-locally-using-pure-javascript 1014 # https://stackoverflow.com/questions/7346563/loading-local-json-file 1015 # importantly, the xobjs.open(...) func NEEDS to have the 'false' 1016 # set to read synchronously, which is necessary for the JSON to 1017 # load fully before use-- slower, but this is a small file 1018 y+= ''' 1019function loadJSON(ifile, callback) { 1020 var xobj = new XMLHttpRequest(); 1021 xobj.overrideMimeType("application/json"); 1022 xobj.open('GET', ifile, false); // need 'false' for synchrony! 1023 xobj.onreadystatechange = function () { 1024 if (xobj.readyState == 4 && xobj.status == "200") { !!!! 1025 callback(xobj.responseText); 1026 } 1027 }; 1028 xobj.send(null); 1029} 1030''' 1031 1032 # Both the QC element names AND their order need to match 1033 y+= ''' 1034function CheckJsonfileMatchesQcbuttons() { 1035 var Nele = qcjson.length; 1036 1037 for( var i=1; i<Nele; i++ ) { 1038 var ele = qcjson[i][0]; 1039 1040 var jj = i - 1; // offset because of col heading in JSON 1041 var bname = new String(allBtn1[jj].id); 1042 var bpost = bname.slice(5); // skip the 'btn1_' part of button ID 1043 1044 if ( bpost != ele ) { 1045 window.alert("**Error: postfix on button ID " + bname + " does not match with JSON entry " + ele); 1046 throw "DONE"; 1047 } 1048 } 1049} 1050''' 1051 1052 # Because order matches (offset by 1), we can just apply directly 1053 # with the counting index, based on the allBtn1 list. 1054 y+= ''' 1055// This function gets run when page loads ("onload"). 1056function ApplyJsonfileToQcbuttons() { 1057 var Nele = qcjson.length; 1058 1059 for( var i=1; i<Nele; i++ ) { 1060 1061 var jj = i - 1; // offset because of col heading in JSON 1062 var bid = new String(allBtn1[jj].id); 1063 1064 // Set the comments first, because the button presentation 1065 // of rating depends on whether that has been set 1066 sendCommentToButtonAndForm(qcjson[i][2], bid); 1067 sendRatingToButton(qcjson[i][1], bid); 1068 } 1069 1070} 1071''' 1072 1073 # This function gets run when page loads ("onload"). 'ss' is the 1074 # JSON rating, and 'bid' is the button ID in AllBtn1. 1075 y+= ''' 1076function sendRatingToButton(ss, bid) { 1077 1078 if ( ss == "good" ) { 1079 setThisButtonRating(bid, 1); 1080 } else if ( ss == "bad" ) { 1081 setThisButtonRating(bid, 2); 1082 } else if ( ss == "other" ) { 1083 setThisButtonRating(bid, 0); 1084 } else if ( ss == "null" || ss == "" ) { 1085 setThisButtonRating(bid, -1); 1086 } else { 1087 window.alert("**Error: unallowed rating in JSON:" + ss); 1088 throw "DONE"; 1089 } 1090} 1091''' 1092 1093 # When the JSON is read in, get comments and give any text to both 1094 # the btn1 and associated comment form textarea 1095 y+= ''' 1096function sendCommentToButtonAndForm(comm, bid) { 1097 thisButtonGetsAComment(bid, comm); 1098 1099 var bname = new String(bid); // basename 1100 var cid = 'comm_' + bname.slice(5); // skip the 'btn1_' part of button ID 1101 1102 // because of how "null" is read in; this just matters in form 1103 if ( comm == "null" ) { 1104 var comm = ""; 1105 } 1106 thisFormTextAreaGetsAComment(cid, comm); 1107} 1108''' 1109 1110 # --------------- scroll location in page stuff ----------------- 1111 1112 # Checks/rechecks whenever change in page location occurs. 1113 y+= ''' 1114window.addEventListener("scroll", function(event) { 1115 1116 var newi = findTopSectionIdx(); 1117 1118 if ( newi != topi ) { 1119 setTd1Border(newi, "#ffea00"); //"yellow"); 1120 setTd1Border(topi, "inherit"); ; //"#FFF", "#444"); 1121 previ = topi; 1122 topi = newi; 1123 } 1124}, false); 1125''' 1126 1127 # Just go through (short) list from top, and first one that has pos 1128 # coor, is the one at top 1129 y+= ''' 1130function findTopSectionIdx() { 1131 for( var i=0; i<allhr_sec.length; i++ ) { 1132 var bid = allhr_sec[i].id; 1133 var bbox = document.getElementById(bid).getBoundingClientRect(); 1134 if ( bbox.top - nb_offset > 1 ) { 1135 break; 1136 } 1137 } 1138 return i-1; 1139} 1140''' 1141 1142 y+= ''' 1143function setTd1Border(ii, bkgdcol) { 1144 var newtid = allTd1[ii].id; 1145 document.getElementById(newtid).style.background = bkgdcol; 1146} 1147''' 1148 1149 # --------------- QC button: toggle indiv or fill group ------------- 1150 1151 # click on the QC buttons will scroll through the color values 1152 ## ctrl+click on the QC buttons will toggle between the comment 1153 ## form being open or closed (saving what is in form when closing). 1154 y+= ''' 1155function btn1Clicked(event, button) { 1156 if (event.ctrlKey) { 1157 btn1ClickedWithCtrl(event, button); 1158 } else { 1159 changeColor(button); //alert("The CTRL key was NOT pressed!"); 1160 } 1161 1162} 1163''' 1164 1165 y+= ''' 1166function btn1ClickedWithCtrl(event, button) { 1167 // get the form ID from button ID 1168 var bname = new String(button.id); 1169 var bpost = bname.slice(5); // skip the 'btn1_' part of button ID 1170 var cFormID = 'cform_' + bpost; 1171 // if closed, this opens it; otherwise, it closes it 1172 if ( document.getElementById(cFormID).style.display == false || 1173 document.getElementById(cFormID).style.display == "none" ) { 1174 openCommentForm(cFormID, button.id); 1175 } else { 1176 keepFromCommentFormViaBtn1(button.id, cFormID); 1177 } 1178} 1179''' 1180 1181 1182 # Toggle individual; 1183 ## ... and VERY useful comment about the "!important" keyword for 1184 ## hovering after changing DOM properties. 1185 ## https://stackoverflow.com/questions/46553405/css-hover-not-working-after-javascript-dom 1186 y+= ''' 1187function changeColor(button) { 1188 newidx = Number(button.dataset.idx || 0); // idx=0 on first click 1189 newidx = (newidx + 1) % bkgds.length; // calc new idx, mod Ncol 1190 button.dataset.idx = newidx; // store new idx in ele 1191 button.style.color = tcols[newidx]; // set color 1192 button.style.background = bkgds[newidx]; // set bkgd 1193 button.style.borderColor = bkgds[newidx]; // set bkgd 1194 button.textContent = valeurs[newidx]; 1195 checkIfButtonCommented( button ); // set text 1196} 1197''' 1198 1199 y+= ''' 1200function checkIfButtonCommented( button ) { 1201 1202 var value = button.textContent; 1203 var bcomm = button.dataset.txtcomm; 1204 var VAL_HAS_QUOTE = value.includes(`"`); 1205 1206 // if no comment, make sure there is no 1207 if ( ( bcomm == "" || bcomm == "null" ) ) { 1208 if ( VAL_HAS_QUOTE ) { 1209 var newval = value.replace(/\"/g, ""); 1210 button.textContent = newval; 1211 } 1212 } else { 1213 if ( !VAL_HAS_QUOTE ) { 1214 button.textContent = `"` + value + `"`; 1215 } 1216 } 1217} 1218''' 1219 1220 # two arguments: the button ID 'bid' from an element of 1221 # AllBt1n, and the 'idx' which picks out valeurs[idx] 1222 # etc. properties. 1223 y+= ''' 1224function setThisButtonRating(bid, idx) {{ 1225 // normal values 1226 if ( idx >= 0 ) {{ 1227 document.getElementById(bid).textContent = valeurs[idx]; 1228 document.getElementById(bid).style.background = bkgds[idx]; 1229 document.getElementById(bid).style.borderColor = bkgds[idx]; 1230 document.getElementById(bid).style.color = tcols[idx]; 1231 document.getElementById(bid).dataset.idx = idx; 1232 checkIfButtonCommented( document.getElementById(bid) ); 1233 1234 }} else {{ 1235 // the reset, for "null" JSON 1236 document.getElementById(bid).textContent = "{}"; 1237 document.getElementById(bid).style.background = ''; // reset to CSS 1238 document.getElementById(bid).style.borderColor = ''; // reset to CSS 1239 document.getElementById(bid).style.color = ''; // reset to CSS 1240 document.getElementById(bid).dataset.idx = 0; //null; 1241 }} 1242}} 1243'''.format ( NULL_BTN1 ) 1244 1245 y+= ''' 1246function isBtn1InNullState( bid ) {{ 1247 var tc = document.getElementById(bid).textContent; 1248 if ( tc == "{}" ) {{ 1249 return true; 1250 }} else {{ 1251 return false; 1252 }} 1253}} 1254'''.format ( NULL_BTN1 ) 1255 1256 # two arguments: the button ID 'bid' from an element of AllBt1n, 1257 # and the 'comment' that gets added/overwritten (in the newly 1258 # created element, txtcomm). Basically used to put the form 1259 # comments into the button fields, and then later into jsons. 1260 y+= ''' 1261function thisButtonGetsAComment(bid, comm) { 1262 document.getElementById(bid).dataset.txtcomm = comm; 1263 1264 // and don't allow a null state anymore if it has a comment: 1265 // update it to "other"/"?" 1266 if ( comm == "" || comm == "null" ) { 1267 } else { 1268 if ( isBtn1InNullState(bid) ) { 1269 setThisButtonRating(bid, 0); 1270 } 1271 } 1272 1273 // and reset quotes, if necessary. 1274 //window.alert(bid); 1275 checkIfButtonCommented( document.getElementById(bid) ); 1276} 1277''' 1278 1279 # "ALL OTHER" fill button, here to set every btn1-button value to 1280 # "+" or "x", depending on input arg 'ii' (index in list) 1281 y+= ''' 1282function allYourBaseAreBelongToUs(ii) {{ 1283 for( var i=0; i<allBtn1.length; i++ ) {{ 1284 var bid = allBtn1[i].id; 1285 var ival = document.getElementById(bid).textContent; 1286 if ( ival == "{}" ) {{ 1287 setThisButtonRating(bid, ii); 1288 }} 1289 }} 1290}} 1291'''.format( NULL_BTN1 ) 1292 1293 # "ALL-ALL" fill button: regardless of initial state set every 1294 # btn1-button value to "+" or "x", depending on input arg 'ii' 1295 # (index in list); that is, this overruns earlier button values 1296 y+= ''' 1297function reallyAllYourBaseAreBelongToUs(ii) { 1298 for( var i=0; i<allBtn1.length; i++ ) { 1299 var bid = allBtn1[i].id; 1300 var ival = document.getElementById(bid).textContent; 1301 setThisButtonRating(bid, ii); 1302 if ( ii < 0 ) { 1303 sendCommentToButtonAndForm("", bid); 1304 } 1305 } 1306} 1307''' 1308 1309 # ------------------- commentize form ------------------------ 1310 1311 # Get position coordinates of an object, knowing its ID 1312 y+= ''' 1313function getBoundingRect(iid) { 1314 var bbox = document.getElementById(iid).getBoundingClientRect(); 1315 return bbox; 1316} 1317''' 1318 1319 # Use this to place the thing: the height comes from the height of 1320 # the menu bar, and the L-R positioning comes from the QC button 1321 # itself. 1322 y+= ''' 1323function openCommentForm(cfID, bid) { 1324 document.getElementById(cfID).style.display = "block"; 1325 var bbox = getBoundingRect(bid); 1326 document.getElementById(cfID).style.left = bbox.left; 1327} 1328''' 1329 1330 # just close the form button when done (mainly for ctrl+click) 1331 y+= ''' 1332function closeCommentForm(cfID) { 1333 document.getElementById(cfID).style.display = "none"; 1334} 1335''' 1336 1337 # close *and* remove value (esc key, or clear+close button) 1338 y+= ''' 1339function clearCommentForm(cid, cfID) { 1340 document.getElementById(cid).value = ""; 1341 1342 // get the btn1 ID from comm ID 1343 var bname = new String(cid); // basename 1344 var bid = "btn1_" + bname.slice(5); // skip the 'comm_' part of button ID 1345 1346 thisButtonGetsAComment(bid, null); 1347 1348 closeCommentForm(cfID); 1349} 1350''' 1351 1352 # needed for when JSON file is read in, to give values from that 1353 # to the text area field (as well as bt1n) 1354 y+= ''' 1355function thisFormTextAreaGetsAComment(cid, comm) { 1356 document.getElementById(cid).value = comm; 1357} 1358''' 1359 1360 # "Saving" here means taking the comment (cid) and associating it 1361 # with a button (bid), while also closing the comment form (cfID). 1362 # (enter key, or keep+close button) 1363 y+= ''' 1364function keepFromCommentForm(cid, cfID) { 1365 1366 // user's text 1367 var commtext = document.getElementById(cid).value; 1368 1369 // get the btn1 ID from comm ID 1370 var bname = new String(cid); // basename 1371 var bid = "btn1_" + bname.slice(5); // skip the 'comm_' part of button ID 1372 1373 thisButtonGetsAComment(bid, commtext); 1374 closeCommentForm(cfID); 1375} 1376''' 1377 1378 # Same as keepFromCommentForm(...), but used when user is 1379 # ctrl+clicking on btn1 to close comment 1380 y+= ''' 1381function keepFromCommentFormViaBtn1(bid, cfID) { 1382 1383 // get the btn1 ID from comm ID 1384 var bname = new String(bid); // basename 1385 var cid = 'comm_' + bname.slice(5); // skip the 'comm_' part of button ID 1386 1387 // user's text 1388 var commtext = document.getElementById(cid).value; 1389 1390 thisButtonGetsAComment(bid, commtext); 1391 closeCommentForm(cfID); 1392} 1393''' 1394 1395 # ------------------- page scrolling ------------------------------ 1396 1397 # THIS is now how we move on the page, so that there is no need to 1398 # jump into the page, and hence tabbing through buttons is allowed. 1399 y+= ''' 1400function moveToDiv( hr_sec ) { 1401 var sid = new String(hr_sec.id) 1402 var rect = getBoundingRect(sid); 1403 1404 var scrtop = this.scrollY; 1405 var newloc = rect.top + scrtop - nb_offset; 1406 window.scrollTo(0, newloc); 1407 1408 //window.alert("earlier: " + rect.top + ", and now: " + this.scrollY); 1409} 1410''' 1411 1412 1413 # ------------------- saving into JSON obj ------------------------ 1414 1415 # submit values by element and col names 1416 y+= ''' 1417function saveJsonValuesByNames(elename, colname, VAL) { 1418 1419 cc = findCol(colname); 1420 rr = findQceleRow(elename); 1421 1422 qcjson[rr][cc] = VAL; 1423} 1424''' 1425 1426 # submit values by row and col nums 1427 y+= ''' 1428function saveJsonValuesByNums(rr, cc, VAL) { 1429 Ncol = qcjson[0].length; 1430 if ( cc >= Ncol ) { 1431 window.alert("**Error: Column [" + cc + "] not in JSON table!"); 1432 throw "DONE"; 1433 } 1434 1435 var Nrow = qcjson.length; 1436 if ( i >= Nrow ) { 1437 window.alert("**Error: QC element [" + rr + "] not in JSON table!"); 1438 throw "DONE!"; 1439 } 1440 1441 qcjson[rr][cc] = VAL; 1442} 1443''' 1444 1445 # find row index of QC element in JSON table 1446 y+= ''' 1447function findQceleRow(elename) { 1448 var Nrow = qcjson.length; 1449 for( var i=1 ; i<Nrow ; i++ ) { 1450 if ( elename == qcjson[i][0] ) { 1451 break; 1452 } 1453 } 1454 1455 if ( i >= Nrow ) { 1456 window.alert("**Error: QC element " + elename + " not in JSON table!"); 1457 throw "DONE"; 1458 } 1459 1460 return i; 1461} 1462''' 1463 1464 # find col index of item in JSON table 1465 y+= ''' 1466function findCol(colname) { 1467 Ncol = qcjson[0].length; 1468 for( var cc=1 ; cc<Ncol ; cc++ ) { 1469 if ( colname == qcjson[0][cc] ) { 1470 break; 1471 } 1472 } 1473 1474 if ( cc >= Ncol ) { 1475 window.alert("**Error: Column " + colname + " not in JSON table!"); 1476 throw "DONE"; 1477 } 1478 1479 return cc; 1480} 1481''' 1482 1483 1484 1485 # At present, THIS is the form of the input json: just a list of 1486 # lists. This will be convenient in order to remain ordered 1487 # (dictionaries are *not* ordered). Column headings are still 1488 # included, for the moment. At the moment, the column heading 1489 # names are hardcoded into the data I/O. 1490 1491 ''' 1492[ 1493 ["qcele", "rating", "comment"], 1494 ["vepi", "good", "null"], 1495 ["ve2a", "bad", "hello"], 1496 ["va2t", "null", "null"], 1497 ["vstat", "null", "null"], 1498 ["mot6", "null", "hello"], 1499 ["motE", "null", "null"], 1500 ["out", "null", "hello"], 1501 ["regps", "null", "null"], 1502 ["regcs", "null", "hello"], 1503 ["warns", "null", "null"], 1504 ["summ", "null", "hello"] 1505] 1506''' 1507 1508 # ----------- SAVE FORM: update JSON (qcjson) and save to file 1509 # ----------- (hopefully)! 1510 1511 # The Saver 1512 y+= ''' 1513function doSaveAllInfo() { 1514 updateLocalJson(); 1515 1516 var text = JSON.stringify(qcjson); 1517 //var filename = "apqc.json"; 1518 saveDownloadJsonfile(text, jsonfile); 1519 1520} 1521''' 1522 # The Helper 1523 y+= ''' 1524function doShowHelp() { 1525 window.open('help.html', '_blank'); 1526 1527} 1528''' 1529 1530 # Step 1 of saving the dataset: push button vals to JSON 1531 y+= ''' 1532function updateLocalJson() { 1533 var Nele = qcjson.length; 1534 for( var i=1; i<Nele; i++ ) { 1535 1536 var qcele = qcjson[i][0]; 1537 1538 var jj = i - 1; // offset because of col heading in JSON 1539 var bid = new String(allBtn1[jj].id); 1540 1541 var rattext = translateBtn1TextToJsonRating(document.getElementById(bid).textContent); 1542 saveJsonValuesByNames(qcele, "rating", rattext); 1543 1544 // save the comment part 1545 var commtext = document.getElementById(bid).dataset.txtcomm; 1546 saveJsonValuesByNames(qcele, "comment", commtext); 1547 } 1548 // window.alert("SAVING JSON: " + qcjson); 1549} 1550''' 1551 1552 # Step 2 of saving the dataset: write to file 1553 y+= ''' 1554function saveDownloadJsonfile(text, filename){ 1555 var a = document.createElement('a'); 1556 a.setAttribute('href', 'data:text/plain;charset=utf-u,'+encodeURIComponent(text)); 1557 a.setAttribute('download', filename); 1558 document.body.appendChild(a); 1559 a.click(); 1560 document.body.removeChild(a); 1561} 1562''' 1563 1564 y+= ''' 1565function translateBtn1TextToJsonRating( tt ) { 1566 if ( tt.includes("+") ) { 1567 return "good"; 1568 } else if ( tt.includes("X") ) { 1569 return "bad"; 1570 } else if ( tt.includes("?") ) { 1571 return "other"; 1572 } else if ( tt == "null" || tt == "" ) { 1573 return "null"; 1574 } else { 1575 window.alert("**Error: unallowed text/rating in button:" + tt); 1576 throw "DONE"; 1577 } 1578} 1579''' 1580 1581 y+= ''' 1582</script> 1583''' 1584 1585 return y 1586 1587# ------------------------------------------------------------------- 1588# ------------------------------------------------------------------- 1589# ------------------------------------------------------------------- 1590# ------------------------------------------------------------------- 1591# ------------------------------------------------------------------- 1592# ------------------------------------------------------------------- 1593 1594def wrap_page_title( xtitle, xstudy, xsubj, 1595 vpad=0, addclass="", blockid='', padmarg=0 ): 1596 1597 # start the first div on the page 1598 y = '''<div class="div_pad_class">''' 1599 1600 # the boundary line: location+ID necessary for highlighting page 1601 # location 1602 y+= '''\n\n<hr class="hr_sec" id="hr_{}"/>'''.format(blockid) 1603 1604 # the title 1605 y+= '''<div id="{}" '''.format(blockid) 1606 1607 # this line offsets the anchor location for the navigation bar to 1608 # head to: the values here should be equal to the height of the 1609 # navigation bar (plus the line beneath it). 1610 y+= ''' style="padding-top: {0}px; margin-top: -{0}px;">'''.format(padmarg) 1611 1612 y+= ''' 1613 <h1><center> {} <center></h1></div> 1614 1615<div style="text-align: center;"> 1616 <div style="display: inline-block; text-align: left;"> 1617 <pre><h2>subj: {}</h2></pre> 1618 <pre><h3>task: {}</h3></pre> 1619 1620 </div> 1621</div> 1622'''.format( xtitle, xsubj, xstudy ) 1623 1624 1625 1626 if vpad: 1627 y = """\n"""+y 1628 y+="""\n""" 1629 1630 return y 1631 1632 1633# ------------------------------------------------------------------- 1634 1635def wrap_block_title(x, vpad=0, addclass="", blockid='', padmarg=0): 1636 1637 # close the previous section div (at least the title will have one) 1638 y = '''</div>\n\n''' 1639 1640 # start the new section div 1641 y+= '''<div class="div_pad_class">''' 1642 1643 # the boundary line: location+ID necessary for highlighting page 1644 # location 1645 y+= '''\n\n<hr class="hr_sec" ''' 1646 if blockid : 1647 y+= ''' id="hr_{}" '''.format(blockid) 1648 y+= '''/>\n''' 1649 1650 # the title 1651 y+= '''<div ''' 1652 if blockid : 1653 y+= ''' id="{}" '''.format(blockid) 1654 # this line offsets the anchor location for the navigation bar to 1655 # head to: the values here should be equal to the height of the 1656 # navigation bar (plus the line beneath it). 1657 y+= ''' style="padding-top: {0}px; margin-top: -{0}px;"'''.format(padmarg) 1658 y+= """><pre """ 1659 y+= ''' {} '''.format(addclass) 1660 y+= """><center>["""+blockid+"""]<b> """ 1661 y+= """<u>"""+x+"""</u>""" 1662 y+= ' '*(len(blockid)+3) # balance blockid text 1663 y+= """</b></center></pre></div>""" 1664 if vpad: 1665 y= """\n"""+y 1666 y+="""\n""" 1667 return y 1668 1669# ------------------------------------------------------------------- 1670 1671def wrap_block_text( x, vpad=0, addclass="", dobold=True, itemid='', 1672 padmarg=0 ): 1673 addid = '' 1674 if itemid : 1675 addid = ''' id="{}" '''.format( itemid ) 1676 1677 y = """<div {0}""".format( addid ) 1678 y+= ''' style="padding-top: {0}px; margin-top: -{0}px;"'''.format(padmarg) 1679 y+= ''' {} >'''.format(addclass) 1680 if dobold : 1681 y+= """<pre><b>"""+x+"""</b></pre></div>""" 1682 else: 1683 y+= """<pre>"""+x+"""</pre></div>""" 1684 if vpad: 1685 y= """\n"""+y 1686 y+="""\n""" 1687 return y 1688 1689# ------------------------------------------------------------------- 1690 1691def wrap_img(x, wid=500, vpad=0, addclass=""): 1692 # [PT: Nov 20, 2018] needed this next line to center the text, and 1693 # needed "display: inline-block" in the img {} def to not have 1694 # whole line be a link. 1695 1696 y = '' 1697 y+= vpad*'\n' 1698 1699 y+= '''<div style="text-align: center"> 1700 <a href="{0}"><img src="{0}" alt="{0}" {1} 1701 style="display: inline-block; text-align: center;"></a> 1702 </div>'''.format( x, addclass) 1703 y+= vpad*'\n' 1704 1705 return y 1706 1707# ------------------------------------------------------------------- 1708 1709# string literal 1710def wrap_dat(x, wid=500, vpad=0, addclass="", warn_level = "", 1711 remove_top_empty=False): 1712 1713 # some formatting: get rid of unnecessary top empty lines 1714 if remove_top_empty: 1715 newx = x.split("\n") 1716 if not(newx[0].strip()): 1717 newx = '\n'.join(newx[1:]) 1718 else: 1719 newx = '\n'.join(newx) 1720 else: 1721 newx = str(x) 1722 1723 top_line = '' 1724 if warn_level: 1725 top_line+= '''<p class="{}">{}</p>'''.format( 1726 'wcol_'+warn_level, warn_level ) 1727 1728 y = '' 1729 y+= vpad*'\n' 1730 y+= '''<div> 1731 <pre {} ><left><b>{}{}</b></left></pre> 1732</div>'''.format(addclass, top_line, newx) 1733 y+= vpad*'\n' 1734 1735 return y 1736 1737# ------------------------------------------------------------------- 1738 1739def read_descrip_json(x): 1740 '''Take the input json file 'x' and return an instance of the 1741apqc_item_info() class. 1742''' 1743 1744 ddd = read_json_to_dict(x) # get json as dictionary 1745 ainfo = apqc_item_info() # initialize obj to hold info 1746 ainfo.set_all_from_dict(ddd) # set everything in this obj that we can 1747 1748 return ainfo 1749 1750# ------------------------------------------------------------------- 1751 1752def read_title_json(x): 1753 '''Take the input json file 'x' and return an instance of the 1754apqc_title_info() class. 1755''' 1756 1757 ddd = read_json_to_dict(x) # get json as dictionary 1758 tinfo = apqc_title_info() # initialize obj to hold title info 1759 tinfo.set_all_from_dict(ddd) # set everything in this obj that we can 1760 1761 return tinfo 1762 1763# ---------------------------------------------------------------------- 1764 1765def read_dat(x, do_join=True): 1766 1767 fff = open(x, 'r') 1768 txt = fff.readlines() 1769 fff.close() 1770 1771 if do_join : 1772 out = ''.join(txt) 1773 return out 1774 else: 1775 return txt 1776 1777# ---------------------------------------------------------------------- 1778 1779# check if json exists- return full or null dict 1780def read_json_to_dict(x): 1781 1782 if os.path.isfile(x): 1783 with open(x, 'r') as fff: 1784 xdict = json.load(fff) 1785 else: 1786 xdict = {} 1787 1788 return xdict 1789 1790# ---------------------------------------------------------------------- 1791 1792def make_pbar_line(d, imgpbar, vpad=0, addclassdiv="", addclassimg="", 1793 dobold=True): 1794 1795 y = '''<div {} ><pre>'''.format(addclassdiv) 1796 if dobold : 1797 y+= """<b>""" 1798 1799 # [PT: Jan 2, 2019] Typically, the pbar/cbar is for an olay, hence 1800 # the default; in some cases, we might want flexibility here, 1801 # though. 1802 voltype = "olay" 1803 if 'pbar_vol' in d : 1804 voltype = d['pbar_vol'] 1805 1806 y+= """{}: {} """.format(voltype, d['pbar_bot']) 1807 1808 y+= '''<img {} '''.format(addclassimg) 1809 y+= '''style="display: inline; margin: -5 -5px;" ''' 1810 y+= '''src="{}" > '''.format(imgpbar) 1811 y+= '''{} ({})'''.format(d['pbar_top'], d['pbar_comm']) 1812 1813 if 'vthr' in d : 1814 y+= '''\nthr : {}'''.format(d['vthr']) 1815 if 'vthr_reason' in d : 1816 y+= ''' ({})'''.format(d['vthr_comm']) 1817 1818 # [PT: Jan 2, 2019] can add in comments, too 1819 if 'pbar_comm' in d : 1820 if type(d['pbar_comm']) == list : 1821 for x in d['pbar_comm']: 1822 y+= '''\n{}'''.format(x) 1823 else : 1824 # assume it is unicode (esp. in py2) or str (likely in 1825 # py3) 1826 y+= '''\n{}'''.format(d['pbar_comm']) 1827 1828 if dobold : 1829 y+= '''</b>''' 1830 y+= '''</pre></div>\n''' 1831 1832 if vpad: 1833 y= """\n"""+y 1834 y+="""\n""" 1835 1836 return y 1837 1838# ---------------------------------------------------------------------- 1839 1840def read_pbar_range(x, dtype="NA"): 1841 1842 fff = open(x, 'r') 1843 txt = fff.readlines() 1844 fff.close() 1845 1846 Nlines = len(txt) 1847 if not(Nlines): 1848 sys.exit("** ERROR: no lines of text in {}?".format(x)) 1849 elif Nlines > 1: 1850 sys.exit("** ERROR: too many lines (={}) in {}?".format(Nlines, x)) 1851 1852 l0 = txt[0] 1853 y = l0.split() 1854 1855 Nnums = len(y) 1856 if Nnums != 3: 1857 sys.exit("** ERROR: wrong number of nums (={}) in {}?".format(Nnums, x)) 1858 1859 out = [] 1860 if dtype == int : 1861 for nn in y: 1862 z = int(nn) 1863 out.append(z) 1864 elif dtype == float : 1865 for nn in y: 1866 z = float(nn) 1867 out.append(z) 1868 else: # the NA or other cases... 1869 for nn in y: 1870 if nn.__contains__('.'): 1871 z = float(nn) 1872 else: 1873 z = int(nn) 1874 out.append(z) 1875 1876 return out[0], out[1], out[2] 1877 1878# ---------------------------------------------------------------------- 1879