From 6780f9f22aae9413bed57c5bbe90362c1a4c9d6a Mon Sep 17 00:00:00 2001 From: moculus Date: Tue, 6 Mar 2007 15:05:38 +0000 Subject: [PATCH] try this again --HG-- branch : pmacs2 --- BUGS | 13 + IDEAS | 12 + aes.py | 107 + application.py | 814 +++++ buffer.py | 516 ++++ bufferlist.py | 104 + cache.py | 60 + code_examples/DataIntegrator.pm | 3169 +++++++++++++++++++ code_examples/LONGLINES.txt | 84 + code_examples/METRIC.txt | 3 + code_examples/Reporting2.pm | 284 ++ code_examples/TEST.txt | 16 + code_examples/blah.c | 28 + code_examples/build.c | 721 +++++ code_examples/example.xml | 12 + code_examples/heredoc.pl | 10 + code_examples/imacfb.c | 425 +++ code_examples/map.js | 143 + code_examples/sim-outorder.c | 5110 +++++++++++++++++++++++++++++++ code_examples/targ-switch.s | 72 + code_examples/wfault.sh | 79 + color.py | 82 + completer.py | 105 + ctag_python.py | 222 ++ ctags.py | 218 ++ default.py | 39 + foo.pl | 8 + global.py | 14 + highlight.py | 431 +++ ispell.py | 80 + keyinput.py | 154 + lex.py | 249 ++ lex2.py | 216 ++ lex2_perl.py | 198 ++ lex_blame.py | 16 + lex_c.py | 122 + lex_diff.py | 41 + lex_javascript.py | 82 + lex_mutt.py | 81 + lex_nasm.py | 100 + lex_perl.py | 207 ++ lex_python.py | 102 + lex_sh.py | 85 + lex_sql.py | 70 + lex_text.py | 39 + lex_tt.py | 87 + lex_xml.py | 83 + method.py | 1229 ++++++++ minibuffer.py | 24 + mode.py | 232 ++ mode_blame.py | 16 + mode_c.py | 41 + mode_console.py | 158 + mode_diff.py | 45 + mode_javascript.py | 43 + mode_mini.py | 48 + mode_mutt.py | 43 + mode_nasm.py | 29 + mode_perl.py | 449 +++ mode_python.py | 220 ++ mode_replace.py | 126 + mode_search.py | 171 ++ mode_sh.py | 36 + mode_sql.py | 37 + mode_text.py | 64 + mode_tt.py | 31 + mode_which.py | 54 + mode_xml.py | 32 + point.py | 66 + regex.py | 21 + run.py | 10 + tab.py | 140 + tab_c.py | 86 + tab_javascript.py | 60 + tab_perl.py | 85 + tab_python.py | 141 + tab_sh.py | 55 + tab_sql.py | 51 + tab_xml.py | 48 + test.py | 15 + util.py | 52 + window.py | 569 ++++ 82 files changed, 19440 insertions(+) create mode 100644 BUGS create mode 100644 IDEAS create mode 100755 aes.py create mode 100755 application.py create mode 100644 buffer.py create mode 100644 bufferlist.py create mode 100644 cache.py create mode 100644 code_examples/DataIntegrator.pm create mode 100644 code_examples/LONGLINES.txt create mode 100644 code_examples/METRIC.txt create mode 100644 code_examples/Reporting2.pm create mode 100644 code_examples/TEST.txt create mode 100644 code_examples/blah.c create mode 100644 code_examples/build.c create mode 100644 code_examples/example.xml create mode 100644 code_examples/heredoc.pl create mode 100644 code_examples/imacfb.c create mode 100644 code_examples/map.js create mode 100644 code_examples/sim-outorder.c create mode 100644 code_examples/targ-switch.s create mode 100755 code_examples/wfault.sh create mode 100644 color.py create mode 100644 completer.py create mode 100755 ctag_python.py create mode 100755 ctags.py create mode 100644 default.py create mode 100644 foo.pl create mode 100644 global.py create mode 100644 highlight.py create mode 100644 ispell.py create mode 100644 keyinput.py create mode 100755 lex.py create mode 100755 lex2.py create mode 100755 lex2_perl.py create mode 100755 lex_blame.py create mode 100644 lex_c.py create mode 100755 lex_diff.py create mode 100755 lex_javascript.py create mode 100755 lex_mutt.py create mode 100644 lex_nasm.py create mode 100755 lex_perl.py create mode 100755 lex_python.py create mode 100755 lex_sh.py create mode 100755 lex_sql.py create mode 100755 lex_text.py create mode 100755 lex_tt.py create mode 100755 lex_xml.py create mode 100644 method.py create mode 100644 minibuffer.py create mode 100644 mode.py create mode 100644 mode_blame.py create mode 100644 mode_c.py create mode 100644 mode_console.py create mode 100644 mode_diff.py create mode 100644 mode_javascript.py create mode 100644 mode_mini.py create mode 100644 mode_mutt.py create mode 100644 mode_nasm.py create mode 100644 mode_perl.py create mode 100644 mode_python.py create mode 100644 mode_replace.py create mode 100644 mode_search.py create mode 100644 mode_sh.py create mode 100644 mode_sql.py create mode 100644 mode_text.py create mode 100644 mode_tt.py create mode 100644 mode_which.py create mode 100644 mode_xml.py create mode 100644 point.py create mode 100644 regex.py create mode 100755 run.py create mode 100644 tab.py create mode 100644 tab_c.py create mode 100644 tab_javascript.py create mode 100644 tab_perl.py create mode 100644 tab_python.py create mode 100644 tab_sh.py create mode 100644 tab_sql.py create mode 100644 tab_xml.py create mode 100644 test.py create mode 100644 util.py create mode 100644 window.py diff --git a/BUGS b/BUGS new file mode 100644 index 0000000..91b8255 --- /dev/null +++ b/BUGS @@ -0,0 +1,13 @@ +2006/07/04: +when in the minibuffer, certain key sequences don't seem to get picked up. + +2006/07/04: +undo/redo should probably show you what is being undone (i.e. by jumping to that +region of code). + +2006/07/04: +undo/redo is mostly fixed, but there are still occasionally problems, which seem +to relate to pasting in multiple lines and cursor positioning. + +2006/06/25: +long prompts will cause problems (particularly filenames) diff --git a/IDEAS b/IDEAS new file mode 100644 index 0000000..73205eb --- /dev/null +++ b/IDEAS @@ -0,0 +1,12 @@ +since the commands are stateless, they should probably only be instantiated once +and stored in the application. that way, anyone can run any command using the +following: + m = app.methods['my-method-name'] + m.execute() +Right now, every mode instance instantiates its own exact copy of the method, +and anyone else who needs to use a method just instantiates the method in +question. +(2006/11/4) +Well, we've made some progress on this. The app now has copies of everything, +but the various modes don't necessarily use that copy, and also don't +necessarily add their own stuff to it. diff --git a/aes.py b/aes.py new file mode 100755 index 0000000..cb33da2 --- /dev/null +++ b/aes.py @@ -0,0 +1,107 @@ +#!/usr/bin/python +# +# by Erik Osheim +import os, popen2 + +class Cipher: + def __init__(self, password, seed='aes.py', hashtype='rmd160'): + self.password = password + self.seed = seed + self.hashtype = hashtype + def encrypt(self, data): + return encrypt_data(data, self.password, self.seed, self.hashtype) + def decrypt(self, encrypted): + return decrypt_data(encrypted, self.password, self.seed, self.hashtype) + +def _check_aespipe(): + result = os.system('which aespipe > /dev/null') + if result != 0: + raise Exception, "Could not find aespipe; is it installed?" + +def encrypt_data(data, password, seed='aes.py', hashtype='rmd160'): + '''uses password to encrypt data''' + _check_aespipe() + cmd = "aespipe -S '%s' -H '%s' -p 0" % (seed, hashtype) + (stdout, stdin, stderr) = popen2.popen3(cmd) + stdin.write(password + '\n') + stdin.write(data) + stdin.close() + encrypted = stdout.read() + err = stderr.read() + if err: + raise Exception, "Problem: %s" % err + return encrypted + +def encrypt_path(path, data, password, seed='aes.py', hashtype='rmd160'): + '''uses password to encrypt data and writes result to path''' + encrypted = encrypt_data(data, password, seed, hashtype) + f = open(path, 'w') + f.write(encrypted) + f.close() + +def decrypt_data(encrypted, password, seed='aes.py', hashtype='rmd160'): + '''uses password to decrypt data''' + _check_aespipe() + cmd = "aespipe -d -S '%s' -H '%s' -p 0" % (seed, hashtype) + (stdout, stdin, stderr) = popen2.popen3(cmd) + stdin.write(password + '\n') + stdin.write(encrypted) + stdin.close() + data = stdout.read() + err = stderr.read() + if err: + raise Exception, "Problem: %s" % err + + # data is null-padded at the end to align on 16 or 512 bytes boundaries + i = len(data) + while i > 1: + if data[i-1] == '\x00': + i -= 1 + else: + break + return data[:i] + +def decrypt_path(path, password, seed='aes.py', hashtype='rmd160'): + '''uses password to decrypt data from path''' + f = open(path, 'r') + encrypted = f.read() + f.close() + data = decrypt_data(encrypted, password, seed, hashtype) + return data + +if __name__ == "__main__": + import optparse, sys + + parser = optparse.OptionParser() + parser.set_defaults(mode='decrypt') + parser.set_defaults(password='insecure1@3$5^') + parser.set_defaults(filename='output.aes') + + parser.add_option('-e', dest='mode', action='store_const', const='encrypt', + help="perform encryption on data from stdin") + parser.add_option('-d', dest='mode', action='store_const', const='decrypt', + help="perform decryption on data from stdin") + parser.add_option('-f', dest='filename', action='store', metavar='FILENAME', + help="encrypt to/from FILENAME (default: output.aes)") + parser.add_option('-p', dest='password', action='store', metavar='PASSWORD', + default="insecure1@3$5^", + help="use password PASSWORD (default: insecure1@3$5^)") + + (opts, args) = parser.parse_args() + + c = Cipher(opts.password) + + if opts.mode == 'encrypt': + data = sys.stdin.read() + encrypted = c.encrypt(data) + f = open(opts.filename, 'w') + f.write(encrypted) + f.close() + print "data written to %r." % opts.filename + else: + print "reading data from %r:" % opts.filename + f = open(opts.filename, 'r') + encrypted = f.read() + f.close() + data = c.decrypt(encrypted) + print data diff --git a/application.py b/application.py new file mode 100755 index 0000000..62bbd12 --- /dev/null +++ b/application.py @@ -0,0 +1,814 @@ +#!/usr/bin/env python +import curses, curses.ascii, getpass, os, re, string, sys, termios, time +import traceback + +import buffer, bufferlist, color, completer, keyinput, method, minibuffer +import mode, point, sets, util, window + +# modes +import mode, mode_c, mode_mini, mode_python, mode_nasm, mode_perl, mode_search +import mode_replace, mode_xml, mode_console, mode_sh, mode_text, mode_which +import mode_mutt, mode_sql, mode_javascript, mode_diff, mode_blame, mode_tt + +def run(buffers, jump_to_line=None, init_mode=None): + # save terminal state so we can restore it when the program exits + attr = termios.tcgetattr(sys.stdin) + keyinput.disable_control_chars() + + retval = 1 + try: + retval = curses.wrapper(run_app, buffers, jump_to_line, init_mode) + except: + traceback.print_exc() + + # restore terminal state + termios.tcsetattr(sys.stdin, termios.TCSANOW, attr) + return retval + +def run_app(stdscr, buffers, jump_to_line=None, init_mode=None): + a = Application(stdscr, buffers, jump_to_line, init_mode) + a.run() + +KILL_RING_LIMIT = 128 +WORD_LETTERS = list(string.letters + string.digits) +ERROR_TIMEOUT = -1 +#ERROR_TIMEOUT = 2 + +#DARK_BACKGROUND = False +DARK_BACKGROUND = True + +class Application: + def __init__(self, stdscr, buffers=[], jump_to_line=None, init_mode=None): + # initalize curses primitives + self.stdscr = stdscr + self.y, self.x = self.stdscr.getmaxyx() + + # initialize some basic stuff + self.margins_visible = False + #self.margins = [(80, 'blue'), (90, 'red')] + self.margins = [(80, 'blue'), ] + # each highlighted_range contains three things: [window, start_p, end_p] + self.highlighted_ranges = [] + self.mini_active = False + self.mini_buffer = None + self.mini_prompt = "" + self.error_string = "" + self.error_timestamp = None + self.input = keyinput.Handler() + + # initialize our colors + if curses.has_colors(): + curses.start_color() + try: + curses.use_default_colors() + color.default_color = True + except: + # guess we weren't on 2.4 + color.default_color = False + color.init() + + # this is how we can change color settings + if curses.can_change_color(): + #curses.init_color(curses.COLOR_BLUE, 750, 400, 0) + pass + else: + self.set_error("Dynamic color not available") + + # initialize our modes + self.modes = { + 'blame': mode_blame.Blame, + 'c': mode_c.C, + 'console': mode_console.Console, + 'diff': mode_diff.Diff, + 'fundamental': mode.Fundamental, + 'mini': mode_mini.Mini, + 'nasm': mode_nasm.Nasm, + 'perl': mode_perl.Perl, + 'python': mode_python.Python, + 'replace': mode_replace.Replace, + 'search': mode_search.Search, + 'sh': mode_sh.Sh, + 'text': mode_text.Text, + 'which': mode_which.Which, + 'xml': mode_xml.XML, + 'mutt': mode_mutt.Mutt, + 'sql': mode_sql.Sql, + 'javascript': mode_javascript.Javascript, + 'template': mode_tt.Template, + } + + # these are used in this order to determine which mode to open certain + # kinds of files + self.mode_paths = { + '/etc/profile': 'sh', + } + self.mode_basenames = { + '.bashrc': 'sh', + '.bash_profile': 'sh', + '.profile': 'sh', + } + self.mode_extensions = { + '.py': 'python', + '.pl': 'perl', + '.pm': 'perl', + '.t': 'perl', + '.c': 'c', + '.txt': 'text', + '.s': 'nasm', + '.sh': 'sh', + '.bash': 'sh', + '.xml': 'xml', + '.xml.in': 'xml', + '.html': 'xml', + '.htm': 'xml', + '.sql': 'sql', + '.js': 'javascript', + '.tt': 'template' + } + self.mode_detection = { + 'python': 'python', + 'perl': 'perl', + 'sh': 'sh', + 'bash': 'sh', + } + + # initialize our methods + self.methods = {} + for name in dir(method): + cls = eval("method.%s" % name) + if hasattr(cls, '_is_method') and cls._is_method: + self.methods[cls._name()] = cls() + + # create all the insert methods for the character ranges we like + for c in string.letters + string.digits + string.punctuation: + ## closing tags are handled differently + #if c == ')' or c == ']' or c == '}': + # continue + obj = method.InsertString(c) + self.methods[obj.name] = obj + + # window/slot height/width + height = self.y - 2 + width = self.x - 1 + + # initialize our buffers + # note that the first buffer in buffers will be initially visible + buffers.append(buffer.ScratchBuffer()) + buffers.append(buffer.ConsoleBuffer()) + self.bufferlist = bufferlist.BufferList(height, width) + self.active_slot = 0 + self.resize_slots() + + # build windows for our buffers + for b in buffers: + window.Window(b, self, height, width, slot=self.active_slot, + mode_name=init_mode) + self.bufferlist.add_buffer(b) + self.resize_windows() + + # see if the user has requested that we go to a particular line + if jump_to_line: + name = buffers[0].name() + b = self.bufferlist.get_buffer_by_name(name) + w = b.get_window(self.active_slot) + method.GotoLine().execute(w, lineno=jump_to_line) + + # initialize our kill ring and last action + self.kill_ring = [] + self.kill_commands = ['kill', 'kill-region'] + self.last_action = None + self.last_search = None + self.last_replace_before = None + self.last_replace_after = None + + # initialize tab handlers + method.DATATYPES['path'] = completer.FileCompleter() + method.DATATYPES['buffer'] = completer.BufferCompleter(self) + method.DATATYPES['command'] = completer.CommandCompleter() + method.DATATYPES['shell'] = completer.ShellCompleter() + method.DATATYPES['method'] = completer.MethodCompleter() + method.DATATYPES['mode'] = completer.ModeCompleter() + method.DATATYPES['perlfunction'] = completer.PerlFunctionCompleter() + + # set up curses + self.win = curses.newwin(self.y, self.x, 0, 0) + self.win.leaveok(1) + curses.meta(1) + curses.halfdelay(1) + self.hide_cursor() + + def globals(self): + return globals() + def locals(self): + return locals() + + def add_slot(self): + b = self.bufferlist.slots[self.active_slot].buffer + n = self.bufferlist.add_slot(0, 0, 0, b) + self.resize_slots() + self.add_window_to_buffer(b, n) + self.resize_windows() + def remove_slot(self, slotname): + assert len(self.bufferlist.slots) > 1, "oh no you didn't!" + assert slotname >= 0 and slotname < len(self.bufferlist.slots), \ + "invalid slot: %r (%r)" % (slotname, len(self.bufferlist.slots)) + b = self.bufferlist.slots[slotname].buffer + self.bufferlist.remove_slot(slotname) + if self.active_slot > slotname: + self.active_slot = max(0, self.active_slot - 1) + self.resize_slots() + self.resize_windows() + def single_slot(self): + while len(self.bufferlist.slots) > 1: + if self.active_slot == 0: + self.remove_slot(1) + else: + self.remove_slot(0) + + def get_window_height_width(self, slotname): + assert slotname >= 0 and slotname < len(self.bufferlist.slots), \ + "invalid slot: %r" % slotname + slot = self.bufferlist.slots[slotname] + return (slot.height, slot.width) + + # mini buffer handling + def get_mini_buffer(self): + return self.mini_buffer + def mini_buffer_is_open(self): + return self.mini_buffer is not None + def open_mini_buffer(self, prompt, callback, method=None, tabber=None, + modename=None): + if self.mini_buffer_is_open(): + self.close_mini_buffer() + self.mini_prompt = prompt + self.mini_buffer = minibuffer.MiniBuffer(callback, method, tabber, + modename) + window.Window(self.mini_buffer, self, height=1, + width=self.x-1-len(self.mini_prompt)-1, slot='mini') + self.mini_active = True + def exec_mini_buffer(self): + self.mini_buffer.callback(self.mini_buffer.make_string()) + self.close_mini_buffer() + def close_mini_buffer(self): + if self.mini_buffer_is_open(): + self.mini_buffer.close() + self.mini_buffer = None + self.mini_prompt = "" + self.mini_active = False + def get_mini_buffer_prompt(self): + return self.mini_prompt + def set_mini_buffer_prompt(self, p): + self.mini_prompt = p + + # window handling + def toggle_window(self): + assert 0 <= self.active_slot and self.active_slot < len(self.bufferlist.slots) + self.active_slot = (self.active_slot + 1) % len(self.bufferlist.slots) + def window(self): + slotname = self.active_slot + return self.bufferlist.slots[slotname].buffer.get_window(slotname) + def active_window(self): + if self.mini_active: + return self.mini_buffer.get_window('mini') + else: + assert 0 <= self.active_slot and self.active_slot < len(self.bufferlist.slots), \ + "0 <= %d < %d" % (self.active_slot, len(self.bufferlist.slots)) + slotname = self.active_slot + return self.bufferlist.slots[slotname].buffer.get_window(slotname) + + # buffer handling + def file_buffer(self, path, data, switch_to=True): + assert not self.has_buffer_name(path), 'oh no! %r is already open' % path + assert not os.path.exists(path), 'oh no! %r already exists in fs' % path + f = open(path, 'w') + f.write(data) + f.close() + b = buffer.FileBuffer(path) + b.open() + self.add_window_to_buffer(b, self.active_slot) + self.add_buffer(b) + if switch_to: + self.switch_buffer(b) + def data_buffer(self, name, data, switch_to=True, modename=None): + if self.has_buffer_name(name): + b = self.bufferlist.buffer_names[name] + self.remove_buffer(b) + b = buffer.DataBuffer(name, data) + if modename is not None: + b.modename = modename + self.add_window_to_buffer(b, self.active_slot) + self.add_buffer(b) + if switch_to: + self.switch_buffer(b) + def get_buffer_by_path(self, path): + return self.bufferlist.get_buffer_by_path(path) + def has_buffer_name(self, name): + return self.bufferlist.has_buffer_name(name) + def get_buffer_by_name(self, name): + return self.bufferlist.get_buffer_by_name(name) + def has_buffer(self, b): + return self.bufferlist.has_buffer(b) + def add_buffer(self, b): + self.bufferlist.add_buffer(b) + def remove_buffer(self, b): + assert b.name() is not "*Scratch*", "can't kill the scratch" + assert self.bufferlist.has_buffer(b), "can't kill what's not there" + assert len(self.bufferlist.buffers) > 1, "can't kill with no other buffers" + self.bufferlist.remove_buffer(b) + b.close() + if self.bufferlist.empty_slot(self.active_slot): + b2 = self.bufferlist.hidden_buffers[0] + self.bufferlist.set_slot(self.active_slot, b2) + def switch_buffer(self, b): + assert self.has_buffer_name(b.name()), "buffer %s does not exist" % (b.name()) + assert 0 <= self.active_slot and self.active_slot < len(self.bufferlist.slots) + self.add_window_to_buffer(b, self.active_slot) + self.bufferlist.set_slot(self.active_slot, b) + + def add_window_to_buffer(self, b, slotname): + if not b.has_window(slotname): + slot = self.bufferlist.slots[slotname] + window.Window(b, self, height=slot.height, width=slot.width, slot=slotname) + + # error string handling + def set_error(self, s): + self.error_string = s + self.error_timestamp = time.time() + def clear_error(self): + self.error_string = "" + self.error_timestamp = None + def resize_event(self): + self.y, self.x = self.stdscr.getmaxyx() + self.resize_slots() + self.resize_windows() + def resize_slots(self): + n = len(self.bufferlist.slots) + x = self.x - 1 + y_sum = self.y - 1 - n + y_pool = y_sum + y_offset = 0 + for i in range(0, n - 1): + slot = self.bufferlist.slots[i] + y = y_sum / n + slot.resize(y, x, y_offset) + y_pool -= y + y_offset += y + 1 + slot = self.bufferlist.slots[n-1].resize(y_pool, x, y_offset) + def resize_windows(self): + for b in self.bufferlist.buffers: + keys = b.windows.keys() + for name in keys: + try: + (height, width) = self.get_window_height_width(name) + b.windows[name].set_size(width, height) + except: + w = b.windows[name] + del b.windows[name] + # kill w now + + # hide the curses cursor + def hide_cursor(self): + self.win.move(self.y-2, 0) + try: + curses.curs_set(0) + except: + pass + + # exit + def exit(self): + self.done = True + + # kill stack manipulation + def push_kill(self, s): + if s is not None: + if self.last_action in self.kill_commands and \ + len(self.kill_ring): + self.kill_ring[-1] = self.kill_ring[-1] + s + else: + self.kill_ring.append(s) + if len(self.kill_ring) > KILL_RING_LIMIT: + self.kill_ring.pop(0) + def pop_kill(self): + return self.kill_ring.pop(-1) + def has_kill(self, i=-1): + return len(self.kill_ring) >= abs(i) + def get_kill(self, i=-1): + return self.kill_ring[i] + + # undo/redo + def undo(self): + try: + self.window().buffer.undo() + except Exception, e: + self.set_error("%s" % (e)) + def redo(self): + try: + self.window().buffer.redo() + except Exception, e: + self.set_error("%s" % (e)) + + # action creating methods + def make_insert_action(self, c): + return lambda: self.window().insert_string(c) + def make_window_action(self, methodname): + f = getattr(self.window(), methodname) + f() + + # we are evil + def eval(self, s): + return eval(s) + + # the might run-loop! + def run(self): + self.done = False + #keycodes = [] + while not self.done: + i = self.win.getch() + #if i > 0: + # if len(keycodes) >= 6: + # keycodes.pop(0) + # keycodes.append(str(i)) + #self.set_error('keycodes: %s' % repr(keycodes)) + if i == curses.KEY_RESIZE: + while i == curses.KEY_RESIZE: + i = self.win.getch() + self.resize_event() + err = '' + try: + self.input.parse(i) + except Exception, e: + err = str(e) + while len(self.input.tokens): + t = self.input.tokens.pop(0) + self.active_window().mode.handle_token(t) + self.draw() + if err: + self.set_error(err) + if self.error_timestamp is not None and \ + ERROR_TIMEOUT > 0 and \ + time.time() - self.error_timestamp > ERROR_TIMEOUT: + self.clear_error() + return + + # highlighting + # each highlighted_range contains three things: [window, start_p, end_p] + def add_highlighted_range(self, w, start_p, end_p): + self.highlighted_ranges.append([w, start_p, end_p]) + def clear_highlighted_ranges(self): + self.highlighted_ranges = [] + + # full screen drawer + def draw(self): + self.hide_cursor() + self.draw_slots() + self.draw_input_bar() + self.hide_cursor() + self.win.noutrefresh() + self.hide_cursor() + curses.doupdate() + + # debugging + def dump(self): + w = self.window() + ll = len(w.buffer.lines) + pl = len(w.get_physical_lines()) + vl = len(w.visible_lines()) + + first = w.first + last = w.last + cursor = w.logical_cursor() + vcursor = w.visible_cursor() + s = "" + s += "width: %d\n" % (w.width) + s += "height: %d\n" % (w.height) + s += "len logical lines: %d\n" % (ll) + s += "logical first: %s\n" % (first) + s += "logical last: %s\n" % (last) + s += "logical cursor: %s\n" % (cursor) + s += "len physical lines: %d\n" % (pl) + s += "physical first: %s\n" % (w.physical_point(first)) + s += "physical last: %s\n" % (w.physical_point(last)) + s += "physical cursor: %s\n" % (w.physical_point(cursor)) + s += "len visible lines: %d\n" % (vl) + s += "visible first: %s\n" % ("n/a") + s += "visible last: %s\n" % ("n/a") + s += "visible cursor: %s\n" % (vcursor) + return s + + # sub-drawing methods + def draw_slots(self): + self.win.erase() + for i in range(0, len(self.bufferlist.slots)): + slot = self.bufferlist.slots[i] + self.draw_slot(i) + self.draw_status_bar(i) + + def draw_slot(self, slotname): + slot = self.bufferlist.slots[slotname] + if not slot.buffer.has_window(slotname): + return + w = slot.buffer.get_window(slotname) + lines = w.visible_lines() + regions = w.mode.visible_regions() + ## FIXME: why isn't this always the same???? + assert (len(lines) == len(regions) or + len(lines) == len(regions) - 1), "%d,%d" % (len(lines), + len(regions)-1) + assert len(lines) > 0, "no lines... why?" + m = min(len(lines), slot.height) + assert m > 0 + x = slot.width + y = slot.height + y_offset = slot.offset + + assert x > 0 + assert y > 0 + red_attr = color.build_attr(color.pairs('red', 'default')) + for i in range(0, m): + j = 0 + line = lines[i] + for r in regions[i]: + try: + # start, end, attr, value, ttype = r + assert 0 <= r.start, "0 <= %d" % (r.start) + assert r.start <= r.end, "%d <= %d" % (r.start, r.end) + assert r.end <= len(line), "%d <= %d" % (r.end, len(line)) + except Exception, e: + s = "\n".join([repr(x) for x in regions]) + raise Exception, "%s\n%s\n\n%s\n\n%s\n\n%s\n\n%d" % \ + (e, s, regions[i], r, repr(line), len(line)) + assert line[r.start:r.end] == r.value, \ + "%r != %r" % (line[r.start:r.end], r.value) + try: + if DARK_BACKGROUND: + attr = r.attr | curses.A_BOLD + else: + attr = r.attr + self.win.addnstr(i + y_offset, r.start, r.value, r.end - r.start, attr) + except Exception, e: + raise Exception, "%s\n%s %s %s %s" % \ + (e, repr(i), repr(r.start), repr(r.value), repr(r.end - r.start)) + j = r.end + if j < len(line): + # this is cheating... FIXME + self.win.addnstr(i + y_offset, j, line[j:], len(line) - j) + j += len(line) - j + if j < x: + self.win.addnstr(i + y_offset, j, ' ' * (x-j), (x-j)) + if w.continued_visible_line(i): + self.win.addch(i + y_offset, x, '\\', red_attr) + else: + self.win.addch(i + y_offset, x, ' ') + + for i in range(m, y): + self.win.addnstr(i + y_offset, 0, '~' + ' ' * (x), x + 1, red_attr) + + for (high_w, lp1, lp2) in self.highlighted_ranges: + if lp1.y != lp2.y: + # this region is incoherent, so skip it, or die, whatever + #raise Exception, "haddock %d != %d" % (lp1.y, lp2.y) + pass + elif w is not high_w: + # this region isn't in the current window so skip it + pass + else: + (pp1, pp2) = (w.physical_point(lp1), w.physical_point(lp2)) + vo = w.visible_offset() + (vp1, vp2) = (pp1.offset(0, -vo), pp2.offset(0, -vo)) + if vp2.y < 0 or vp1.y > w.height: + # this region is not visible, so skip it + pass + else: + # first let's fix our points so we're sure they're visible + if vp1.y < 0: + vp1 = point.Point(0,0) + if vp2.y > w.height: + vp2 = point.Point(len(lines[-1]), w.height-1) + + if vp1.y == vp2.y: + # our region physically fits on one line; this is easy + b = lines[vp1.y][vp1.x:vp2.x] + self.win.addstr(vp1.y + y_offset, vp1.x, b, curses.A_REVERSE) + else: + # our region spans multiple physical lines, so deal + b1 = lines[vp1.y][vp1.x:] + self.win.addstr(vp1.y + y_offset, vp1.x, b1, curses.A_REVERSE) + for i in range(vp1.y + 1, vp2.y): + b = lines[i] + self.wind.addstr(i + y_offset, 0, b, curses.A_REVERSE) + b2 = lines[vp2.y][:vp2.x] + self.win.addstr(vp2.y + y_offset, 0, b2, curses.A_REVERSE) + + if self.margins_visible: + for (limit, shade) in self.margins: + if self.x > limit: + for i in range(0, y): + # the actual character is the lower 8 bits, and the + # attribute is the upper 8 bits; we will ignore the + # attribute and just get the character + char = self.win.inch(i + y_offset, limit) & 255 + attr = color.build('default', shade, 'bold') + self.win.addch(i + y_offset, limit, char, attr) + + if self.mini_active is False and self.active_slot == slotname: + if w.active_point is not None and w.point_is_visible(w.active_point): + pa = w.physical_point(w.active_point) + va = pa.offset(0, -w.visible_offset()) + if len(lines[va.y]): + a = lines[va.y][va.x] + else: + a = ' ' + self.win.addch(va.y + y_offset, va.x, a, curses.A_REVERSE) + else: + cursor = w.visible_cursor() + cx, cy = (cursor.x, cursor.y) + if cy >= len(lines): + self.set_error('in main1: cursor error; %d >= %d' % + (cy, len(lines))) + return + elif cx == len(lines[cy]): + c = ' ' + elif cx > len(lines[cy]): + self.set_error('why? %r %r' % (cursor, len(lines[cy]))) + return + else: + c = lines[cy][cx] + self.win.addch(cy + y_offset, cx, c, curses.A_REVERSE) + + def draw_status_bar(self, slotname): + slot = self.bufferlist.slots[slotname] + if not slot.buffer.has_window(slotname): + return + + w = slot.buffer.get_window(slotname) + b = w.buffer + cursor = w.logical_cursor() + pcursor = w.physical_cursor() + first = w.first + last = w.last + + if b.readonly(): + if b.changed(): + modflag = '%*' + else: + modflag = '%%' + else: + if b.changed(): + modflag = '**' + else: + modflag = '--' + + if w.mark: + mark = w.mark + else: + mark = point.Point(-1, -1) + name = b.name() + + if w.first_is_visible(): + perc = "Top" + elif w.last_is_visible(): + perc = "Bot" + else: + perc = "%2d%%" % (first.y*100 / len(b.lines)) + + # XYZ: we should actually use more of the 'state' variables + + format = "----:%s-Fl %-18s (%s)--L%d--C%d--%s" + status = format % (modflag, name, w.mode.name(), cursor.y+1, cursor.x+1, perc) + status = status[:slot.width + 1] + status += "-" * (slot.width - len(status) + 1) + self.win.addnstr(slot.height + slot.offset, 0, status, slot.width + 1, + curses.A_REVERSE) + + # input bar drawing + def draw_input_bar(self): + if self.error_string: + self.draw_error() + elif self.mini_buffer_is_open(): + self.draw_mini_buffer() + else: + self.draw_nothing() + try: + # fucking python, fucking curses, fucking fuck + self.win.addch(self.y-1, self.x-1, ' ') + except: + pass + def draw_error(self): + l = self.x - 1 + s1 = self.error_string + s2 = util.cleanse(util.padtrunc(s1, l)) + self.win.addnstr(self.y-1, 0, s2, l) + def draw_mini_buffer(self): + l = self.x - 1 + w = self.mini_buffer.get_window('mini') + lines = w.visible_lines() + s1 = self.mini_prompt + lines[0] + s2 = util.padtrunc(s1, l) + self.win.addnstr(self.y-1, 0, s2, l) + + if self.mini_active: + cursor = w.visible_cursor() + cx, cy = (cursor.x, cursor.y) + if cy >= len(lines): + #self.set_error('in main2: cursor error; %d >= %d' % + # (cy, len(lines))) + self.set_error('in main2: %r, %r [f:%r,l:%r] {h:%r,w:%r} %r' % + (len(lines), cursor, w.first, w.last, + w.height, w.width, len(lines[0]))) + + return + elif cx == len(lines[cy]): + c = ' ' + else: + c = lines[cy][cx] + self.win.addch(self.y-1, cx + len(self.mini_prompt), c, + curses.A_REVERSE) + def draw_nothing(self): + l = self.x - 1 + self.win.addnstr(self.y-1, 0, util.pad('', l), l) + +def open_aes_file(path, nl, name=None): + p = getpass.getpass("Please enter the AES password: ") + b = buffer.AesBuffer(path, p, nl, name) + return b + +def open_plain_file(path, nl, name=None): + b = buffer.FileBuffer(path, nl, name) + return b + +if __name__ == "__main__": + ciphers = { 'none': open_plain_file, + 'aes': open_aes_file } + + linetypes = { 'win': '\r\n', + 'mac': '\r', + 'unix': '\n' } + + import optparse + + parser = optparse.OptionParser() + parser.set_defaults(debug=False) + parser.set_defaults(goto=None) + parser.set_defaults(mode=None) + parser.set_defaults(cipher='none') + parser.set_defaults(linetype='unix') + + parser.add_option('-d', '--debug', dest='debug', action='store_true', + help='run in debug mode') + parser.add_option('-e', '--encrypt', dest='cipher', metavar='CIPHER', + help='decrypt and encrypt with CIPHER (default: none)') + parser.add_option('-g', '--goto', dest='goto', metavar='NUM', type='int', + help='jump to line NUM of the first argument') + parser.add_option('-l', '--line-end', dest='linetype', metavar='TYPE', + help='use TYPE (win,mac,unix) line endings (default: unix)') + parser.add_option('-m', '--mode', dest='mode', metavar='MODE', + help='open arguments in MODE') + + (opts, args) = parser.parse_args() + + # if debugging, disable error handling to produce backtraces + if opts.debug: + mode.DEBUG = True + + # we will support using +19 as the first argument to indicate opening the + # first file on line 19 (same as -g 19 or --goto 19) + if len(sys.argv) > 1 and args[0].startswith('+'): + opts.goto = int(args[0][1:]) + args = args[1:] + + # figure out which kind of line types we're using + if opts.linetype not in linetypes: + sys.stderr.write('invalid linetype: %r' % opts.linetype) + sys.exit(1) + nl = linetypes[opts.linetype] + + # figure out what kind of file open function to use + if opts.cipher not in ciphers: + sys.stderr.write('invalid cipher: %r' % opts.cipher) + sys.exit(2) + f = ciphers[opts.cipher] + + # open each path using our callback to get a buffer, open that buffer, etc. + buffers = [] + names = sets.Set() + paths = sets.Set() + for path in args: + path = os.path.abspath(os.path.realpath(util.expand_tilde(path))) + if path in paths: + continue + name = os.path.basename(path) + if name in names: + i = 1 + auxname = '%s/%d' % (name, i) + while auxname in names: + i += 1 + auxname = '%s/%d' % (name, i) + name = auxname + b = f(path, nl, name) + b.open() + buffers.append(b) + paths.add(path) + names.add(name) + + # ok, now run our app + run(buffers, opts.goto, opts.mode) diff --git a/buffer.py b/buffer.py new file mode 100644 index 0000000..b5827e3 --- /dev/null +++ b/buffer.py @@ -0,0 +1,516 @@ +import md5, os, sets, shutil +import aes, point, method, regex + +# set this to 0 or less to have infinite undo/redo +REDO_STACK_LIMIT = 1024 +UNDO_STACK_LIMIT = 1024 + +# abstract class +class Buffer(object): + def __init__(self, nl='\n'): + self.lines = [""] + self.windows = {} + self.undo_stack = [] + self.redo_stack = [] + assert nl in ('\n', '\r', '\r\n'), "Invalid line ending" + self.nl = nl + self.modified = False + + def num_chars(self): + n = 0 + for line in self.lines[:-1]: + n += len(line) + 1 + n += len(self.lines[-1]) + return n + + # basic file operation stuff + def _open_file_r(self, path): + path = os.path.realpath(path) + if not os.path.isfile(path): + raise Exception, "Path '%s' does not exist" % (path) + if not os.access(path, os.R_OK): + raise Exception, "Path '%s' cannot be read" % (path) + f = open(path, 'r') + return f + def _open_file_w(self, path): + if os.path.isfile(path): + raise Exception, "Path '%s' already exists" % (path) + d = os.path.dirname(path) + if not os.access(d, os.R_OK): + raise Exception, "Dir '%s' cannot be read" % (path) + if not os.access(d, os.W_OK): + raise Exception, "Dir '%s' cannot be written" % (path) + f = open(path, 'w') + return f + def _temp_path(self, path): + (dirname, basename) = os.path.split(path) + return os.path.join(dirname, ".__%s__pmacs" % (basename)) + + # undo/redo stack + def add_to_stack(self, move, stack="undo"): + if stack == "undo": + self.redo_stack = [] + self.undo_stack.append(move) + if UNDO_STACK_LIMIT > 0: + while len(self.undo_stack) > UNDO_STACK_LIMIT: + self.undo_stack.pop(0) + elif stack == "redo": + self.redo_stack.append(move) + if REDO_STACK_LIMIT > 0: + while len(self.redo_stack) > REDO_STACK_LIMIT: + self.redo_stack.pop(0) + elif stack == "none": + self.undo_stack.append(move) + if UNDO_STACK_LIMIT > 0: + while len(self.undo_stack) > UNDO_STACK_LIMIT: + self.undo_stack.pop(0) + else: + raise Exception, "Invalid stack to add to: %s" % (stack) + def restore_move(self, move, stack="redo"): + if move[0] == "insert": + self.insert_string(move[1], move[2], stack=stack) + elif move[0] == "delete": + self.delete_string(move[1], move[2], stack=stack) + else: + raise Exception, "Invalid undo move type: '%s'" % (move[0]) + def undo(self): + if len(self.undo_stack): + move = self.undo_stack.pop(-1) + self.restore_move(move, stack="redo") + else: + raise Exception, "Nothing to Undo!" + def redo(self): + if len(self.redo_stack): + move = self.redo_stack.pop(-1) + self.restore_move(move, stack="none") + else: + raise Exception, "Nothing to Redo!" + + # window-buffer communication + def add_window(self, w, name): + assert name not in self.windows, "window %r already exists" % name + self.windows[name] = w + def remove_window(self, name): + del self.windows[name] + def _region_added(self, p, xdiff, ydiff, str=None, stack="undo"): + y = p.y + ydiff + if ydiff == 0: + x = p.x + xdiff + else: + x = xdiff + p2 = point.Point(x, y) + move = ["delete", p, p2, str] + self.add_to_stack(move, stack) + for w in self.windows.itervalues(): + w._region_added(p, xdiff, ydiff, str) + def _region_removed(self, p1, p2, str=None, stack="undo"): + move = ["insert", p1, str] + self.add_to_stack(move, stack) + for w in self.windows.itervalues(): + w._region_removed(p1, p2, str) + def has_window(self, name): + return name in self.windows + def get_window(self, name): + if name in self.windows: + return self.windows[name] + else: + raise Exception, "uh oh %r" % self.windows + + # internal validation + def _validate_point(self, p): + self._validate_xy(p.x, p.y) + def _validate_xy(self, x, y): + assert y >= 0 and y < len(self.lines), \ + "xy1: %d >= 0 and %d < %d" % (y, y, len(self.lines)) + assert x >= 0 and x <= len(self.lines[y]), \ + "xy2: %d >= 0 and %d <= %d" % (x, x, len(self.lines[y])) + def _validate_y(self, y): + assert y >= 0 and y < len(self.lines), \ + "y: %d >= 0 and %d < %d" % (y, y, len(self.lines)) + + # internal + def make_string(self, start=0, end=None, nl='\n'): + assert end is None or start < end + if start == 0 and end is None: + return nl.join(self.lines) + else: + lines = [] + i = 0 + offset = 0 + while i < len(self.lines): + l = self.lines[i] + if offset + len(l) < start: + pass + elif offset <= start: + if end is None or offset + len(l) < end: + lines.append(l[start - offset:]) + else: + lines.append(l[start - offset:end - offset]) + elif end is None or offset + len(l) < end: + lines.append(l) + else: + lines.append(l[:end]) + offset += len(l) + 1 + i += 1 + return nl.join(lines) + + # methods to be overridden by subclasses + def name(self): + return "Generic" + def close(self): + pass + def open(self): + pass + def changed(self): + return self.modified + def reload(self): + raise Exception, "%s reload: Unimplemented" % (self.name()) + def save_as(self, path, force=False): + # check to see if the path exists, and if we're prepared to overwrite it + # if yes to both, get its mode so we can preserve the path's permissions + mode = None + if os.path.exists(path): + if force: + mode = os.stat(self.path)[0] + else: + raise Exception, "oh no! %r already exists" % path + + # create the string that we're going to write into the file + data = self.write_filter(self.make_string(nl=self.nl)) + + # create a safe temporary path to write to, and write out data to it + temp_path = self._temp_path() + f2 = self._open_file_w(temp_path) + f2.write(data) + f2.close() + + # move the temporary file to the actual path; maybe change permissions + shutil.move(temp_path, path) + if mode: + os.chmod(path, mode) + + # the file has not been modified now + self.modified = False + def readonly(self): + return False + def read_filter(self, data): + return data + def write_filter(self, data): + return data + + # point retrieval + def get_buffer_start(self): + return point.Point(0, 0, "logical") + def get_buffer_end(self): + y = len(self.lines) - 1 + return point.Point(len(self.lines[y]), y, "logical") + def get_line_start(self, y): + self._validate_y(y) + return Point(0, y, "logical") + def get_line_end(self, y): + self._validate_y(y) + return Point(len(self.lines[y]), y, "logical") + def get_point_offset(self, p): + '''used to find positions in data string''' + self._validate_point(p) + offset = 0 + for line in self.lines[:p.y]: + offset += len(line) + 1 + offset += p.x + return offset + def get_offset_point(self, offset): + i = 0 + y = 0 + for line in self.lines: + if i + len(line) + 1 > offset: + break + else: + i += len(line) + 1 + y += 1 + return point.Point(offset - i, y) + + # data retrieval + def get_character(self, p): + self._validate_point(p) + if p.x == len(self.lines[p.y]): + if p1.y < len(self.lines): + return "\n" + else: + return "" + else: + return self.lines[p.y][p.x] + def get_substring(self, p1, p2): + self._validate_point(p1) + self._validate_point(p2) + assert p1 <= p2, "p1.x (%d) > p2.x (%d)" % (p1.x, p2.x) + if p1 == p2: + return "" + elif p1.y == p2.y: + return self.lines[p1.y][p1.x:p2.x] + else: + if p1.x == 0: + text = "%s\n" % (self.lines[p1.y]) + else: + text = "%s\n" % (self.lines[p1.y][p1.x:]) + for i in range(p1.y+1, p2.y): + text = "%s%s\n" % (text, self.lines[i]) + if p2.x > 0: + text = "%s%s" % (text, self.lines[p2.y][:p2.x]) + return text + + def set_data(self, d, force=False): + if not force and self.readonly(): + raise Exception, "set_data: buffer is readonly" + start = self.get_buffer_start() + end = self.get_buffer_end() + self.delete_string(start, end, force=force) + self.insert_string(start, d, force=force) + self.modified = True + + # insertion into buffer + def insert_string(self, p, s, stack="undo", force=False): + if not force: + assert not self.readonly(), "insert_string: buffer is read-only" + new_lines = s.split("\n") + if len(new_lines) > 1: + xdiff = len(new_lines[-1]) - p.x + else: + xdiff = len(new_lines[-1]) + ydiff = len(new_lines) - 1 + new_lines[0] = self.lines[p.y][:p.x] + new_lines[0] + new_lines[-1] = new_lines[-1] + self.lines[p.y][p.x:] + self.lines[p.y:p.y+1] = new_lines + self._region_added(p, xdiff, ydiff, s, stack) + self.modified = True + + # deletion from buffer + def delete_character(self, p, stack="undo", force=False): + """delete character at (x,y) from the buffer""" + if not force: + assert not self.readonly(), "delete_character: buffer is read-only" + self._validate_point(p) + x, y = p.x, p.y + if p.x < len(self.lines[p.y]): + s = self.lines[y][x] + self.lines[y] = "%s%s" % (self.lines[y][:x], self.lines[y][x+1:]) + self._region_removed(p, p.offset(1, 0, "logical"), str=s, stack=stack) + elif p.y < len(self.lines) - 1: + s = "\n" + self.lines[y:y+2] = ["%s%s" % (self.lines[y], self.lines[y+1])] + self._region_removed(p, point.Point(0, p.y + 1, "logical"), str="\n", stack=stack) + self.modified = True + def delete_string(self, p1, p2, stack="undo", force=False): + """delete characters from p1 up to p2 from the buffer""" + if not force: + assert not self.readonly(), "delete_string: buffer is read-only" + self._validate_xy(p1.x, p1.y) + self._validate_xy(p2.x, p2.y) + if p1 == p2: + return + assert p1 < p2, "p1 %r > p2 %r" % (p1, p2) + + s = self.get_substring(p1, p2) + + if p1.y < p2.y: + start_line = self.lines[p1.y][:p1.x] + end_line = self.lines[p2.y][p2.x:] + self.lines[p1.y:p2.y+1] = ["%s%s" % (start_line, end_line)] + elif p1.y == p2.y: + if p1.x == p2.x - 1: + s = self.lines[p1.y][p1.x] + self.delete_character(p1, stack=stack) + # make sure we don't call _region_removed twice, so return + return + elif p1.x < p2.x: + s = self.lines[p1.y][p1.x:p2.x] + self.lines[p1.y] = "%s%s" % (self.lines[p1.y][:p1.x], + self.lines[p1.y][p2.x:]) + else: + raise Exception, "p1.x (%d) >= p2.x (%d)" % (p1.x, p2.x) + else: + raise Exception, "p1.y (%d) > p2.y (%d)" % (p1.y, p2.y) + + self._region_removed(p1, p2, str=s, stack=stack) + self.modified = True + + # random + def count_leading_whitespace(self, y): + line = self.lines[y] + m = regex.leading_whitespace.match(line) + if m: + return m.end() + else: + # should not happen + raise Exception, "iiiijjjj" + return 0 + +# scratch is a singleton +scratch = None +class ScratchBuffer(Buffer): + def __new__(cls, *args, **kwargs): + global scratch + if scratch is None: + scratch = object.__new__(ScratchBuffer, *args, **kwargs) + return scratch + def name(self): + return "*Scratch*" + def close(self): + global scratch + scratch = None + +class DataBuffer(Buffer): + def __init__(self, name, data, nl='\n'): + Buffer.__init__(self, nl) + self._name = name + self.lines = data.split("\n") + def name(self): + return self._name + def close(self): + pass + def readonly(self): + return True + +# console is another singleton +console = None +class ConsoleBuffer(Buffer): + def __new__(cls, *args, **kwargs): + global console + if console is None: + b = object.__new__(ConsoleBuffer, *args, **kwargs) + console = b + return console + def __init__(self, nl='\n'): + Buffer.__init__(self, nl) + lines = ['Python Console\n', + "Evaluate python expressions in the editor's context (self)\n", + 'Press Control-] to exit\n', + '\n'] + console.set_data(''.join(lines), force=True) + def name(self): + return '*Console*' + def changed(self): + return False + def close(self): + global console + console = None + def readonly(self): + return True + +class FileBuffer(Buffer): + def __init__(self, path, nl='\n', name=None): + '''fb = FileBuffer(path)''' + Buffer.__init__(self, nl) + self.path = os.path.realpath(path) + self.checksum = None + if name is None: + self._name = os.path.basename(self.path) + else: + self._name = name + if os.path.exists(self.path) and not os.access(self.path, os.W_OK): + self._readonly = True + else: + self._readonly = False + def readonly(self): + return self._readonly + + def _open_file_r(self, path=None): + if path is None: + path = self.path + path = os.path.realpath(path) + self.path = path + if not os.path.isfile(path): + raise Exception, "Path '%s' does not exist" % (path) + if not os.access(path, os.R_OK): + raise Exception, "Path '%s' cannot be read" % (path) + f = open(path, 'r') + return f + def _open_file_w(self, path=None): + if path is None: + path = self.path + if os.path.isfile(path): + raise Exception, "Path '%s' already exists" % (path) + d = os.path.dirname(path) + if not os.access(d, os.R_OK): + raise Exception, "Dir '%s' cannot be read" % (path) + if not os.access(d, os.W_OK): + raise Exception, "Dir '%s' cannot be written" % (path) + f = open(path, 'w') + return f + def _temp_path(self, path=None): + if path is None: + path = self.path + (dirname, basename) = os.path.split(path) + return os.path.join(dirname, ".__%s__pmacs" % (basename)) + + # methods for dealing with the underlying resource, etc. + def name(self): + #return self.path + return self._name + def path_exists(self): + return os.path.exists(self.path) + def store_checksum(self, data): + self.checksum = md5.new(data) + def read(self): + if self.path_exists(): + f = self._open_file_r() + data = f.read() + f.close() + self.store_checksum(data) + else: + data = '' + data = self.read_filter(data) + #FIXME: this is horrible...but maybe not as horrible as using tabs?? + data = data.replace("\t", " ") + return data + def open(self): + data = self.read() + self.lines = data.split(self.nl) + def reload(self): + self.open() + def changed_on_disk(self): + assert self.checksum is not None + f = open(self.path) + data = f.read() + f.close() + m = md5.new(data) + return self.checksum.digest() != m.digest() + def save(self, force=False): + if self.readonly(): + raise Exception, "can't save a read-only file" + + if self.checksum is not None and force is False: + # the file already existed and we took a checksum so make sure it's + # still the same right now + if not self.path_exists(): + raise Exception, "oh no! %r disappeared!" % self.path + if self.changed_on_disk(): + raise Exception, "oh no! %r has changed on-disk!" % self.path + + temp_path = self._temp_path() + data = self.make_string(nl=self.nl) + data = self.write_filter(data) + + f2 = self._open_file_w(temp_path) + f2.write(data) + f2.close() + + if self.path_exists(): + mode = os.stat(self.path)[0] + os.chmod(temp_path, mode) + + shutil.move(temp_path, self.path) + self.store_checksum(data) + self.modified = False + def save_as(self, path): + self.path = path + self.save() + +class AesBuffer(FileBuffer): + def __init__(self, path, password, nl='\n'): + '''fb = FileBuffer(path)''' + FileBuffer.__init__(self, path, nl) + self.password = password + def read_filter(self, data): + return aes.decrypt(data, self.password) + def write_filter(self, data): + return aes.encrypt(data, self.password) diff --git a/bufferlist.py b/bufferlist.py new file mode 100644 index 0000000..ae4f027 --- /dev/null +++ b/bufferlist.py @@ -0,0 +1,104 @@ +import sets + +class Slot: + def __init__(self, height, width, offset, buffer=None): + self.height = height + self.width = width + self.offset = offset + self.buffer = buffer + self.resize(height, width, offset) + def is_empty(self): + return self.buffer is None + def resize(self, height, width, offset): + self.height = height + self.width = width + self.offset = offset + # possible callbacks + def remove(self): + # possible callbacks + pass + +class BufferList: + def __init__(self, height, width, buffers=()): + self.slots = [] + self.add_slot(height, width, 0) + + self.buffers = sets.Set() + self.buffer_names = {} + self.hidden_buffers = [] + for b in buffers: + self.add_buffer(b) + + # manipulate slots + def add_slot(self, height, width, offset=0, buffer=None): + self.slots.append(Slot(height, width, offset, buffer)) + return len(self.slots) - 1 + def empty_slot(self, i): + assert i > -1 and i < len(self.slots), "slot %d does not exist" % i + return self.slots[i].is_empty() + def set_slot(self, i, b): + assert i > -1 and i < len(self.slots), "slot %d does not exist" % i + assert b in self.buffers, "buffer %s does not exist" % (b.name()) + if b in self.hidden_buffers: + self.hidden_buffers.remove(b) + if not self.slots[i].is_empty(): + b2 = self.slots[i].buffer + self.hidden_buffers.insert(0, b2) + self.slots[i].buffer = b + def remove_slot(self, i): + assert i > -1 and i < len(self.slots), "slot %d does not exist" % i + if not self.slots[i].is_empty(): + b = self.slots[i].buffer + self.hidden_buffers.insert(0, b) + self.slots[i].remove() + del self.slots[i] + + # now fix the stored slot numbers for all the + for b in self.buffers: + for j in range(i, len(self.slots)): + if b.has_window(j+1): + w = b.get_window(j+1) + del b.windows[j+1] + w.slot = j + b.windows[j] = w + + # add/remove buffers + def add_buffer(self, b): + assert b not in self.buffers, "buffer %s already exists" % (b.name()) + self.buffers.add(b) + self.buffer_names[b.name()] = b + self.hidden_buffers.append(b) + for i in range(0, len(self.slots)): + if self.empty_slot(i): + self.set_slot(i, b) + def has_buffer(self, b): + return b in self.buffers + def has_buffer_name(self, name): + return name in self.buffer_names + def get_buffer_by_name(self, name): + return self.buffer_names[name] + def get_buffer_by_path(self, path): + for b in self.buffers: + if hasattr(b, 'path') and b.path == path: + return b + return None + def remove_buffer(self, b): + assert b in self.buffers, "buffer %s does not exist" % (b.name()) + for slot in self.slots: + if slot.buffer is b: + slot.buffer = None + self.buffers.remove(b) + del self.buffer_names[b.name()] + if b in self.hidden_buffers: + self.hidden_buffers.remove(b) + + # query buffers + def is_buffer_hidden(self, b): + assert b in self.buffers, "buffer %s does not exist" % (b.name()) + return b in self.hidden_buffers + def is_buffer_visible(self, b): + assert b in self.buffers, "buffer %s does not exist" % (b.name()) + for slot in self.slots: + if slot.buffer is b: + return True + return False diff --git a/cache.py b/cache.py new file mode 100644 index 0000000..0bbb1d6 --- /dev/null +++ b/cache.py @@ -0,0 +1,60 @@ +import bisect, time + +class CacheDict(dict): + """This class works like a basic dictionary except that you can put + constraints on its size. Once that size is reached, the key that was + inserted or accessed the least recently is removed every time a new key + is added.""" + def __init__(self, max_size=1000000): + '''CacheDict(max_size=1000000): build a cache''' + # once max_size is reached, the oldest cache entry will be + # pushed out to make room for each new one + self.max_size = max_size + dict.__init__(self) + # _times_dict will map keys to timestamps + self._times_dict = {} + # _times_list will store (timestamp, key) pairs in sorted + # order (oldest first) + self._times_list = [] + + def timestamp(self, key): + '''find the timestamp for key''' + assert key in self + # construct a (timestamp, key) item + item = (self._times_dict[key], key) + # look for the item in the (sorted) list + i = bisect.bisect_left(self._times_list, item) + # make sure the index we are returning really is valid + if item != self._times_list[i]: + raise LookupError + return i + + def __getitem__(self, key): + # find the value in the dict + value = dict.__getitem__(self, key) + # do this to update the timestamp on this key + self[key] = value + return value + + def __setitem__(self, key, value): + # delete any old instance of the key to make way for the new + if key in self: + del self._times_list[self.timestamp(key)] + # remove old keys until we have enough space to add this one + while len(self._times_list) >= self.max_size: + key = self._times_list[0][1] + del self[key] + # add this key, create a timestamp, and update our other data + # structures accordingly + t = time.time() + dict.__setitem__(self, key, value) + self._times_dict[key] = t + # make sure we keep the list sorted + bisect.insort_left(self._times_list, (t, key)) + + def __delitem__(self, key): + # we need to make sure we delete this key out of all three of + # our data structures + del self._times_list[self.timestamp(key)] + del self._times_dict[key] + dict.__delitem__(self, key) diff --git a/code_examples/DataIntegrator.pm b/code_examples/DataIntegrator.pm new file mode 100644 index 0000000..ab4cf70 --- /dev/null +++ b/code_examples/DataIntegrator.pm @@ -0,0 +1,3169 @@ +package TBB::BenefitDelivery::DataIntegrator; + +use strict; +use warnings; + +use TBB::Crash; +use Data::Dumper; + +use TBB::LogManager; +use TBB::Resource::BNode; +use TBB::ID_old; +use TBB::Resource::Condition; +use TBB::Expression; +use TBB::XML; +use TBB::ID; +use TBB::Utils::Method qw/argument named_argument/; + +##?REFACTOR: Make the word 'ordinal' TBB::Crash::crash1 everywhere in this module +# EXCEPT when we are actually setting things in the XML +# (which will be used by the XSL). +# Also, while I'm suggesting global changes, I'm going +# to go on record saying ALL methods in this module should +# use opts hashes rather than the long param lists they +# currently have. + +=head1 NAME + +TBB::BenefitDelivery::DataIntegrator + +=head1 SYNOPSIS + + use TBB::BenefitDelivery::DataIntegrator; + + my $data_integrator = TBB::BenefitDelivery::DataIntegrator->new($bnode); + + my $user_data = TBB::UserData->new(); + $user_data->add($some_data); + + my $action = '/tbb/bds'; + my $primary_lang = 'en-US'; + my $secondary_lang = 'es'; + my $id_user = get_client_id_user(); + + $data_integrator->populate_bnode($action, + $primary_lang, + $secondary_lang, + $user_data); + + my $integrated_bnode = $data_integrator->get_bnode(); + +=head2 Description + +TBB::BenefitDelivery::DataIntegrator populates a bnode with various pieces of dynamic data. +It populates the bnode with language preferences and the form action, and it +pre-populates the MQs with any pre-existing data. For group questions, the +DataIntegrator determines how many instances of the question should be displayed +and what id_users are be associated with each instance. + + +=item new($bnode) + + Constructor + + $bnode is the TBB::Resource::BNode object you want to populate + +=cut +sub new +{ + my $type = shift; + my $class = (ref $type) || $type; + my ($bnode, %opts) = @_; + + my $self = + { + bnode => $bnode, + system_hidden_field_node_name => 'system_field', + ##?REFACTOR: don't use 'descendant::...' here (*especially* here) + ##?REFACTOR: see my comments in __populate_glossary + system_hidden_field_parent_node_xpath => 'descendant::body', + gq_question_default_order => $TBB::Config->get_config_parameters('order'=>'bds_parameters/gq_group_question_default_order') || 'question', + resource_manager => ( $opts{resource_manager} || $TBB::BenefitDelivery::ResourceManager), + bmod_id => $opts{bmod_id}, + primary_lang => '', + secondary_lang => '', + }; + $self->{system_hidden_field_xpath} = $self->{system_hidden_field_parent_node_xpath} . '/system_hidden_fields'; + + ##?REFACTOR: Move the following to the populate_bnode method + # Just check (out of paranoia) to make sure we haven't been given a redirect bnode to integrate. + TBB::Crash::crash2 "DataIntegrator mistakenly called on to integrate " + . $bnode->get_id() + . " which is a REDIRECT bnode!\n" + if ($bnode->is_redirect_bnode()); + + # Equally, we really shouldn't be trying to integrate end BNodes. + TBB::Crash::crash3 "DataIntegrator mistkenly called on to integrate END bnode.\n" if $bnode->is_end(); + + # If necessary, add a system_hidden_fields node to the BNode tree. + my $bnode_tree = $bnode->get_xml_resource(); + + my $shf_nodes = $bnode_tree->get_nodes_by_xpath($bnode_tree->get_root(), + $self->{system_hidden_field_xpath}); + unless (scalar @$shf_nodes) + { + ##?REFACTOR: move this unless block to its own private method + my $new_shf_node = $bnode_tree->create_node('system_hidden_fields'); + my $potential_parents_nodelist = $bnode_tree->get_nodes_by_xpath($bnode_tree->get_root(), + $self->{system_hidden_field_parent_node_xpath}); + + TBB::Crash::crash4 "Should be one-and-only-one parent node for System Hidden Fields found by '" + . $self->{system_hidden_field_parent_node_xpath} + . ". But found " + . (scalar @$potential_parents_nodelist || "0") + . " nodes that match!\n" + unless ((scalar @$potential_parents_nodelist) == 1); + my $parent_node = $potential_parents_nodelist->[0]; + $bnode_tree->append_child($parent_node, $new_shf_node); + } + + $shf_nodes = $bnode_tree->get_nodes_by_xpath($bnode_tree->get_root(), + $self->{system_hidden_field_xpath}); + + ##?REFACTOR: This looks suspiciously like debugging stuff. + unless (scalar @$shf_nodes) + { + TBB::Crash::crash5 "Here's the problem: That xpath don't set shit!\n"; + } + + # Bless self into the class. + bless ($self, $class); + + return $self; +} + +=item get_bnode() + + Returns the $bnode attribute. Call this after calling populate_bnode to + retrieve the resulting bnode. + +=cut +sub get_bnode +{ + my $self = shift; + + return $self->{bnode}; +} + +=item populate_bnode($action, $primary_lang, $secondary_lang, + $user_data_obj, $resource_manager) + + For each group gq, populates all the necessary instances of the gq. + Pre-populates with default values from $user_data (if necessary). + Populates the primary and secondary language preferences. + Populates the action attribute of the form with the value of $action. + + $action is the form action + + $primary_lang is the primary language preference + + $secondary_lang is the secondary language preference + + $user_data_obj is a reference to a TBB::UserData object + + $resource_manager is a reference to the TBB::BenefitDelivery::ResourceManager object. + if $resource_manager is not provided, it uses the global + $TBB::BenefitDelivery::ResourceManager object + +=cut +sub populate_bnode +{ + my $self = shift; + my ($action, $primary_lang, $secondary_lang, $user_data, %opts) = @_; + + my $subtime = TBB::LogManager::log_elapsed_time(undef, "POPULATE: starting"); + #&TBB::LogManager::write_log('crit', "populate_bnode(): primary_lang = \"$primary_lang\", secondary_lang = \"$secondary_lang\"\n"); + $self->{'primary_lang'} = $primary_lang; + $self->{'secondary_lang'} = $secondary_lang; + + # Initialize an empty opts hash if we weren't passed one. + %opts = () unless %opts; + + # Process optional_arguments. + + ##?REFACTOR: Can we just do this the New School way and set a $self->{resource_manager} in + ##?REFACTOR: the constructor now? + my $resource_manager = $opts{resource_manager} || $TBB::BenefitDelivery::ResourceManager; + my $navigator = $opts{navigator}; + $self->{navigator} = $navigator; + + ##?TEST: + TBB::Crash::crash6 "populate_bnode : Given a resource_manager object which is a '" + . (ref $resource_manager) + . "' not a TBB::BenefitDelivery::ResourceManager!\n" + unless ((ref $resource_manager) eq 'TBB::BenefitDelivery::ResourceManager'); + TBB::Crash::crash7 "populate_bnode : Given a user_data object which is a '" + . (ref $user_data) + . "' not a TBB::UserData!\n" + unless ( ((ref $user_data) eq 'TBB::UserData') || ((ref $user_data) eq 'TBB::UserData::New') + || ($opts{test} && ((ref $user_data) eq 'Test::MockObject'))); + TBB::Crash::crash8 "populate_bnode : I now require a TBB::Navigator!\n" + unless ( + (ref $navigator eq 'TBB::Navigator') + || + (ref $navigator eq 'Test::MockObject' && $opts{test}) + ); + + my $this_id_user = $navigator->get_this_id_user() || $user_data->current_client_id(); + my $context_id_user = $navigator->get_context_id_user(); + + TBB::LogManager::write('debug', "CONTEXT_ID_USER: \"" . ($context_id_user || "") . "\""); + # Now detect whether we need to handle and ordinal, if so which ordinal to use, and set + # an outgoing ordinal for (potential) use by a subsequent page. + # The "context_instance" refers to ANOTHER ordinal which is not this one but rather + # some other ordinal which this ordinal is intended to establish a relationship with. + my ($this_instance, $context_instance) = $self->__set_instance_fields($user_data, $this_id_user, %opts); + + my %subroutine_opts = ( + user_data => $user_data, + resource_manager => $resource_manager, + this_id_user => $this_id_user, + this_instance => $this_instance, + ); + $subroutine_opts{context_instance} = $context_instance if ($context_instance); + $subroutine_opts{context_id_user} = $context_id_user if ($context_id_user); + + $subtime = TBB::LogManager::log_elapsed_time($subtime, "POPULATE: finished init"); + + ########################################################################### + # + # Now begins a series of method calls, each of which performs a particular + # operation on the XML tree for our bnode. + # + # It's not immediately clear what order these operations should be + # performed in. Some have time dependencies on others of them, but + # in large part they are autonomous. + # + ########################################################################### + + $self->__populate_display_instancesets( + $user_data, + $resource_manager, + $this_id_user, + %subroutine_opts + ); + + $subtime = TBB::LogManager::log_elapsed_time($subtime, "POPULATE: display instancesets"); + + # Evaluate conditional blocks and drop them from the xml tree if their conditions are + # not true. + ##?NOTE: The placement of this method call at this point means that ALL conditional_blocks + # on a page are evaluated for whomever is the page user (this_id_user). So a + # conditional_block within a question within a gq_group tag will *NOT* be evaluated + # differently for group members; everyone will get the evaluation for the page user! + # This is a feature, but it may also be a bug. + $self->__evaluate_conditional_blocks( + $user_data, + $resource_manager, + %subroutine_opts + ); + $subtime = TBB::LogManager::log_elapsed_time($subtime, "POPULATE: eval conditional blocks"); + + # Insert variables set within the BNode by its processes. + $self->__insert_dynamic_variable_values ($user_data); + + ##?TODO: Move these actions to live with the rest of the hidden field stuff. + $self->__integrate_form_action( $action ); + $self->__set_system_hidden_field('bnode_id', $self->get_bnode()->get_id()); + $self->__set_system_hidden_field('set_bmod_id', $self->{bmod_id}); + $self->__integrate_language_preference($primary_lang, $secondary_lang); + + # Now handle any tags which wrap questions. We don't actually *evaluate* them + # at this point. Instead, we push the "hidden"ness down into the individual MQs and + # leave them to be evaluated by the __populate_*_gqs methods. + $self->__set_udid_hidden_fields(); + + # Drop sets are a special kind of dealio: Instead of having a set interface to use, + # they allow you to pick members of a Set (FilterSet or InstanceSet) from a list + # and store their label (id_user or instance) as a GQ:MQ value, or set it as a + # BDS navigation token. + ##?REFACTOR: Note that again the placement at this level means that these sets can't + # effectively live inside a gq_group tag. They should probably be moved. + $self->__populate_drop_filtersets($user_data, $resource_manager, %subroutine_opts); + $self->__populate_drop_instancesets( + $user_data, + $resource_manager, + $this_id_user, + %subroutine_opts + ); + $subtime = TBB::LogManager::log_elapsed_time($subtime, "POPULATE: block drop_*sets"); + + + # Set hidden fields for BDS navigation. + my $form_id = $self->__set_hidden_form_id(); + $self->__set_system_hidden_field('set_id_user', $this_id_user) + unless ($this_id_user == $user_data->current_client_id()); + $self->__set_system_hidden_field('set_context_user', $context_id_user) + if ($context_id_user); + $self->__debug_mode_on() if $opts{'debug_mode'}; + + # Handle summary fields and display tables. + $self->__populate_summary_fields( %subroutine_opts, 'form_id' => $form_id ); + $subtime = TBB::LogManager::log_elapsed_time($subtime, "POPULATE: hidden fields and summary fields"); + + # Newer, sexier, leaner summary_tables code. + $self->__populate_summary_tables( %subroutine_opts, 'form_id' => $form_id ); + + # Now go through and actually integrate the question tags -- first for single users... + $self->__populate_single_user_gqs($user_data, $this_id_user, $this_instance, %subroutine_opts); + $subtime = TBB::LogManager::log_elapsed_time($subtime, "POPULATE: populate single user gqs"); + + # ... and then for gq_groups. + $self->__populate_group_gqs($user_data, $resource_manager, $this_instance, %subroutine_opts); + $subtime = TBB::LogManager::log_elapsed_time($subtime, "POPULATE: populate group gqs"); + + # Check instances are an ugly old technology which has been superceded by the + # type. We only support them for legacy purposes. + ##?REFACTOR: Can we hassle the PA tax team to get rid of them for us so we can make + # them go away forever? + $self->__populate_check_instancesets( + $user_data, + $resource_manager, + $this_id_user, + %subroutine_opts + ); + $subtime = TBB::LogManager::log_elapsed_time($subtime, "POPULATE: check instancesets"); + + # Now make all the glossary words on the page do the right thing. + $self->__populate_glossary(); + + # Moved the call to this (the most global) here so we don't overwrite any text in questions accidentally. + $self->__populate_page_text($user_data, $this_id_user, $this_instance, %subroutine_opts); + $subtime = TBB::LogManager::log_elapsed_time($subtime, "POPULATE: glossary and page text."); + + ##?REFACTOR: Find out the usage of this and use a better name if it's used. + # such as: set_bnode_attribute + $self->__set_current_id_user($user_data) + if ($self->get_bnode()->is_current_id_user_set()); + + ###print STDERR 'THE BIG ONE: ' . $self->get_bnode()->to_string(); + TBB::LogManager::write('debug','THE BIG ONE: ' . $self->get_bnode()->to_string()); + + return 1; +} + +=item __set_current_id_user + + Sets the BNode attribute 'current_id_user' to the self's id_user + This mainly tells xslt to prepend this self id_user to some form elements in order to + proceed successfully. One example used is in instance summary pages + +=cut +sub __set_current_id_user +{ + my $self = shift; + my $user_data_obj = shift; + + my $self_id_user = $user_data_obj->current_client_id(); + my $bnode_tree = $self->get_bnode()->get_xml_resource(); + $bnode_tree->remove_attribute($bnode_tree->get_root(), "current_id_user"); + $bnode_tree->add_attributes($bnode_tree->get_root(), {'current_id_user' => $self_id_user}); +} + +=item __debug_mode_on + + Sets the BNode attribute 'debug' = 1. + This tells the XSLT to display GQIDs and do various other tricks. + +=cut +sub __debug_mode_on +{ + my $self = shift; + my $tree = $self->get_bnode()->get_xml_resource(); + $tree->add_attributes($tree->get_root(), {'debug' => "1"}); +} + +=item __set_instance_fields($user_data_obj, $id_user, %opts) + + This subroutine handles the behaviour of ordinal bnodes. + + If a bnode is ordinal then we need to get an ordinal value for it -- either passed in from + the form from the last bnode (telling us that we're still working with "this ordinal") + or else we need to ask UserData to give us a brand shiny new ordinal because we're creating + a brand new instance. + + It's also possible to get a "context ordinal" from the form data. That means that we'll + create a new instance, but that we'll associate it with some previous instance. For example, + we might create a new Box 12 but associate it with a given W-2 instance. Or we might create + a new instance of monies paid for child-care and associate it with an instance of a childcare + provider, etc. + + Finally, we'll need to pass on an ordinal value to the next bnode. We do this by setting + a system_hidden_field. The default behaviour is to pass on "this ordinal" context so + the next bnode can continue to work with the ordinal we're working with now. However, + if the current bnode has the attribute set_context_ordinal="1" set then instead we'll set + our ordinal as the "context ordinal" for the next bnode to use. In this case, we don't + pass on any "this ordinal" value, so the next bnode will create its own new instance and + associate it with the one we were using in this bnode. + +=cut +sub __set_instance_fields +{ + my $self = shift; + my ($user_data_obj, $id_user, %opts) = @_; + my ($this_instance, $context_instance); + + my $navigator = $self->{navigator}; + TBB::Crash::crash9 "I really actually need a navigator now.\n" unless $navigator; + + my $is_ordinal_bnode = $self->get_bnode()->is_ordinal(); + + if ($is_ordinal_bnode) + { + if ($navigator->get_this_instance()) { + $this_instance = $navigator->get_this_instance() + } else { + $this_instance = $user_data_obj->get_next_instance(); + } + + + ### If we create a new instance, we inform the navigator and update the current move + my $current_instance = $navigator->get_this_instance(); + if (!defined $current_instance || $this_instance ne $current_instance) + { + $navigator->set_this_instance( $this_instance, update_current_move => 1 ); + } + $context_instance = $navigator->get_context_instance(); + + # Set the ordinal property of the tree to "_$ordinal" so that it can be + # used by the XSLT transform. + + ##DEBUG: + TBB::LogManager::write( + 'debug', + $self->get_bnode()->get_id() + . " : is_ordinal='" + . $self->get_bnode()->is_ordinal() + . "' set ordinal '" + . $this_instance || '(none)' + . "' and context ordinal '" + . $context_instance || '' + . "'\n" + ) if TBB::LogManager::writes_at('debug'); + + # Just pass on the ordinal as this_ordinal and + # pass on the context ordinal as the context ordinal. Special nodes will handle + # changes between them. + $self->__set_system_hidden_field('set_ordinal', $this_instance); + $self->__set_system_hidden_field('set_context_ordinal', $context_instance) + if ($context_instance); + } + + return ($this_instance, $context_instance); +} + +=item __populate_group_gqs($user_data_obj, $resource_manager, $ordinal) + + Creates and populates all the instances of the group gqs in the bnode. + Returns 0 on success. If a filterset filters out all the members, returns + the filterset id. + + $user_data_obj is a reference to a TBB::UserData object + + $resource_manager is a reference to a TBB::BenefitDelivery::ResourceManager object. + if $resource_manager is not provided, it uses the global + $TBB::BenefitDelivery::ResourceManager object + + $ordinal is the ID of an ordinal which may have been set on this page. We DO NOT + currently process ordinals for group questions, but this function is the only place + where we know whether we process group_gqs or not. So we pass ordinal in just so + we can TBB::Crash::crash10 if it's been erroneously set. Of course, if we one day upgrade to + handle ordinals for groups of users then we need to pass ordinal in so that we + can actually make use of it. + +=cut +sub __populate_group_gqs +{ + my $self = shift; + my ($user_data_obj, $resource_manager, $ordinal, %opts) = @_; + + ##?TEST: + TBB::Crash::crash11 "__populate_group_gqs : Given a resource_manager object which is a '" + . (ref $resource_manager) + . "' not a TBB::BenefitDelivery::ResourceManager!\n" + unless ((ref $resource_manager) eq 'TBB::BenefitDelivery::ResourceManager'); + + my $bnode_tree = $self->get_bnode()->get_xml_resource(); + my $gq_groups = $bnode_tree->get_nodes_by_xpath($bnode_tree->get_root(), + "descendant::gq_group"); + + # Now TBB::Crash::crash12 if we have an ordinal set. + #TBB::Crash::crash13 "DataIntegrator is not equipped to handle ordinals for group questions!\n" + # if ((scalar @$gq_groups) && $ordinal); + + my $default_order = $self->{gq_question_default_order}; + TBB::Crash::crash14 "No gq_group question default order set!\n" + unless ($default_order); + + # We now iterate through each of the gq_group tags found in the BNode. + # Within each gq_group tags, the order in which we list questions and users + # depends on the order_by attribute: + # ORDER BY USER: list users, listing all questions under each user + # ORDER BY QUESTION: list questions, listing all users under each question + # + # Note that expanding this out is NOT done explicitly by this module. Instead, it's done by + # the XSL Transformation the "finished" BNode goes through. The job of DataIntegrator is just + # to add relevant userdata nodes to each gq node in the group, one for each user. These + # contain the appropriate data for that user and are there to facilitate the XSLT in producing + # both OBU and OBQ output pages. + # + # If this attribute is not explicitly set in a given gq_group tag then we use the $default_order + # that we dereferenced above. (And which was originally specified in tbb_bds.conf.) + foreach my $gq_group (@$gq_groups) + { + # Figure out the order_by attr to use for this gq_group + my $order_by = $bnode_tree->get_attribute($gq_group, "order_by"); + unless ($order_by) + { + $order_by = $default_order; + $bnode_tree->add_attributes($gq_group, {'order_by' => $order_by}); + } + + # Get the other attributes of this gq_group tag. + my $is_root = $bnode_tree->get_attribute($gq_group, "root"); + my $gqs = $bnode_tree->get_nodes_by_xpath($gq_group, "descendant::gq"); + my $fs_id = $bnode_tree->get_attribute($gq_group, "filterset"); + my $this_page_id_user = $bnode_tree->get_attribute($gq_group, "this_page_id_user"); + my $instanceset_id = $bnode_tree->get_attribute($gq_group, "instanceset"); + if ($instanceset_id) + { + TBB::Crash::crash15 "Ill-formed gq_group on " + . $self->get_bnode()->get_id() + . " : Can't have both a FilterSet ($fs_id) and an InstanceSet ($instanceset_id).\n" + if ($fs_id); + $self->__populate_instanceset_gq_group( + $user_data_obj, + $opts{this_id_user}, + $opts{this_instance}, + $instanceset_id, + $bnode_tree, + $gq_group + ); + next; + } + + #FilterSet::filter_data returns a sorted list of id_users + #if we need to change the sort order, do it in FilterSet + my $id_users = []; + unless ($this_page_id_user) + { + ##?REFACTOR: more comments! (delete) + #unless ($self->{navigator} && $self->{navigator}->get_this_id_user()) + #{ + TBB::Crash::crash16 "no filterset id given to group_gq!" unless $fs_id; + $id_users = $resource_manager->get_filterset($fs_id) + ->filter_data($user_data_obj, 'context_user' => $opts{context_id_user}); + #} + #print STDERR "#####id users are: " . Dumper (@$id_users); + } + else + { + if ($self->{navigator} && $self->{navigator}->get_this_id_user()) + { + ##?TODO: In this case, we should pre-select the only user in the select box. + $id_users = [ $self->{navigator}->get_this_id_user() ]; + } + else + { + $self->__add_dummy_ids($id_users, 1); + } + } + + TBB::Crash::crash17 "$order_by is not a valid way to order questions!\n" + unless ($order_by eq 'user' || $order_by eq 'question'); + + # "Root" questions are those which allow us to gather data about *new users*. Obviously, + # these users don't exist in user_data yet, nor do they have id_users. Instead, we set + # a number of "dummy_id_users" (letters from A, B, ...) as their id_users. Magic in other + # modules will pick up these dummy id users and translate them into real id_users at an + # appropriate point, but we don't have to worry about that here. + # (If you're curious, it's currently done in TBB::FormData, but that may be subject to + # change....) + if ($is_root) + { + # Root questions HAVE to be order by user. + TBB::Crash::crash18 "Sorry, at this time ROOT questions MUST be ordered by USER!\n" + if ($order_by ne 'user'); + + # Get the default_count attr of the gq_group -- which tells us what size to pad the + # list of users to. + my $default_count = $bnode_tree->get_attribute($gq_group, "default_count"); + my $id_user_count = scalar @$id_users; + my $dummy_id_count; + + # We need a number of dummy ids to pad us up to the default count, or a minimum of + # one dummy even if we're at or over the default count. + if ($id_user_count < $default_count) + { + $dummy_id_count = $default_count - $id_user_count; + } + else + { + $dummy_id_count = 1; + } + + # Call this method to add dummies to the list $id_users. + $self->__add_dummy_ids($id_users, $dummy_id_count); + } + + # Now iterate through the gq (general question) tags inside the gq_group tag. For each + # of them we are going to add userdata blocks pertaining to each user in our id_users list. + foreach my $gq (@$gqs) + { + my $gq_id = $bnode_tree->get_attribute($gq, "id"); + my $mqs = $bnode_tree->get_nodes_by_xpath($gq, 'descendant::mq'); + + foreach my $mq (@$mqs) + { + my $mq_id = $bnode_tree->get_attribute($mq, "id"); + + # Get the default value for this mq: + ##?REFACTOR: do we need to use 'descendant::...' here? if default_value + ##?REFACTOR: is always in the same relative position, we should put the exact path + ##?REFACTOR: be careful about this, maybe wait until we have a DTD or some sort of + ##?REFACTOR: xml validation script thingy + my $default_value_xpath = 'descendant::defaultvalue'; + my $default_value_node = $bnode_tree->get_single_node_by_xpath( + $mq, + $default_value_xpath, + ok_to_return_null => 1 + ); + my ($default_value, $dynamic_vars, $force_default_value, $normalization); + if ( $default_value_node ) + { + $default_value = $bnode_tree->get_node_value( $default_value_node ); + $dynamic_vars = $bnode_tree->get_attribute( $default_value_node, 'dynamic' ); + $normalization = $bnode_tree->get_attribute( $default_value_node, 'normalization' ); + } + ##?NOTE: We now have TWO ways to make this happen: + ##?NOTE: 1) we can have an + ##?NOTE: + ##?NOTE: 2) or we can have + ##?NOTE: + ##?NOTE: + ##?NOTE: + ##?NOTE: + ##?NOTE: + ##?NOTE: + ##?NOTE: We strictly need both of these methods! + ##?NOTE: The 2nd is used to include external mqs in a locally hidden + ##?NOTE: context. The 1st means the mq is *always* hidden. + ##?NOTE: Note that (some method) translates #2 into #1 before + ##?NOTE: we reach this stage. + if ($bnode_tree->get_attribute($mq, "hidden")) { + # Process hidden fields a bit differently. + # The key here is that the "default value" should override any value set in + # UserData for hidden fields, whereas it specifically shouldn't do this in all + # other cases. Additionally, this is where we evaluate "dynamic" hidden fields, + # ie. hidden fields whose value is set to the evaluant of some expression rather + # than just to a hardcoded value. + $force_default_value = 1; + } + + # Process the label for this micro-question. + my $question_label_xpath = $order_by eq 'user' + ? 'labelset/label[@context="user"]' + : 'labelset/label[@context="question"]'; + + my $question_label_txtopts = $bnode_tree->get_single_node_by_xpath( + $mq, + $question_label_xpath, + ok_to_return_null => 1 + ); + + $self->__process_text_options( + $bnode_tree, $question_label_txtopts, $user_data_obj, $id_users->[0], $ordinal + ); + + # Get the 'member' label from this mq's labelset. + my $member_label_xpath = $order_by eq 'user' + ? 'ancestor::gq_group/labelset/label[@context="user_member"]' + : 'labelset/label[@context = "question_member"]'; + + my $member_label_txtopts = $bnode_tree->get_single_node_by_xpath( + $mq, + $member_label_xpath, + ok_to_return_null => 1 + ); + + foreach my $id_user (@$id_users) { + + my $user_value = $dynamic_vars + ? $self->__evaluate_dynamic_value( + $default_value, + $id_user, + $ordinal, + $user_data_obj, + %opts, + 'normalization' => $normalization, + ) + : $default_value; + + $self->__add_question_userdata_node( + $id_user, + $gq_id, + $mq_id, + $ordinal, + $mq, + $user_data_obj, + $member_label_txtopts, + $user_value, + use_default_not_user_data => $force_default_value, + context => $order_by + ); + } + } + + } + + + #create some auxiliary nodes in the tree to assist the xslt presentation + ##?REFACTOR: refactor mq_groups so that this is no longer necessary, and then remove it + $self->__add_misc_data($gq_group, scalar @$id_users); + + } + return 0; +} + +##?REFACTOR: we need to actually do the POD + +=item __populate_instanceset_gq_group( $user_data, $id_user, $this_instance, $instanceset_id, $bnode_xml_tree, $gq_group_node, %opts ) + + TODO: POD. + +=cut +sub __populate_instanceset_gq_group +{ + my $self = shift; + my ($user_data, $id_user, $context_instance, $instanceset_id, $bnode_xml_tree, $gq_group_node, %opts) = @_; + + # Get other attributes of the gq_group node: + my $gq_node_list = $bnode_xml_tree->get_nodes_by_xpath($gq_group_node, 'descendant::gq'); + my $this_page_id_user = $bnode_xml_tree->get_attribute($gq_group_node, 'this_page_id_user'); + + # Determine order by for this group. + my $default_order = $self->{gq_question_default_order}; + my $order_by = $bnode_xml_tree->get_attribute($gq_group_node, "order_by"); + if ($order_by) + { + TBB::Crash::crash19 "'$order_by' is not a valid way to order gq groups!\n" + unless ($order_by eq 'user' | $order_by eq 'question'); + } + else + { + $order_by = $default_order; + $bnode_xml_tree->add_attributes($gq_group_node, {'order_by' => $order_by}); + } + TBB::Crash::crash20 "Problem at " . $self->get_bnode()->get_id() . ". At this time we do not support order_by USER for instanceset gq groups. Sorry.\n" + if ($order_by eq 'user'); + + # Get the instanceset for this group. + my $resource_manager = $self->{resource_manager}; + my $instanceset = $resource_manager->get_instanceset( $instanceset_id ); + + # Run the instanceset. It will internally cache the result, which we then ask for. + $instanceset->filter_data( $user_data, $id_user, $context_instance ); + + ### We pass get_users a user_data object so we are given a sorted list of users + my @is_users_list = $instanceset->get_users(user_data => $user_data); + ##?REFACTOR: This can go away now. + TBB::Crash::crash21 "Problem running '$instanceset_id' : cacheing did not occur.\n" + unless (ref \@is_users_list eq 'ARRAY'); + + foreach my $gq_node ( @$gq_node_list ) + { + my $gq_id = $bnode_xml_tree->get_attribute($gq_node, 'id'); + my $mq_node_list = $bnode_xml_tree->get_nodes_by_xpath($gq_node, 'descendant::mq'); + + foreach my $mq_node ( @$mq_node_list ) + { + my $mq_id = $bnode_xml_tree->get_attribute($mq_node, 'id'); + + # Get the default value for this mq: + my $default_value_xpath = 'descendant::defaultvalue'; + my $default_value_node = $bnode_xml_tree->get_single_node_by_xpath( + $mq_node, + $default_value_xpath, + ok_to_return_null => 1, + ); + + my ($default_value, $dynamic_vars, $force_default_value, $normalization); + if ( $default_value_node ) + { + $default_value = $bnode_xml_tree->get_node_value( $default_value_node ); + $dynamic_vars = $bnode_xml_tree->get_attribute( $default_value_node, 'dynamic' ); + $normalization = $bnode_xml_tree->get_attribute( $default_value_node, 'normalization' ); + } + + ##?TODO: Revise the following (and other methods as necessary) such that there is only + ##?TODO: one call to __add_question_userdata_node, shared by hidden and non-hidden + ##?TODO: fields alike. This is actually an iteration task as it will allow us to + ##?TODO: pre-populate non-hidden fields with FM values (something Team:Analysis has + ##?TODO: asked for. + ##?TODO: Just to note, this change needs to be made in all three "main" methods of + ##?TODO: DI -- this one, __populate_group_gqs and __populate_single_user_gqs. + if ($bnode_xml_tree->get_attribute($mq_node, "hidden")) + { + # If this MQ is hidden then we always want to prefer the default value over whatever + # is in user data. + $force_default_value = 1; + } + + ##?REFACTOR: The following block (till END_BLOCK) is pretty much verbatim from __populate_group_gqs + ##?REFACTOR: Should we consider breaking this down into more stages, which can call each other? + # Process the label for this micro-question. + my $question_label_xpath = $order_by eq 'user' + ? 'labelset/label/@context="user"' + : 'labelset/label/@context="question"'; + my $question_label_txtopts = $bnode_xml_tree->get_single_node_by_xpath( + $mq_node, + $question_label_xpath, + ok_to_return_null => 1, + ); + + # Note that we DON'T pass an $id_user into process_text_options. + # This is deliberate. The question pertains to the whole household! + ##?TODO: But is this correct here? I mean, this could be all children of current_user + ##?TODO: or it could be all pets of current_user or something. What can it hurt to + ##?TODO: pass the id_user in? + ##?TODO: Examine above and try passing it in (consider %opts) + $self->__process_text_options( + $bnode_xml_tree, $question_label_txtopts, $user_data, undef, undef + ); + + # Get the 'member' label from this mq's labelset. + my $member_label_xpath = $order_by eq 'user' + ? 'ancestor::gq_group/labelset/label[@context="user_member"]' + : 'labelset/label[@context="question_member"]'; + my $member_label_txtopts = $bnode_xml_tree->get_single_node_by_xpath( + $mq_node, + $member_label_xpath, + ok_to_return_null => 1, + ); + ##?REFACTOR: END_BLOCK (see above to know what this means) + + ##?DEBUG: Dump out the cache of users/instances generated within $instanceset. + #TBB::LogManager::write( + # 'warn', + # "DI IS PROBLEM: IS " + # . $instanceset->get_id() + # . " with UserData: " + # . ($user_data->is_new() ? "NEW" : "OLD") + # . " has cache: " + # . Dumper( $instanceset->{instance_hash} ) + #) if TBB::LogManager::writes_at("warn"); + + foreach my $this_id_user ( @is_users_list ) + { + my @this_users_instances = $instanceset->get_instances_by_user( $this_id_user ); + + my %this_id_user_opts = %opts; + $opts{this_id_user} = $this_id_user; + ##?DEBUG: + ##TBB::LogManager::write('debug', "For id_user=$this_id_user found instances: " . join(", ", @this_users_instances)); + foreach my $instance (@this_users_instances) + { + my $user_value = $dynamic_vars + ? $self->__evaluate_dynamic_value( + $default_value, $this_id_user, $instance, $user_data, %this_id_user_opts, 'normalization' => $normalization + ) + : $default_value; + #TBB::LogManager::write('debug', + # "DYNAMIC: Assigning default value for user \"" . + # ($this_id_user || "") . + # "\", instance \"" . + # ($instance || "") . + # "\": '" . + # ($user_value || "") . + # "'. [DYNAMIC_VARS = \"" . + # ($dynamic_vars || "") . + # "\"]\n") if TBB::LogManager::writes_at("debug"); + $self->__add_question_userdata_node( + $this_id_user, + $gq_id, + $mq_id, + $instance, + $mq_node, + $user_data, + $member_label_txtopts, + $user_value, + use_default_not_user_data => $force_default_value, + context => $order_by, + ); + } + + } + } + } + + return 1; +} + +=item __populate_single_user_gqs($user_data_obj, $id_user, $ordinal) + + Populates all the non-group gqs in the bnode. + These questions are all assumed to be asked of one single user, + whose id_user is set = $id_user. + + $user_data_obj is a reference to a TBB::UserData object + + If $ordinal is defined, then $ordinal should be appended to the UDID to retrieve the + correct data; and the hidden field for ordinal should be set at the same time + +=cut +sub __populate_single_user_gqs +{ + my $self = shift; + my ($user_data_obj, $id_user, $ordinal, %opts) = @_; + + my $bnode_tree = $self->{bnode}->get_xml_resource(); + + #the following two queries retrieve only the non-group gq nodes + #we assume there is only one block in the bnode + my $gqs = $bnode_tree->get_nodes_by_xpath($bnode_tree->get_root(), + "descendant::questions/gq"); + + ##?REFACTOR: Find out if we can kill this line. There are no hidden_fields by this point. + my $hidden_gqs = $bnode_tree->get_nodes_by_xpath($bnode_tree->get_root(), + "descendant::questions/hidden_field/gq"); + TBB::Crash::crash22 "Oops! We were wrong. There ARE still hidden_field tags in the XML. Why is that?\n" + if (@$hidden_gqs); + + foreach my $gq (@$gqs) + { + my $gq_id = $bnode_tree->get_attribute($gq, 'id'); + my $mqs = $bnode_tree->get_nodes_by_xpath($gq, "descendant::mq"); + + foreach my $mq (@$mqs) + { + my $mq_id = $bnode_tree->get_attribute($mq, 'id'); + + # Get the default value for this mq. + ##?REFACTOR: again, reconsider using an explicit path rather than 'descendant::...' (be careful) + my $default_value_xpath = 'descendant::defaultvalue'; + my $default_value_node = $bnode_tree->get_single_node_by_xpath( + $mq, + $default_value_xpath, + ok_to_return_null => 1 + ); + + my ($default_value, $dynamic_vars, $force_default_value, $normalization); + if ( $default_value_node ) + { + $default_value = $bnode_tree->get_node_value( $default_value_node ); + $dynamic_vars = $bnode_tree->get_attribute( $default_value_node, 'dynamic' ); + $normalization = $bnode_tree->get_attribute( $default_value_node, 'normalization' ); + } + + if ( $bnode_tree->get_attribute($mq, "hidden") ) + { + # Process hidden fields a bit differently. + # The key here is that the "default value" should override any value set in + # UserData for hidden fields, whereas it specifically shouldn't do this in all + # other cases. Additionally, this is where we evaluate the sexy new "dynamic" + # hidden fields. Ooooooooooh. + + $force_default_value = 1; + }; + + my $user_label_xpath = 'labelset/label[@context="user"]'; + my $user_label_txtopts = $bnode_tree->get_single_node_by_xpath( + $mq, + $user_label_xpath, + ok_to_return_null => 1 + ); + + ##?TODO: The following line is objectively wrong. We should NOT be mucking with the original + ##?TODO: labelset for this question. Instead, we should pass $user_label_txtopts on to the + ##?TODO: __add_question_userdata method, which will process them directly. + ##?TODO: The trouble is, the XLST for single user questions doesn't know to look in the "right + ##?TODO: place" -- ie. in the blocks -- to find labels, it still looks in the toplevel + ##?TODO: labelset. Since we are v. close to the FLFS launch and messing with the XSLT might have + ##?TODO: unexpected consequences, I am leaving this "wrong" in this method. + ##?TODO: So the actual "TODO" is to change the XSLT to look only in blocks for + ##?TODO: labels and to use them, then to comment out the following two lines. + $self->__process_text_options($bnode_tree, $user_label_txtopts, $user_data_obj, $id_user, $ordinal, context => 'user'); + undef $user_label_txtopts; + + my $user_value = $dynamic_vars + ? $self->__evaluate_dynamic_value( + $default_value, $id_user, $ordinal, $user_data_obj, %opts, 'normalization' => $normalization + ) + : $default_value; + + #TBB::LogManager::write('debug', "SINGLE USER QUESTION $gq_id:$mq_id with label: " . ( $user_label_txtopts ? $user_label_txtopts->toString() : "NONE") ); + #TBB::LogManager::write('debug', "WHICH IS UNDERSTANDABLE given that:\n\n" . Dumper( $mq->toString() ) ); + + $self->__add_question_userdata_node( + $id_user, + $gq_id, + $mq_id, + $ordinal, + $mq, + $user_data_obj, + $user_label_txtopts, + $user_value, + use_default_not_user_data => $force_default_value, + context => 'user' + ); + } + } +} + +=item __insert_dynamic_variable_values + +__insert_dynamic_variable_values ($user_data_obj, $resource_manager) -> $success + +__insert_dynamic_variable_values inserts the values of dynamic variables set +by the bnode set_dynamic_variable() method (at the moment, just by TBB::Processes). + +The values are inserted as CDATA text in the element. + +=cut +sub __insert_dynamic_variable_values +{ + my $self = shift; + my $bnode_tree = $self->{bnode}->get_xml_resource(); + my $dynamic_variable_elements = $bnode_tree->get_nodes_by_xpath( + $bnode_tree->get_root(), + "descendant::insert_dynamic_variable" + ); + foreach my $dynamic_variable_element (@$dynamic_variable_elements) + { + my $dynamic_variable_name = + $bnode_tree->get_attribute ($dynamic_variable_element, "name"); + unless ($dynamic_variable_name) + { + TBB::LogManager::write( + 'warn', + "No dynamic variable name found in dynamic variable element." + ); + next; + } + my ($dynamic_variable_value) = + $self->{bnode}->get_dynamic_variable($dynamic_variable_name); + $bnode_tree->set_node_value ( $dynamic_variable_element, $dynamic_variable_value ); + } + return 1; +} + + +=item __evaluate_conditional_blocks + +__evaluate_conditional_blocks ($user_data_obj, $resource_manager) -> $success + +__evaluate_conditional_blocks evaluates the condition in each +and drops the block entirely if the condition does not evaluate true. + +##?TODO: This should also evaluate dynamically set bnode variables when they +##?TODO: are specified as an attribute of the conditional_block + +=cut +sub __evaluate_conditional_blocks +{ + my $self = shift; + my ($user_data_obj, $resource_manager, %opts) = @_; + + my $ecbtime = TBB::LogManager::log_elapsed_time(undef, "ecb: starting __evaluate_conditional_blocks"); + + my $id_user = $opts{this_id_user} || $user_data_obj->current_client_id(); + my $this_instance = $opts{this_instance}; + + my $bnode_tree = $self->{bnode}->get_xml_resource(); + my $conditional_blocks = $bnode_tree->get_nodes_by_xpath( + $bnode_tree->get_root(), + "descendant::conditional_block" + ); + + $ecbtime = TBB::LogManager::log_elapsed_time($ecbtime, "ecb: init complete"); + + my $looptime = TBB::LogManager::log_elapsed_time(undef, "starting loop"); + ## test condition + foreach my $conditional_block ( @$conditional_blocks ) + { + TBB::LogManager::write("debug", "In a conditional block."); + my $dynamic_evaluation = $bnode_tree->get_attribute ($conditional_block, "dynamic_evaluation"); + my $condition_string = $bnode_tree->get_attribute ($conditional_block, "condition"); + TBB::LogManager::write("debug", "In a conditional block: $condition_string"); + if ($condition_string || $dynamic_evaluation) + { + my $condition; + if ($condition_string && !$dynamic_evaluation) + { + $condition = TBB::Resource::Condition->new('ANONYMOUS', $condition_string, $resource_manager); + } + elsif ($dynamic_evaluation && !$condition_string) + { + my $dynamic_variable = $bnode_tree->get_attribute ($conditional_block, "dynamic_variable"); + my $value = $bnode_tree->get_attribute ($conditional_block, "value"); + my $dynamic_value = $self->{bnode}->get_dynamic_variable($dynamic_variable); + $condition = TBB::Resource::Condition->new( + 'ANONYMOUS', + "'$dynamic_value' eq '$value'", + $resource_manager + ); + } + else + { + TBB::Crash::crash23 "Conditional block can only take either condition or dynamic_evaluation, not both!"; + } + my $condition_result = $condition->evaluate( $user_data_obj, $id_user, $this_instance, %opts ); + + TBB::LogManager::write("debug", "Condition ($condition_string) result is: $condition_result"); + + if ($condition_result) { + my $condition_parent = $bnode_tree->get_parent_node($conditional_block); + + ##?REFACTOR: change gq to node + my $gqs = $bnode_tree->get_nodes_by_xpath($conditional_block, + "child::node()"); + foreach my $gq (@$gqs) + { + $bnode_tree->insert_before($condition_parent, + $gq, $conditional_block); + } + } + + $bnode_tree->remove_node( $conditional_block ); + } + $looptime = TBB::LogManager::log_elapsed_time($looptime, "ecb: looping..."); + } + $ecbtime = TBB::LogManager::log_elapsed_time($ecbtime, "ecb: loop complete"); + + return 1; +} + +=item __populate_summary_fields (%opts) + + Function to do the proper pre-population for the summary pages. + $user_data_obj is a TBB::UserData object, + $resource_manager is a TBB::BenefitDelivery::ResourceManager object, + "form_id" and "id_user" can be passed to this funciton through %opts + +=cut +sub __populate_summary_fields +{ + my $self = shift; + my %opts = @_; + + ##?REFACTOR: Use Method::named_argument here. + my $user_data = $opts{user_data} || TBB::Crash::crash24 "Need a UserData.\n"; + my $resource_manager = named_argument( 'resource_manager', 'TBB::BenefitDelivery::ResourceManager', \%opts, required => 1 ); + my $this_id_user = named_argument( 'this_id_user', 'SCALAR', \%opts, required => 1 ); + my $form_id = named_argument( 'form_id', 'SCALAR', \%opts, required => 1 ); + + # Get a list of summary nodes on this page. + my $bnode_tree = $self->{bnode}->get_xml_resource(); + my $summary_nodes = $bnode_tree->get_nodes_by_xpath($bnode_tree->get_root(), + "descendant::summary"); + + # If there are no summary nodes on this page, do nothing. + return if ((scalar @$summary_nodes) == 0); + + ##?REFACTOR: Is this necessary? Couldn't we just move the thing that does this. It can go away + # earlier in popluate_bnode() and not worry about it here? + # Set form_id for the bnode so the micro fields can get it. + $self->get_bnode()->add_attribute_by_xpath("/bnode", form_id =>$form_id); + + ##?REFACTOR: gq_question_default_order should be a package global: $GQ_QUESTION_DEFAULT_ORDER. + # Check the default order for ordering questions (this is by user or by question, and right + # now I think it's set to by question. + my $default_order = $self->{gq_question_default_order} || TBB::Crash::crash25 "No default order set!\n"; + + # Process each summary node on the page. + foreach my $summary_node (@$summary_nodes) + { + ##?REFACTOR: Write better comments to explain what the heck this does. + # Set this page's id_user as an attr of the summary page, in case it needs to + # use it to look up the id_user to set for the addeditdelete_id. In practice, + # only one BNode so far uses this - BN6087, the summary page for the HH mod. + $bnode_tree->add_attributes($summary_node, {'this_id_user' => $this_id_user}); + + # obtain the attributes for this summary node to decide the different behaviors + my $instanceset_id = $bnode_tree->get_attribute($summary_node, "instanceset"); + my $order_by = $bnode_tree->get_attribute($summary_node, "order_by"); + + # If the summary field doesn't have an order_by attribute, just give it the + # default order_by value. + unless ($order_by) + { + $order_by = $default_order; + $bnode_tree->add_attributes($summary_node, {'order_by' => $order_by}); + } + + # If the BNode is an instance BNode then set this_instance as the context_instance + # for 'add' and 'edit' fields. This is explicitly for the case where we are showing + # eg. a summary page of "all box-12s on this (this_instance) W-2". Now if users + # want to 'add' box-12s they are adding them to *this* W-2, so when they click + # 'add' we will set the W-2's instance as context_instance so they can set it + # as a hidden field on the box-12 page at which they arrive. Whew. + ##?REFACTOR: Should change to is_instance() + if ( $self->get_bnode()->is_ordinal() ) + { + TBB::LogManager::write( + 'debug', + "WARNING: this_instance not found for '" + . $self->get_bnode()->get_id() + . "' but we need to set it as a context_instance for 'add' fields." + ) unless ($opts{this_instance}); + $bnode_tree->add_attributes($summary_node, {'context_instance' => $opts{this_instance}}); + } + + # Now find @id_users -- the complete list of users to be considered for this summary page. + my @id_users; + ##?REVIEW: Why is $fs_id defined outside the if block? + my $fs_id; + if ($bnode_tree->has_attribute($summary_node, "filterset")) + { + $fs_id = $bnode_tree->get_attribute($summary_node, "filterset"); + @id_users = $user_data->retrieve_set_members( + base => $fs_id, + this_id_user => $opts{this_id_user}, + this_instance => $opts{this_instance}, + ##?REFACTOR: we need to pass the context_* vars + ); + TBB::LogManager::write( + 'debug', + "$fs_id returned members: [" . join(",", @id_users) . "]" + ); + } + else + { + ##?REVIEW: Just use $this_id_user, which we already have. + @id_users = ( $opts{'this_id_user'} ); + } + + # Opts hash for adding blocks to the summary field. + ##?TODO: Pass the context_instance, context_id_user vars here. + # Figure it out so it makes sense. + my %add_ud_opts = ( + 'order_by' => $order_by, + 'instanceset' => $instanceset_id, + 'this_instance' => $opts{this_instance} + ); + TBB::LogManager::write( + 'debug', + "SF: populating field for '" + . ($fs_id || "") + . "', order_by='" + . ($order_by || "") + . "Found users: " + . join(", ", @id_users) + . "." + ); + + # Get the content node + my $content_nodes = $bnode_tree->get_nodes_by_xpath($summary_node, 'descendant::content'); + + ##?BEGIN HERE: + + ##?TODO: Change this method to use absolute xpaths from summary node to labels, rather than + # descendant. + + if ($order_by eq 'user') + { + #create the corresponding user_data node + # Process the label node for the question context + my $user_label_txtopts = $bnode_tree->get_single_node_by_xpath( + $summary_node, + 'descendant::label[@context="user"]', + ok_to_return_null => 1 + ); + + foreach my $id_user (@id_users) + { + $self->__add_summary_userdata_node( + this_id_user => $id_user, + user_data => $user_data, + member_label => $user_label_txtopts, + content_nodes => $content_nodes, + summary_node => $summary_node, + %add_ud_opts + ); + } + } + elsif ($order_by eq 'question') + { + # Create the corresponding user_data node + # Process the label node for the question context + my $question_label_txtopts = $bnode_tree->get_single_node_by_xpath( + $summary_node, + 'descendant::label[@context="question"]', + ok_to_return_null => 1 + ); + $self->__process_text_options($bnode_tree, $question_label_txtopts, $user_data, $id_users[0], $opts{this_instance}, context => $order_by); + + # Get the 'member' label from this summary node's labelset. + my $member_label_txtopts = $bnode_tree->get_single_node_by_xpath( + $summary_node, + 'descendant::label[@context="question_member"]', + ok_to_return_null => 1 + ); + + # Process the label for this summary node + foreach my $id_user (@id_users) + { + $self->__add_summary_userdata_node( + this_id_user => $id_user, + user_data => $user_data, + member_label => $member_label_txtopts, + content_nodes => $content_nodes, + summary_node => $summary_node, + %add_ud_opts + ); + } + } + else + { + TBB::Crash::crash26 "$order_by is not a valid way to order questions!\n"; + } + } +} + +=item __add_summary_userdata_node( %opts ) + + Private function called in the __populate_summary_fields to create the userdata nodes + for the summary fields. + +=cut +sub __add_summary_userdata_node +{ + my $self = shift; + my %opts = @_; + + # Dereference %opts. + my $id_user = named_argument( 'this_id_user', 'SCALAR', \%opts, 'required' => 1 ); + ##?TODO: Replace this with named_argument call once UserData::New goes away. + my $user_data = $opts{user_data}; + unless (((ref $user_data) eq 'TBB::UserData') || ((ref $user_data) eq 'TBB::UserData::New')) + { + TBB::Crash::crash27 "Need a TBB::UserData or TBB::UserData::New object, not '" . (ref $user_data) . "\n"; + } + my $label_textopts = named_argument( 'member_label', 'XML::LibXML::Element', \%opts ); + my $content_nodes = named_argument( 'content_nodes', 'XML::LibXML::NodeList', \%opts, 'required' => 1 ); + my $parent_node = named_argument( 'summary_node', 'XML::LibXML::Element', \%opts, 'required' => 1 ); + my $this_instance = named_argument( 'this_instance', 'SCALAR', \%opts ); + my $context_id_user = named_argument( 'context_id_user', 'SCALAR', \%opts ); + my $context_instance = named_argument( 'context_instance', 'SCALAR', \%opts ); + my $instanceset_id = named_argument( 'instanceset', 'SCALAR', \%opts ); + my $order_by = named_argument( 'order_by', 'SCALAR', \%opts ); + + my $bnode_tree = $self->get_bnode()->get_xml_resource(); + + # Create the userdata node with id_user as attribute + my $userdata_node = $bnode_tree->create_node("userdata"); + $bnode_tree->add_attributes($userdata_node, { 'id_user' => $id_user } ); + + # Process the textopts for this user's question_member label. + if (defined $label_textopts) + { + my $local_label = $bnode_tree->clone_node($label_textopts, 1); + $self->__process_text_options($bnode_tree, $local_label, $user_data, $id_user, $this_instance, context => $order_by); + $bnode_tree->append_child($userdata_node, $local_label); + } + + # Process the content node + if (defined $content_nodes) + { + # If it's ordinal, we need to wrap the content nodes for each ordinal into the + # ordinal node...and append ordinal node to userdata node + if ($instanceset_id) + { + # Obtain all the ordinals for a specific id_user and udid + # Obtain the udid + my @instance_list = $user_data->retrieve_set_members( + base => $instanceset_id, + this_id_user => $id_user, + this_instance => $this_instance, + context_id_user => $context_id_user, + context_instance => $context_instance, + ); + TBB::LogManager::write('debug', "SF: Using '$instanceset_id' found ordinals: " . join(", ", @instance_list) . " with this_id_user=$id_user AND this_instance=$this_instance AND context_id_user=$context_id_user AND context_instance=$context_instance"); + + # Now iterate through the ordinals. + foreach my $id_instance (@instance_list) + { + my $instance_node = $bnode_tree->create_node("ordinal"); + $bnode_tree->add_attributes($instance_node, { id_ordinal => $id_instance } ); + foreach my $content_node (@$content_nodes) + { + my $local_content = $bnode_tree->clone_node($content_node, 1); + $self->__process_content_node( + $bnode_tree, $local_content, $user_data, $id_user, $id_instance, $order_by + ); + $bnode_tree->append_child($instance_node, $local_content); + } + + $bnode_tree->append_child($userdata_node, $instance_node); + } + } + else + { + foreach my $content_node (@$content_nodes) + { + my $local_content = $bnode_tree->clone_node($content_node, 1); + $self->__process_content_node($bnode_tree, $local_content, $user_data, $id_user, undef, $order_by); + $bnode_tree->append_child($userdata_node, $local_content); + } + } + } + + # append the node to mq node + $bnode_tree->append_child($parent_node, $userdata_node); +} + +##?REFACTOR: Shall we use a different subroutine to process this variable substitution? +##?REFACTOR: Cannot reuse __process_text_options because it's xml structure specific +##?REFACTOR: Probably *all* the variable substitution in this module could be outsourced +# to a single private method. But that might have to be part of a well +# conceived large-scale REFACTOR of this whole module.... +=item __process_content_node + + This function is to do the variable substitution for the content node of the summary + page. Called by function __add_summary_userdata_node + +=cut +sub __process_content_node +{ + my $self = shift; + ##?REFACTOR: This method should take in %opts and pass them on to the calls it initiates. + my ($bnode_tree, $node_to_process, $user_data_obj, $id_user, $this_instance, $order_by) = @_; + + # Needed for language normalization. + my $normalizer = $self->{resource_manager}->get_normalizer(); + my $current_language = $user_data_obj->get_current_language() || 'en-US'; + + TBB::LogManager::write('debug', "SF: Processing content node with args: " . join(", ", @_) . "."); + + # If there isn't a node to process then we're all done. + unless ($node_to_process) { + return; + } + + $id_user ||= $user_data_obj->current_client_id(); + + my $value_nodes = $bnode_tree->get_nodes_by_xpath( + $node_to_process, + "descendant::summary_field_value" + ); + + TBB::LogManager::write("debug", "SF: before processing content nodes"); + + if (scalar @$value_nodes == 0) { + return; + } + + foreach my $value_node (@$value_nodes) + { + my $node_value = $bnode_tree->get_node_value($value_node); + + TBB::LogManager::write("debug", "SF: in process content node: $node_value, id_user: $id_user"); + + my $no_instance = $bnode_tree->get_attribute($value_node, "no_ordinal"); + + if ($bnode_tree->get_attribute($value_node, 'dynamic')) { + + my $value_expression = TBB::Expression->new( + $node_value, + $TBB::BenefitDelivery::ResourceManager + ); + + my $actual_value = $value_expression->evaluate( + $user_data_obj, + $id_user, + $this_instance + ); + + $node_value = $actual_value || ''; + + } else { + + ##?REFACTOR: outsource ALL cases of replacing IDs in strings + # to one internally consistent method, The + # following will break in lots of cases, including + # aliases, INSTANCE()s etc. + my @userdata_components_in_node = TBB::ID::find_ids_in_string( + $node_value, + ['formula', 'question_with_mandatory_mq'] + ); + + foreach my $udid (@userdata_components_in_node) + { + my $instance_to_pass = $this_instance unless($no_instance); + #TBB::LogManager::write( + # 'crit', + # "CALLING UD->retrieve_value with base='$udid' this_id_user='$id_user' this_instance='$instance_to_pass'" + #); + my $value = $user_data_obj->retrieve_value( + 'base' => $udid, + 'this_id_user' => $id_user, + 'this_instance' => $instance_to_pass + ); + #TBB::LogManager::write( + # 'crit', + # "PRE NORM: $value" + #); + $value = $normalizer->normalize_for_presentation( + $value, + $udid, + $current_language, + context => $order_by + ); + #TBB::LogManager::write( + # 'crit', + # "POST NORM: $value" + #); + $node_value =~ s/$udid/$value/g; + } + } + $bnode_tree->set_node_value($value_node, $node_value); + } +} + +=item __populate_summary_tables() + + A brand spankin' new method to handle the new summary_table to end all + summaries. + + ##?REFACTOR: POD. More specific comment...also, what's the interface for the xml + +=cut +sub __populate_summary_tables +{ + my $self = shift; + my %opts = @_; + + # Get args from opts. + my $page_id_user = named_argument( 'this_id_user', 'SCALAR', \%opts, 'required' => 1 ); + my $page_instance = named_argument( 'this_instance', 'SCALAR', \%opts ); + my $context_id_user = named_argument( 'context_id_user', 'SCALAR', \%opts ); + my $context_instance = named_argument( 'context_instance', 'SCALAR', \%opts ); + + ##?TODO: Migrate EVERYONE to new UserData so we can get out of this shit. + ##?REFACTOR: please use named_argument to check the argument type + my $user_data = $opts{user_data}; + TBB::Crash::crash28 "Bah! Need a UserData or UserData::New, not $user_data.\n" + unless (((ref $user_data) eq 'TBB::UserData') || ((ref $user_data) eq 'TBB::UserData::New')); + + # Get the XML document for the + my $page_xml = $self->get_bnode()->get_xml_resource(); + + my $list_of_summary_tables = $page_xml->get_nodes_by_xpath( + $page_xml->get_root(), + 'descendant::summary_table' + ); + + # Iterate over the summary_table nodes in this BNode. + foreach my $summary_table ( @$list_of_summary_tables ) + { + my $populated_table_node = $page_xml->create_node( 'populated_table' ); + # Set the page-wide id_user and instance values as attributes of the populated_table + # node. These are there for the purposes of the XSLT when it needs to build + # navigation buttons. + $page_xml->add_attributes($populated_table_node, {this_id_user => $page_id_user}); + $page_xml->add_attributes($populated_table_node, {this_instance => $page_instance}) + if $page_instance; + + # Get all child nodes. + my $child_nodes_of_summary_table = $page_xml->get_non_empty_children( $summary_table ); + + foreach my $this_group_node (@$child_nodes_of_summary_table) + { + my $this_node_name = $page_xml->get_node_name( $this_group_node ); + ##?REFACTOR: Comments. what's label_group, what's record_group + if ($this_node_name eq 'label_group') + { + # Make a deep copy of the label_group. + my $label_node_to_populate = $page_xml->clone_node( $this_group_node, 1 ); + + # Attach it to the populated table + $page_xml->append_child( $populated_table_node, $label_node_to_populate ); + + # Process its contents. + ##?NOTE: Due to the way it's written, __process_text_options will + # process ALL text descendants of its 2nd argument. + $self->__process_text_options( + $page_xml, + $label_node_to_populate, + $user_data, + $page_id_user, + $page_instance, + context_id_user => $context_id_user, + context_instance => $context_instance + ); + } + elsif ($this_node_name eq 'record_group') + { + my $rg_filterset = $page_xml->get_attribute( $this_group_node, 'filterset' ); + my $rg_instanceset = $page_xml->get_attribute( $this_group_node, 'instanceset' ); + my $record_element_list = $page_xml->get_nodes_by_xpath( $this_group_node, 'record_element' ); + + if ($rg_filterset && $rg_instanceset) + { + TBB::Crash::crash29 "Don't support BOTH instanceset AND filterset attributes for record_groups."; + } + elsif ($rg_instanceset) + { + my @instances_in_rg = $user_data->retrieve_set_members( + base => $rg_instanceset, + this_id_user => $page_id_user, + this_instance => $page_instance, + context_id_user => $context_id_user, + context_instance => $context_instance + ); + + foreach my $this_instance (@instances_in_rg) + { + # Ask UserData which user owns $this_instance. + my $this_id_user = $user_data->get_id_user_for_instance( $this_instance ); + + $self->__process_record_element( + record_element_node_list => $record_element_list, + user_data => $user_data, + this_id_user => $this_id_user, + this_instance => $this_instance, + context_id_user => $context_id_user, + context_instance => $context_instance, + summary_table_node => $populated_table_node, + page_xml => $page_xml, + ); + } + + } + elsif ($rg_filterset) + { + my @users_in_rg = $user_data->retrieve_set_members( + base => $rg_filterset, + this_id_user => $page_id_user, + this_instance => $page_instance, + context_id_user => $context_id_user, + context_instance => $context_instance + ); + + foreach my $this_id_user (@users_in_rg) + { + $self->__add_record_to_summary_table( + record_element_node_list => $record_element_list, + user_data => $user_data, + this_id_user => $this_id_user, + this_instance => $page_instance, + context_id_user => $context_id_user, + context_instance => $context_instance, + summary_table_node => $populated_table_node, + page_xml => $page_xml, + ); + } + + } + else + { + TBB::Crash::crash30 "Need to have EITHER an instanceset OR a filterset to work with!"; + } + + } + elsif( ($this_node_name eq 'text') and ($page_xml->get_node_value( $this_group_node ) eq '')) + { + ##?TODO: Please make this go away. + TBB::LogManager::write( 'debug', "Skipping stupid empty text node." ); + } + else + { + TBB::LogManager::write( + 'debug', + "Found '$this_node_name' with value '" + . $page_xml->get_node_value( $this_group_node ) + . "'" + ); + TBB::Crash::crash31 "$this_node_name is not a legal child of a summary_table node!"; + } + } + + # If we have been told to "diagonally flip" the table, ie. to make + # records be columns, not rows, then regenerate the $populated_table_node + # in this wise. + if ($page_xml->get_attribute( $summary_table, 'diagonal_flip' )) + { + $populated_table_node = $self->__diagonally_flip_populated_table( + page_xml => $page_xml, + populated_table_node => $populated_table_node, + ); + } + + # Append the populated_table node to the parent summary_table. + $page_xml->append_child( $summary_table, $populated_table_node ); + } + + return 1; +} + +=item __add_record_to_summary_table( %opts ) + + This method adds a node to the node of + a summary_table. It does this by processing the data for + +=cut +sub __add_record_to_summary_table +{ + my $self = shift; + my %opts = @_; + + # Parse %opts. + my $record_element_node_list = named_argument( 'record_element_node_list', 'XML::LibXML::NodeList', \%opts, required => 1 ); + ##?TODO: Use named_argument as soon as its type changes OR we get rid of + # UserData::New. + my $user_data = $opts{'user_data'} || TBB::Crash::crash32 "D'oh! Need UserData."; + my $this_id_user = named_argument( 'this_id_user', 'SCALAR', \%opts, required => 1 ); + my $this_instance = named_argument( 'page_instance', 'SCALAR', \%opts ); + my $context_id_user = named_argument( 'context_id_user', 'SCALAR', \%opts ); + my $context_instance = named_argument( 'context_instance', 'SCALAR', \%opts ); + my $summary_table_node = named_argument( 'summary_table_node', 'XML::LibXML::Element', \%opts, required => 1 ); + my $page_xml = named_argument( 'page_xml', 'TBB::XML', \%opts, required => 1 ); + + # Create the new record_group node. Give it attributes describing the + # id_user and (if appropriate) instance to which it belongs. + my $record_group_node = $page_xml->create_node( 'record_group' ); + $page_xml->add_attributes( $record_group_node, { this_id_user => $this_id_user } ); + $page_xml->add_attributes( $record_group_node, { this_instance => $this_id_user } ) + if ($this_instance); + + # Now process each record_element within the group and attach the + # record_data nodes to the record_group. + foreach my $record_element_node (@$record_element_node_list) + { + $self->__process_record_element( + record_element_node => $record_element_node, + user_data => $user_data, + this_id_user => $this_id_user, + this_instance => $this_instance, + context_id_user => $context_id_user, + context_instance => $context_instance, + record_node => $record_group_node, + page_xml => $page_xml, + ); + } + + # Attach the whole record_group node to the populated_table parent node. + return $page_xml->append_child( $summary_table_node, $record_group_node ); +} + +=item __process_record_element( %opts ) + + This subroutine processes a single tag within a summary_table. + The contents of a record_element should be processed as a TBB::Expression. + This means that we should correctly handle GQ:MQs, quoted strings, FMs + and INSTANCE(GQ:MQ). + + When the record_element is processed correctly we attach a record_element_data + node containing the result to the parent record_node. + + Required %opts for this method: + + user_data : A TBB::UserData (or UserData::New) object. + this_id_user : The id_user for whom to process this record_element. + + + Optional %opts for this method: + + this_instance : The instance for which to process this record_element. + context_id_user : The context_id_user " " " " " + context_instance : The context_instance " " " " " + +=cut +sub __process_record_element +{ + my $self = shift; + my %opts = @_; + + # Parse opts. + my $record_element_node = named_argument( 'record_element_node', 'XML::LibXML::Element', \%opts, required => 1 ); + my $user_data = $opts{'user_data'} || TBB::Crash::crash33 "D'oh! Need UserData."; + my $this_id_user = named_argument( 'this_id_user', 'SCALAR', \%opts, required => 1 ); + my $this_instance = named_argument( 'page_instance', 'SCALAR', \%opts ); + my $context_id_user = named_argument( 'context_id_user', 'SCALAR', \%opts ); + my $context_instance = named_argument( 'context_instance', 'SCALAR', \%opts ); + my $record_node = named_argument( 'record_node', 'XML::LibXML::Element', \%opts, required => 1 ); + my $page_xml = named_argument( 'page_xml', 'TBB::XML', \%opts, required => 1 ); + + # Get the value of the $record_element_node. + my $rf_node_value = $page_xml->get_node_value( $record_element_node ); + + my $tbb_expression = TBB::Expression->new( $rf_node_value ); + my $specific_value = $tbb_expression->evaluate( + $user_data, + $this_id_user, + $this_instance, + context_id_user => $context_id_user, + context_instance => $context_instance + ); + + TBB::LogManager::write( + 'debug', + "Setting value " + . TBB::ID::implode( id_user => $this_id_user, base => $rf_node_value, instance => $this_instance ) + . " to " + . $specific_value + ); + + my $rg_element_node = $page_xml->create_node( 'record_data', $specific_value ); + + return $page_xml->append_child( $record_node, $rg_element_node ); +} + +=item __diagonally_flip_populated_table( %opts ) + +##?REFACTOR: POD +=cut +sub __diagonally_flip_populated_table +{ + my $self = shift; + my %opts = @_; + + # Parse %opts. + my $populated_table_node = named_argument( 'populated_table_node', 'XML::LibXML::Element', \%opts, required => 1 ); + my $page_xml = named_argument( 'page_xml', 'TBB::XML', \%opts, required => 1 ); + + # Make a "flipped" node. + # We do so by "cloning" ie. copying the $populated_table_node. Note that for once + # we DON'T want a "deep" copy (ie. all children) since we just want to replicate + # the attributes of the original, not its structure. + my $flipped_pt_node = $page_xml->clone_node( $populated_table_node, 0 ); + + # Work out the number of cross-section nodes to make. + my $first_child = $page_xml->get_first_child( $populated_table_node ); + my $children_of_first_child_node = $page_xml->get_non_empty_children( $first_child ); + my $number_of_cross_sections = scalar @$children_of_first_child_node; + + for (my $cross_section = 1; $cross_section <= $number_of_cross_sections; $cross_section ++) + { + my $cross_section_node = $page_xml->create_node( 'record_cross_section' ); + + # Get the Nth child (matching the cross section) of all children of the pop tab. + my $list_of_cross_section_elements = $page_xml->get_nodes_by_xpath( + $populated_table_node, + "child::*/child::*[$cross_section]" + ); + + TBB::LogManager::write( + 'debug', + "In CROSS SECTION $cross_section, found " . (scalar @$list_of_cross_section_elements) + . " nodes to migrate." + ); + + # Attach our list of children to the record_cross_section element. + foreach my $this_child (@$list_of_cross_section_elements) + { + # Take a copy of the child and append it to the record cross section. + my $copy_of_this_child = $page_xml->clone_node( $this_child, 1 ); + $page_xml->append_child( $cross_section_node, $copy_of_this_child ); + } + + $page_xml->append_child( $flipped_pt_node, $cross_section_node ); + } + + # Return the "flipped" pop tab. + return $flipped_pt_node; +} + +=item __set_udid_hidden_fields() + + This finds every hidden_field node in $bnode_tree. For each hidden_field, + it adds a hidden="1" attribute to the child mq, sets the default value for + the child mq, and removes the hidden_field tag. + + We now also find mqs which have the child + and process them similarly. + +=cut +sub __set_udid_hidden_fields +{ + my $self = shift; + + my $bnode_tree = $self->{bnode}->get_xml_resource(); + + my $hidden_field_nodes = + $bnode_tree->get_nodes_by_xpath($bnode_tree->get_root(), + "descendant::hidden_field"); + + foreach my $hidden_field (@$hidden_field_nodes) + { + #there is exactly one mq per hidden_field node + my $mq_node = $bnode_tree->get_single_node_by_xpath($hidden_field, "descendant::mq"); + + my %attributes = ("hidden" => "1"); + #$attributes{dynamic} = 1 if ($bnode_tree->get_attribute($hidden_field, "dynamic")); + $bnode_tree->add_attributes($mq_node, \%attributes); + + my $is_dynamic = $bnode_tree->get_attribute( $hidden_field, "dynamic" ); + my $value = $bnode_tree->get_attribute($hidden_field, "value"); + $self->__set_default_value($bnode_tree, $mq_node, $value, dynamic => $is_dynamic); + + # We now remove the hidden field tag from the tree, attaching whatever its children are to its parents. + my $hf_children = $bnode_tree->get_children($hidden_field); + my $parent_node = $bnode_tree->get_parent_node($hidden_field); + ##?REFACTOR: Moan (ie. TBB::Crash::crash34()) if there is not one and only one child of + ##?REFACTOR: this hidden_field. + foreach my $child_node (@$hf_children) + { + # Clone the child node. The '1' means a deep copy, ie. the whole structure + # beneath the child node is copied. + my $child_clone = $bnode_tree->clone_node($child_node, 1); + + # Attach the child node to the hidden_field's parent node. + $bnode_tree->append_child($parent_node, $child_node); + } + + # Now remove the hidden_field node and all of its original descendants (who, by now, have + # been safely copied to the same position the hidden_field was originally in. + $bnode_tree->remove_node($hidden_field); + } +} + +=item __set_default_value($bnode_tree, $mq, $default_value, %opts) + + Sets the default value of the MQ node to $default_value + + $bnode_tree is the TBB::XML object representing the BNode + + $mq is a XML::LibXML::Element object representing an MQ node + + $default_value is the value that we want to set for $mq + + Valid opts include: + + dynamic => Mark the hidden field with the "dynamic" attribute, + indicating that it should be evaluated as an expression + for each user when adding userdata nodes. + + ##?REFACTOR: We don't call this anywhere, and I think we're right not to; it's + ##?REFACTOR: out-of-paradigm in that this doesn't get called globally. Probably + ##?REFACTOR: this opt and its functionality should just go away. + ##?REFACTOR: Check this is true then remove this. + ignore_hidden_fields => If true, this function will not set the default + value for hidden fields. This should be set to 1 + when being called by __populate_group_questions and + __populate_non_group_questions. + +=cut +sub __set_default_value +{ + my $self = shift; + my ($bnode_tree, $mq, $default_value, %opts) = @_; + my $dynamic = $opts{dynamic}; + my $ignore_hidden_fields = $opts{ignore_hidden_fields}; + + ##?REFACTOR: Should the following two lines should go away? + ##?REFACTOR: Should this whole method go away? + my $is_hidden_field = $bnode_tree->get_attribute($mq, "hidden"); + return if ($ignore_hidden_fields && $is_hidden_field); + + if ($default_value) + { + my $default_value_node = $bnode_tree->get_single_node_by_xpath($mq, 'defaultvalue', ok_to_return_null => 1); + unless ( $default_value_node ) + { + $default_value_node = $bnode_tree->create_node("defaultvalue"); + $bnode_tree->append_child($mq, $default_value_node); + } + $bnode_tree->set_node_value($default_value_node, $default_value); + if ( $dynamic ) + { + $bnode_tree->add_attributes( $default_value_node, {dynamic => 1} ); + } + } +} + +=item __populate_drop_filtersets( $user_data, $resource_manager, %opts ) --> 1 + + Populate the dynamic drop-down box with the household member based on the + specified filterset. Pass in the TBB::UserData object and + TBB::BenefitDelivery::ResourceManager object + +=cut +sub __populate_drop_filtersets +{ + my $self = shift; + my ($user_data_obj, $resource_manager, %opts) = @_; + + $resource_manager = $TBB::BenefitDelivery::ResourceManager + if (not defined $resource_manager); + + my $bnode_tree = $self->{bnode}->get_xml_resource(); + my $root_node = $bnode_tree->get_root(); + my $drop_filtersets = $bnode_tree->get_nodes_by_xpath($bnode_tree->get_root(), + "descendant::drop_fs"); + + # If there is no filterset based dynamic drop-down, no need to preceed, return + return if (scalar @$drop_filtersets == 0); + #TBB::LogManager::write('debug', 'found drop_filterset'); + foreach my $drop_filterset (@$drop_filtersets) + { + my ( $udid_label_nodes, $udid_label_node, $udid_label_string, @label_udids); + my $filtered_data = {}; + my $fs_id = $bnode_tree->get_attribute($drop_filterset, "filterset"); + ### check to see if the trigger_user attribute is set + # A trigger_user drop filter sets the "id_user" form variable + # to a particular id_user value to indicate to the following page + # that all questions on that page are being asked about a particular id_user + # (the id_user will be prepended to all partial UDIDs) + my $is_trigger_user_dropfs = + $bnode_tree->get_attribute($drop_filterset, "trigger_user"); + my $is_set_this_page_id_user = + $bnode_tree->get_attribute($drop_filterset, "this_page_id_user"); + #TBB::LogManager::write('debug', "value of is_trigger_user_dropfs: $is_trigger_user_dropfs"); + my $id_users; + + ### if this is a this_page_id_user and an id user has been set, + ### we assume that we are coming off of an edit transition from a summary page, + ### and because changing the "this_page_id_user" will not delete the old data, + ### we do not allow the this_page_id_user to be changed. + if ( $is_set_this_page_id_user and $self->{navigator}->get_this_id_user() ) + { + ##?TODO: In this case, we should pre-select the only user in the select box. + $id_users = [ $self->{navigator}->get_this_id_user() ]; + } + else + { + my $filterset = $resource_manager->get_filterset($fs_id); + $id_users = $filterset->filter_data($user_data_obj, resource_manager => $resource_manager, this_id_user => $opts{this_id_user}); + } + + #TBB::LogManager::write('debug', "**NOW: id_user is: ".Dumper($id_users)) if TBB::LogManager::writes_at('debug'); + $udid_label_nodes = $bnode_tree->get_nodes_by_xpath($drop_filterset, + "descendant::text"); + ### we are not checking for language for now (the label is probably ##fullname##) + $udid_label_node = shift ( @{ $udid_label_nodes } ); + $udid_label_string = + &TBB::Utils::trim_whitespace($bnode_tree->get_node_value($udid_label_node)); + +##?REFACTOR: come up with a better way to assemble display_text + + # obtain the name for each of the related UDID + @label_udids = split (/ /, $udid_label_string); + + # build option display text (e.g. full name) + foreach my $id_user (@$id_users) + { + my ($display_text); + foreach my $udid (@label_udids) + { + ##?REFACTOR: Do we need to search label udids for aliases and pass them + # in to the retrieve call? We don't need to yet, but one + # day we might. + ##?REFACTOR: normalization_for_presentation and also write the normalization method for this case + ##?REFACTOR: use join. + $display_text .= $user_data_obj->retrieve_value( + base => $udid, + this_id_user => $id_user + ) + . " "; + } + chop($display_text); + $filtered_data->{$id_user} = $display_text; + } + + ### Add a node with the drop_filterset data + # For the time being, we are directly building a select node + + my $select_node = $bnode_tree->create_node ("select"); + + if ($is_trigger_user_dropfs) + { + ##?TODO We should be setting "set_user_id" by + ##?TODO $TBB::config{form_paramaters}{set_id_user_id} + #TBB::LogManager::write('debug', "determined dropfs is a is_trigger_user_dropfs"); + $bnode_tree->add_attributes ( + $select_node, + { + name => "set_id_user" + } + ); + } + if ($is_set_this_page_id_user) + { + $bnode_tree->add_attributes ( + $select_node, + { + name => "this_page_id_user" + } + ) + } + + ## Note: We use $id_users as it's a pre-sorted list, rather than + ## just iterating over the keys of %$filtered_data. + foreach my $id_user ( @$id_users ) + { + my $option_node = $bnode_tree->create_node ( + "option", + $filtered_data->{$id_user} + ); + $bnode_tree->add_attributes ( $option_node, + { + value => $id_user + } + ); + if (scalar (@$id_users) == 1) + { + $bnode_tree->add_attributes ( $option_node, + { + selected => 1 + } + ); + + } + $bnode_tree->append_child ( $select_node, $option_node); + } + $bnode_tree->replace_child ( $root_node, $select_node, $drop_filterset ); + } + + return 1; +} + +=item __populate_display_instancesets($user_data_obj, $resource_manager, $id_user[, %opts]) + + Function to list the labels specified in the tag based on the instances + for the users specified in the filterset. + + $user_data_obj is TBB::UserData object + $resource_manager is TBB::BenefitDelivery::ResourceManager object + $id_user is the whatever the user id passed in, but it would be overritten if the filterset + is specified. + + Interface in the xml is: + + + Interface should go to BNode level and within a conditional block. + +=cut +##?REFACTOR: this needs SEVERE refactoring +sub __populate_display_instancesets +{ + my $self = shift; + my ($user_data_obj, $resource_manager, $id_user, %opts) = @_; + + $id_user ||= $user_data_obj->current_client_id(); + my $this_instance = $opts{this_instance}; + + my $bnode_tree = $self->{bnode}->get_xml_resource(); + my $root_node = $bnode_tree->get_root(); + + my $conditional_blocks = $bnode_tree->get_nodes_by_xpath($bnode_tree->get_root(), + "descendant::conditional_block"); + foreach my $conditional_block (@$conditional_blocks) + { + my $display_instancesets = $bnode_tree->get_nodes_by_xpath($conditional_block, + "display_is"); + + next unless (scalar @$display_instancesets); + + my $paragraph_text_nodes = $bnode_tree->get_nodes_by_xpath($conditional_block, + "descendant::text"); + foreach my $text_node (@$paragraph_text_nodes) + { + my $text = $bnode_tree->get_node_value($text_node); + my $label_hash; + foreach my $display_is (@$display_instancesets) + { + my $is_id = $bnode_tree->get_attribute($display_is, 'instanceset'); + my $fs_id = $bnode_tree->get_attribute($display_is, 'filterset'); + my $label_expression_string = $bnode_tree->get_attribute($display_is, 'label'); + my $instanceset = $resource_manager->get_instanceset( $is_id ); + my $filterset = $resource_manager->get_filterset( $fs_id ); + my $label_expression = TBB::Expression->new( + $label_expression_string, + $resource_manager + ); + ##?TODO: Figure out how to set a CONTEXT_INSTANCE() for this is. + #Should it be based on $this_instance or $context_instance or either or neither? #OK. Passing the new %opts in should do it. + my $id_users = $filterset->filter_data( + $user_data_obj, + %opts, + ); + next unless (scalar @$id_users); + + foreach my $id_user (@$id_users) + { + my $instance_list = $instanceset->filter_data( + $user_data_obj, + $id_user, + %opts + ); + + next unless (scalar @$instance_list); + + foreach my $this_instance ( @$instance_list ) + { + my $this_label = $label_expression->evaluate( + $user_data_obj, + $id_user, + $this_instance + ); + $label_hash->{$id_user . "_" . $this_label} = $this_label; + } + } + $bnode_tree->remove_node($display_is); + + } + $text .= ": " . join(", ", (values %$label_hash)); + $bnode_tree->set_node_value($text_node, $text); + } + } + + return 1; +} + +##?TODO: Get rid of this when we refactor taxes. It is now subsumed by +##?TODO: instanceset gq_groups and should go away. Make sure it's not in +##?TODO: production BMods though before we do away with it. +=item __populate_check_instancesets($user_data_obj, $resource_manager, $id_user[, %opts]) + + Function to list the checkboxes for all the instances specified by instanceset in the + 's attribute for the users specified in the filterset. The gq with + has to be grouped gq with the filterset specified in the + + $user_data_obj is TBB::UserData object + $resource_manager is TBB::BenefitDelivery::ResourceManager object + $id_user is the whatever the user id passed in, but it would be overritten if the filterset + is specified. + + Interface in the xml is: + + + + +=cut +sub __populate_check_instancesets +{ + my $self = shift; + my ($user_data_obj, $resource_manager, $id_user, %opts) = @_; + + $id_user ||= $user_data_obj->current_client_id(); + my $this_instance = $opts{this_instance}; + + my $bnode_tree = $self->{bnode}->get_xml_resource(); + my $root_node = $bnode_tree->get_root(); + my $mqs = $bnode_tree->get_nodes_by_xpath($bnode_tree->get_root(), + "descendant::mq"); + + # Now go through the check_instanceset nodes and create fake mqs in their place. + ### Our strategy: + ### For each instance in the instance set (for which we want to make a checkbox) + ### we will make a fake MQ where the MQ id ends with an underscore and the + ### instance id. We'll set the label of that MQ to the calculated label from + ### instance data. And then we'll let the XSL present it as if it was any MQ. + foreach my $mq_ancestor (@$mqs) + { + my $check_instancesets = $bnode_tree->get_nodes_by_xpath($bnode_tree->get_root(), + "descendant::check_is"); + TBB::LogManager::write('debug', "CHECKIS: in populate_check_is"); + next unless (scalar @$check_instancesets); + + foreach my $check_is (@$check_instancesets) + { + TBB::LogManager::write('debug', "CHECK IS: in populate_check_is, found one"); + my $is_id = $bnode_tree->get_attribute($check_is, 'instanceset'); + my $gq_group = $bnode_tree->get_single_node_by_xpath($check_is, + "ancestor::gq_group"); + my $fs_id = $bnode_tree->get_attribute($gq_group, 'filterset'); + my $label_expression_string = $bnode_tree->get_attribute($check_is, 'label'); + my $instanceset = $resource_manager->get_instanceset( $is_id ); + my $filterset = $resource_manager->get_filterset( $fs_id ); + my $label_expression = TBB::Expression->new( + $label_expression_string, + $resource_manager + ); + my $gq_ancestor = $bnode_tree->get_single_node_by_xpath($check_is, 'ancestor::gq'); + my $mq_id = $bnode_tree->get_attribute( $mq_ancestor, 'id' ); + ##?TODO: Figure out how to set a CONTEXT_INSTANCE() for this is. + #Should it be based on $this_instance or $context_instance or either or neither? + + my $id_users = $filterset->filter_data($user_data_obj, %opts); + next unless (scalar @$id_users); + + foreach my $id_user (@$id_users) + { + my $instance_list = $instanceset->filter_data( + $user_data_obj, + $id_user, + %opts + ); + TBB::LogManager::write( + 'debug', + "CHECK IS: '$is_id' returned " + . (scalar @$instance_list) + . " members: " + . join(", ", @$instance_list) + ) if TBB::LogManager::writes_at('debug'); + next unless (scalar @$instance_list); + + foreach my $this_instance ( @$instance_list ) + { + TBB::LogManager::write('debug', "CHECK IS: generating mq for $this_instance"); + my $mq_clone = $bnode_tree->clone_node($mq_ancestor, 1); + my $this_label = $label_expression->evaluate( + $user_data_obj, + $id_user, + $this_instance, + %opts + ); + + ### Set labelset to individual instance label + # This is a little too aggressive -- too many value changes + my $text_nodes = $bnode_tree->get_nodes_by_xpath($mq_clone, 'descendant::label/textoptions/text'); + foreach my $text_node ( @$text_nodes ) + { + $bnode_tree->set_node_value( $text_node, $this_label ); + } + my $clarifying_question_nodes = $bnode_tree->get_nodes_by_xpath($mq_clone, 'descendant::clarifying_questions'); + foreach my $clarifying_question_node ( @$clarifying_question_nodes ) + { + $bnode_tree->remove_node($clarifying_question_node); + } + + ### update mq + $bnode_tree->add_attributes ( + $mq_clone, + { + id => $mq_id ."_$this_instance" + } + ); + + ### update interface + my $clone_check_is = $bnode_tree->get_nodes_by_xpath($mq_clone, 'descendant::check_is'); + my $interface_node = $bnode_tree->get_single_node_by_xpath($mq_clone, 'descendant::interface'); + foreach my $clone_single_check_is (@$clone_check_is) + { + $bnode_tree->remove_node($clone_single_check_is); + } + my $checkbox_node = $bnode_tree->create_node ("checkbox"); + $bnode_tree->append_child ( $interface_node, $checkbox_node ); + + ### append clone to gq + $bnode_tree->append_child( $gq_ancestor, $mq_clone ); + + ## modify the tag based on the id_user for this instance + my $userdata = $bnode_tree->get_nodes_by_xpath($mq_clone, "userdata"); + foreach my $userdata_node (@$userdata) + { + if ($bnode_tree->get_attribute($userdata_node, "id_user") ne $id_user) + { + $bnode_tree->remove_node($userdata_node); + } + else + { + $bnode_tree->remove_node($bnode_tree->get_single_node_by_xpath($userdata_node, "label")); + } + } + } + } + } + # remove the unnecessary tag from the mq_ancestor for the presentation purpose + my $userdata = $bnode_tree->get_nodes_by_xpath($mq_ancestor, "userdata"); + foreach my $userdata_node (@$userdata) + { + $bnode_tree->remove_node($userdata_node); + } + } + + return 1; +} + +##?REFACTOR: POD. +sub __populate_drop_instancesets +{ + my $self = shift; + my ($user_data, $resource_manager, $id_user, %opts) = @_; + + $id_user ||= $user_data->current_client_id(); + my $this_instance = $opts{this_instance}; + + my $bnode_tree = $self->{bnode}->get_xml_resource(); + my $root_node = $bnode_tree->get_root(); + my $drop_instancesets = $bnode_tree->get_nodes_by_xpath($bnode_tree->get_root(), + "descendant::drop_is"); + + # If there is no filterset based dynamic drop-down, no need to preceed, return + return unless (scalar @$drop_instancesets); + + # Now go through the drop_id nodes and give them a working over. + foreach my $drop_is (@$drop_instancesets) + { + my $is_id = $bnode_tree->get_attribute($drop_is, 'instanceset'); + my $label_expression_string = $bnode_tree->get_attribute($drop_is, 'label'); + my $trigger_instance = $bnode_tree->get_attribute($drop_is, 'trigger_instance'); + my $instanceset = $resource_manager->get_instanceset( $is_id ); + my $label_expression = TBB::Expression->new( + $label_expression_string, + $resource_manager + ); + ##?TODO: Figure out how to set a CONTEXT_INSTANCE() for this is. Should it be based on $this_instance or $context_instance or either or neither? + my $instance_list = $instanceset->filter_data( + $user_data, + $id_user, + context_instance => $this_instance + ); + TBB::LogManager::write( + 'debug', + "DROP IS: '$is_id' returned " + . (scalar @$instance_list) + . " members: " + . join(", ", @$instance_list) + ) if TBB::LogManager::writes_at('debug'); + + next unless (scalar @$instance_list); + + my $select_node = $bnode_tree->create_node ("select"); + + my $select_name; + if ($trigger_instance) + { + ##?TODO We should be setting "set_user_id" by + ##?TODO $TBB::config{form_paramaters}{set_id_user_id} + #TBB::LogManager::write('debug', "determined dropfs is a is_trigger_user_dropfs"); + $select_name = 'set_ordinal'; + } + else + { + my $gq_ancestor = $bnode_tree->get_single_node_by_xpath($drop_is, 'ancestor::gq'); + my $mq_ancestor = $bnode_tree->get_single_node_by_xpath($drop_is, 'ancestor::mq'); + $select_name = $id_user + . ":" + . $bnode_tree->get_attribute( $gq_ancestor, 'id' ) + . ":" + . $bnode_tree->get_attribute( $mq_ancestor, 'id' ); + $select_name .= "_$this_instance" if $this_instance; + + } + $bnode_tree->add_attributes ( + $select_node, + { name => $select_name } + ); + + + foreach my $this_instance ( @$instance_list ) + { + my $this_id_user = $user_data->get_id_user_for_instance( $this_instance ); + my $this_label = $label_expression->evaluate( + $user_data, + $this_id_user, + $this_instance + ); + + my $option_node = $bnode_tree->create_node ( + "option", + $this_label + ); + $bnode_tree->add_attributes ( + $option_node, + { value => $this_instance } + ); + $bnode_tree->append_child ( $select_node, $option_node); + } + my $drop_is_parent_mq = $bnode_tree->get_single_node_by_xpath( $drop_is, 'ancestor::interface' ); + $bnode_tree->append_child($drop_is_parent_mq, $select_node); + $bnode_tree->remove_node( $drop_is ); + } + +} + + +=item __add_dummy_ids($id_users, $dummy_id_count) + + Adds $dummy_id_count dummy IDs to $id_users. A dummy ID is a single letter, + so if $dummy_id_count is 3, for example, this function will push the letters + A, B and C to the end of the array referenced by $id_users + + $id_users is an array reference + + $dummy_id_count is an integer + +=cut +sub __add_dummy_ids +{ + my $self = shift; + my ($id_users, $dummy_id_count) = @_; + + for (my $i = 1; $i <= $dummy_id_count; $i++) + { + ## chr($i + 64) returns 'A' if $i == 1, 'B' if $i == 2, etc. + push @$id_users, chr($i + 64); + } +} + +=item __integrate_form_action($action) + + Populates the bnode with the action specified in $action. + +=cut +sub __integrate_form_action +{ + my $self = shift; + my ($action) = @_; + + my $bnode = $self->{bnode}; + + $bnode->add_attribute_by_xpath("/bnode", + 'action' => $action); +} + +=item __integrate_language_preference($primary_lang, $secondary_lang) + + Populates the bnode with the primary and secondary languages specified + in the parameters. + +=cut +sub __integrate_language_preference +{ + my $self = shift; + my ($primary_lang, $secondary_lang) = @_; + + my $bnode = $self->{bnode}; + + $bnode->add_attribute_by_xpath("/bnode", + 'lang_primary' => $primary_lang, + 'lang_secondary' => $secondary_lang + ); +} + +=item __set_system_hidden_field ($field_name, $value) + + System feilds are non-UDID fields containing pieces of (typically navigational) + data which are set in the BNode and set as hidden form fields + on the resultant page. They all live in the XML node + + ##TODO: Finish this POD. + +=cut +sub __set_system_hidden_field +{ + my $self = shift; + my ($field_name, $value) = @_; + + # Set some local variables for ease of reference. + my $bnode_tree = $self->get_bnode()->get_xml_resource(); + my $shf_xpath = $self->{system_hidden_field_xpath}; + + # Get the node to append hidden fields to. + my $shf_parent_nodelist = $bnode_tree->get_nodes_by_xpath($bnode_tree->get_root(), $shf_xpath); + TBB::Crash::crash35 "Expected one (1) System Hidden Field node found with xpath '$shf_xpath'. Found " + . (scalar @$shf_parent_nodelist || "0") . "!\n" + unless ((scalar @$shf_parent_nodelist) == 1); + my $shf_parent_node = $shf_parent_nodelist->[0]; + + # Create a new System Hidden Field node with attribute name="$field_name" + # and value="$value". + my $new_shf_node = $bnode_tree->create_node( $self->{system_hidden_field_node_name} ); + $bnode_tree->add_attributes( $new_shf_node, {name => $field_name} ); + $bnode_tree->add_value( $new_shf_node, $value ); + + # Append the new node to the appropriate place in the BNode's XML tree. + $bnode_tree->append_child($shf_parent_node, $new_shf_node); +} + +=item __set_hidden_form_id () + + ##?REFACTOR: POD !! + +=cut +sub __set_hidden_form_id +{ + use Digest::MD5; + my $self = shift; + + my $form_id_html_field_name = $TBB::Config->get_config_parameters('form_id'=>'form_parameters/form_id'); + + srand( int( rand(time()) ) ); + my $form_id = Digest::MD5::md5_hex( rand(time()) ); + + # In the test environment, fake $form_id so that we can have explicit matching + # against a pre-canned XML output: + my $force_MD5 = $TBB::Config->get_config_parameters('md5'=>'test/force_MD5_sum_id'); + $form_id = $force_MD5 + if ($force_MD5); + + #TBB::LogManager::write('debug', "Adding hidden form id [$form_id_html_field_name => $form_id]"); + + $self->__set_system_hidden_field($form_id_html_field_name, $form_id); + return $form_id; +} + +=item __populate_page_text($user_data_obj, $id_user, $ordinal, %subroutine_opts) + + Fills GQ:MQ and FM strings in all textopts nodes on the page NOT associated with + questions. + +=cut +sub __populate_page_text +{ + my $self = shift; + my ($user_data_obj, $id_user, $ordinal, %subroutine_opts) = @_; + + my $bnode_tree = $self->get_bnode()->get_xml_resource(); + my @page_text_nodes = ('head', 'paragraph', 'title', 'subtitle', 'description', 'interface'); + + foreach my $node_type (@page_text_nodes) + { + my $xpath = 'descendant::' . $node_type; + my $text_container_nodes = $bnode_tree->get_nodes_by_xpath($bnode_tree->get_root(), $xpath); + + foreach my $container (@$text_container_nodes) + { + $self->__process_text_options($bnode_tree, $container, $user_data_obj, $id_user, $ordinal); + } + } + + return 1; +} + +=item __process_text_options($bnode_tree, $node_to_process, $user_data_obj, $id_user]) + + Finds all of the textoptions nodes that are children of $node_to_process, + and replaces all UDIDs contained in those text nodes with their value in + $user_data. If $id_user is provided, it uses the value of $id_user as + the id_user, otherwise it uses $user_data->current_client_id() + + $bnode_tree is the XML tree containing the bnode that we're populating + + $node_to_process is the node whose descendants will be considered + for processing + + $user_data_obj is a reference to a TBB::UserData object + + $id_user is the id_user of the user whose data will be integrated + into the text nodes (optional) + + Note that as of this check-in (use bonsai to see which one I mean) we + really actually parse text() nodes (in an xml sense) not just nodes + that we have been calling .... This means we can do inline crazy + shit like + + + + I am some blah text and I contain another + right in the FM000-002-311 middle and glossary words and + all sorts of craziness. + + + +=cut +sub __process_text_options +{ + my $self = shift; + my ($bnode_tree, $node_to_process, $user_data_obj, $this_id_user, $this_instance, %opts) = @_; + my $context = $opts{context}; + + # If there isn't a node to process then we're all done. + return 0 + unless ($node_to_process); + + my $questions_nodes = + $bnode_tree->get_nodes_by_xpath($node_to_process, + "descendant::questions"); + TBB::Crash::crash36 "__process_text_options called on a parent of a 'questions' node" + if (scalar @$questions_nodes > 0); + + $this_id_user ||= $user_data_obj->current_client_id(); + + my $text_nodes = $bnode_tree->get_nodes_by_xpath( + $node_to_process, + 'descendant::text' + ); + return if (scalar @$text_nodes == 0); + + ##?REFACTOR: Use TBB::ID. + my $question_regex = TBB::ID_old::QUESTION_REGEX(); + my $formula_regex = TBB::ID_old::FORMULA_REGEX(); + + ##?TODO: Note that the following strategy is a bit expensive: We iterate over + # *all* text nodes, rather than just focusing on the ones we know are + # needed for the current page. A better strategy would be to *only* + # address the nodes of the language we are displaying on the page. + # This, however would require that either: + # (1) every call to __process_text_options would pass in a + # current_language => 'whatever' opt, or + # (2) we set $self->{current_language} in a meaningful way and + # then used that. + # Finally, it's worth mentioning that both of these strategies are + # brittle as we *always* want to process the en-US version of a node: + # It is needed for "failover" if a translated version of the node is + # not available. + + foreach my $text_node (@$text_nodes) + { + ##?REFACTOR: Create a central string process method + ##?NOTE: The attribute is coded as 'xml:lang' but, for reasons which I + # must confess not to understand, the xpath which finds it is + # just to search for 'lang'. Beats me why.... + my $node_language = $bnode_tree->get_attribute( $text_node, 'lang' ) || 'en-US'; + + # Now iterate over all the individual textnodes (not nodes, but the + # actual blobs of text) within this node. + # eg. "Your household needs help. + # + # has 3 textnodes: + # 1) "Your " + # 2) "household" + # 3) " needs help." + # + # Right now, we handle each of these separately. + ##?TODO: We HAVE to handle them separately because TBB::XML::set_node_value + # doesn't know how to set multi-node values correctly. Once it is + # REFACTORED to do so then we can replace a lot of this strategy with + # just processing at the node level instead. + + ##?NOTE: 'descendant::text' returns xml nodes of type 'text', ie, the node + # in our example above. 'descendant::text()' returns xml textnodes, + # ie the text fragments like "Your " in the example above. + my $text_fragments = $bnode_tree->get_nodes_by_xpath( $text_node, 'descendant::text()' ); + + foreach my $text_fragment (@$text_fragments) + { + ##?TODO: Try to pull all this native LibXML stuff out of this method. + my $text_node_value = $text_fragment->data(); + + # Iterate over any GQ:MQ, FM, INSTANCE(GQ:MQ) and the now deprecated INSTANCE(FM) + # matches in the text. For each one, get its value from UserData (for the current + # user, instance, etc), normalize it for presentation and then substitute it into + # the string in place of the original. + # Note that the 'g' flag means we will iterate over matches. + ##?REVIEW: IS the 'g' flag still necessary given that we now *always* substitute + # even if we can't find a match? + while ($text_node_value =~ /((INSTANCE\()?(\S+:)?($question_regex|$formula_regex)\)?)/mg) + { + # This is the complete text of the text_node_value, including INSTANCE + my $token = $1; + # The instance wrapped expression + my $instance_function = $2; + # The aliasing on the front of the item, or nothing + my $alias = $3 || ''; + # The question or formula ID + my $query_stem = $4; + + # Strip the : off the end of the alias + $alias =~ s/:$//; + + if (TBB::LogManager::writes_at('debug')) { + TBB::LogManager::write_log( + 'debug', + "PROCESS TEXT OPTIONS:\n" . + "token = \"" . ($token || '') . "\"\n" . + "instance function = \"" . ($instance_function || '') . "\"\n" . + "alias = \"" . ($alias || '') . "\"\n" . + "query stem = \"" . ($query_stem || '') . "\"\n" + ); + } + + # we only want to pass an instance if it really is an instance, and not if the + # last one was an instance + ##?NOTE: Use a separate variable so we don't clobber the global $this_instance + # in any one case. + my $instance_to_pass = $this_instance; + + ##?REVIEW: there are cases in the wild where $query_stem is undef! + # I don't understand why and cannot deal with the issue right + # now, so instead am adding the following hack: + if (defined($query_stem)) { + + ##?NOTE: Instance masking should only be for questions. + if (TBB::ID::is_a( $query_stem, 'question')) + { + if ($instance_function) + { + # There is an INSTANCE() block round this GQ:MQ, so there better + # should be an instance to pass. + TBB::Crash::crash37 "Cannot evaluate \"$text_node_value\" on " + . $self->get_bnode()->get_id() + . " : No instance given to $0->__process_text_options()\n" + unless ($instance_to_pass); + } + else + { + undef $instance_to_pass; + } + } + + } else { + TBB::LogManager::write("debug", "JDM: query stem is undefined!"); + } + + ##?REVIEW: Could we make normalize => 'for_presentation' an + # opt to UD->retrieve_value? Might save repeating + # a chunk of coding in the various places we use it.... + my $value = $user_data_obj->retrieve_value( + base => $query_stem, + alias => $alias, + this_id_user => $this_id_user, + this_instance => $instance_to_pass, + ); + + ### Normalize the value for the current language. + my $normalizer = $self->{resource_manager}->get_normalizer(); + $value = $normalizer->normalize_for_presentation( + $value, + $query_stem, + $node_language, + context => $context + ); + + # If for some reason we have an undef value then just use null string. + unless (defined $value) + { + # Get rid of the embarassing TBB::ID so it doesn't appear on the page. + $value = ''; + + # But also log this error as it's not cool. + TBB::LogManager::write( + 'warn', + "Unable to get a defined value for $token from UserData." + ); + } + + unless ($text_node_value =~ s/\Q$token\E/$value/g) + { + TBB::Crash::crash38 "Messed up the token and now can't find \"$token\" in the string \"$text_node_value\".\n"; + } + } + + # Set the final text string in the text node. + ##?REVIEW: Again, we should use TBB::XML, not LibXML. + $text_fragment->setData( $text_node_value ); + } + } +} + +=item __add_question_userdata_node ($id_user, $gq_id, $mq_id, $this_instance, $parent_node, $user_data, $label_textopts, $default_value, %opts) + + Adds a node to the XML tree as a child of $parent_node. + + $gq_id and $mq_id have to be valid General Question and Micro Question ids. + $label_textopts is a