text: # coding: utf8
  
  """
      Delphi Decision Maker - Controllers
  """
  
  module = request.controller
  resourcename = request.function
  
  if not settings.has_module(module):
      raise HTTP(404, body="Module disabled: %s" % module)
  
  # =============================================================================
  def index():
      """
          Module Home Page
          - provide the list of currently-Active Problems
      """
  
      # Simply redirect to the Problems REST controller
      redirect(URL(f="problem"))
  
      # Alternative dashboard
      module_name = settings.modules[module].name_nice
  
      table = s3db.delphi_group
      groups = db(table.active == True).select()
      result = []
      for group in groups:
          actions = []
          duser = s3db.delphi_DelphiUser(group)
          if duser.authorised:
              actions.append(("group/%d/update" % group.id, T("Edit")))
              actions.append(("new_problem/create/?group=%s&next=%s" % \
                      (group.id,
                      URL(f="group_summary", args=group.id)),
                      "Add New Problem"))
              actions.append(("group_summary/%s/#request" % group.id, T("Review Requests")))
          else:
              actions.append(("group_summary/%s/#request" % group.id,
                      "Role: %s%s" % (duser.status,
                                      (duser.membership and duser.membership.req) and "*" or "")))
  
          table = s3db.delphi_problem
          query = (table.group_id == group.id) & \
                  (table.active == True)
          latest_problems = db(query).select(orderby =~ table.modified_on)
          result.append((group, latest_problems, actions))
  
      response.title = module_name
      return dict(groups_problems=result,
                  name=T("Active Problems"),
                  module_name=module_name)
  
  # =============================================================================
  # Groups
  # =============================================================================
  def group_rheader(r, tabs = []):
      """ Group rheader """
  
      if r.representation == "html":
          if r.record is None:
              # List or Create form: rheader makes no sense here
              return None
  
          tabs = [(T("Basic Details"), None),
                  (T("Problems"), "problem"),
                  ]
  
          group = r.record
  
          # Get this User's permissions for this Group
          duser = s3db.delphi_DelphiUser(group.id)
          if duser.authorised:
              tabs.append((T("Membership"), "membership"))
  
          rheader_tabs = s3_rheader_tabs(r, tabs)
  
          rheader = DIV(
                      TABLE(
                          TR(TH("%s: " % T("Group")),
                             group.name,
                             ),
                          TR(TH("%s: " % T("Description")),
                             group.description,
                             ),
                          TR(TH("%s: " % T("Active")),
                             group.active,
                             ),
                          ),
                          rheader_tabs
                      )
          return rheader
      return None
  
  # -----------------------------------------------------------------------------
  def group():
      """ Problem Group REST Controller """
  
      if not s3_has_role("DelphiAdmin"):
          s3db.configure("delphi_group",
                         deletable=False,
                         # Remove ability to create new Groups
                         #insertable=False
                         )
  
      def prep(r):
          if r.interactive:
  
              if r.component:
                  tablename = r.component.tablename
                  list_fields = s3db.get_config(tablename,
                                                "list_fields")
                  try:
                      list_fields.remove("group_id")
                  except:
                      pass
                  s3db.configure(tablename,
                                 deletable = s3_has_role("DelphiAdmin"),
                                 list_fields = list_fields)
          return True
      s3.prep = prep
  
      rheader = group_rheader
      return s3_rest_controller(rheader=rheader,
                                # Allow components with components (such as problem) to breakout from tabs
                                native=True,
                                )
  
  # =============================================================================
  # Problems
  # =============================================================================
  def problem_rheader(r, tabs = []):
      """ Problem rheader """
  
      if r.representation == "html":
          if r.record is None:
              # List or Create form: rheader makes no sense here
              return None
  
          problem = r.record
  
          tabs = [# Components & Custom Methods
                  (T("Problems"), "problems"),
                  (T("Solutions"), "solution"),
                  (T("Discuss"), "discuss"),
                  (T("Vote"), "vote"),
                  (T("Scale of Results"), "results"),
                  ]
  
          # Get this User's permissions for this Group
          duser = s3db.delphi_DelphiUser(problem.group_id)
          if duser.authorised:
              tabs.append((T("Edit"), None))
  
          rheader_tabs = s3_rheader_tabs(r, tabs)
  
          rtable = TABLE(TR(TH("%s: " % T("Problem")),
                            problem.name,
                            TH("%s: " % T("Active")),
                            problem.active,
                            ),
                         TR(TH("%s: " % T("Description")),
                            problem.description,
                            ),
                         TR(TH("%s: " % T("Criteria")),
                            problem.criteria,
                            ),
                         )
  
          if r.component and \
             r.component_name == "solution" and \
             r.component_id:
              stable = s3db.delphi_solution
              query = (stable.id == r.component_id)
              solution = db(query).select(stable.name,
                                          stable.description,
                                          limitby=(0, 1)).first()
              rtable.append(DIV(TR(TH("%s: " % T("Solution")),
                                   solution.name,
                                   ),
                                TR(TH("%s: " % T("Description")),
                                   solution.description,
                                   ),
                                ))
  
          rheader = DIV(rtable,
                        rheader_tabs)
          return rheader
      return None
  
  # -----------------------------------------------------------------------------
  def problem():
      """ Problem REST Controller """
  
      tablename = "%s_%s" % (module, resourcename)
      table = s3db[tablename]
  
      # Custom Methods
      set_method = s3db.set_method
      set_method(module, resourcename,
                 method="problems",
                 action=problems)
  
      set_method(module, resourcename,
                 method="discuss",
                 action=discuss)
  
      # Discussion can also be done at the Solution component level
      set_method(module, resourcename,
                 component_name="solution",
                 method="discuss",
                 action=discuss)
  
      set_method(module, resourcename,
                 method="vote",
                 action=vote)
  
      set_method(module, resourcename,
                 method="results",
                 action=results)
  
      # Filter to just Active Problems
      s3.filter = (table.active == True)
  
      if not s3_has_role("DelphiAdmin"):
          s3db.configure(tablename,
                         deletable=False,
                         # Remove ability to create new Problems
                         #insertable=False
                         )
  
      def prep(r):
          if r.interactive:
              if r.record:
                  duser = s3db.delphi_DelphiUser(r.record.group_id)
                  if duser.authorised:
                      s3db.configure(tablename,
                                     deletable=True,
                                     )
              if r.component_name == "solution":
                  r.component.table.modified_on.label = T("Last Updated")
                  s3db.configure(r.component.tablename,
                                 deletable = duser.authorised,
                                 )
          return True
      s3.prep = prep
  
      def postp(r, output):
          if r.interactive:
              if not r.component:
                  s3.actions = [
                          dict(label=str(T("Solutions")),
                               _class="action-btn",
                               url=URL(args=["[id]", "solution"])),
                          dict(label=str(T("Vote")),
                               _class="action-btn",
                               url=URL(args=["[id]", "vote"])),
                      ]
              elif r.component_name == "solution":
                  s3.actions = [
                          dict(label=str(T("Discuss")),
                               _class="action-btn",
                               url=URL(args=["solution", "[id]", "discuss"])),
                      ]
          return output
      s3.postp = postp
  
      rheader = problem_rheader
      return s3_rest_controller(rheader=rheader)
  
  # -----------------------------------------------------------------------------
  def problems(r, **attr):
      """
          Redirect to the list of Problems for the Group
          - used for a Tab
      """
  
      try:
          group_id = r.record.group_id
      except:
          raise HTTP(400)
      else:
          redirect(URL(f="group", args=[group_id, "problem"]))
  
  # =============================================================================
  # Voting
  # =============================================================================
  def vote(r, **attr):
      """
          Custom Method to allow Voting on Solutions to a Problem
      """
  
      problem = r.record
  
      # Get this User's permissions for this Group
      duser = s3db.delphi_DelphiUser(problem.group_id)
  
      # Add the RHeader to maintain consistency with the other pages
      rheader = problem_rheader(r)
  
      # Lookup Solution Options
      stable = s3db.delphi_solution
      query = (stable.problem_id == problem.id)
      rows = db(query).select(stable.id,
                              stable.name)
      options = Storage()
      for row in rows:
          options[row.id] = row.name
  
      if duser.user_id:
          vtable = s3db.delphi_vote
          query = (vtable.problem_id == problem.id) & \
                  (vtable.created_by == auth.user.id)
          votes = db(query).select(vtable.solution_id,
                                   orderby = vtable.rank)
      else:
          votes = []
  
      rankings = OrderedDict()
      for v in votes:
          # Add to the list of ranked options
          rankings[v.solution_id] = options[v.solution_id]
          # Remove from the unranked options
          options.pop(v.solution_id)
  
      # Add Custom CSS from Static (cacheable)
      s3.stylesheets.append("S3/delphi.css")
  
      # Add Custom Javascript
      # Settings to be picked up by Static code
      js = "".join((
  '''var problem_id=''', str(problem.id), '''
  i18n.delphi_failed="''', str(T("Failed!")), '''"
  i18n.delphi_saving="''', str(T("Saving...")), '''"
  i18n.delphi_saved="''', str(T("Saved.")), '''"
  i18n.delphi_vote="''', str(T("Save Vote")), '''"'''))
      s3.js_global.append(js)
  
      # Static code which can be cached
      s3.scripts.append(URL(c="static", f="scripts",
                            args=["S3", "s3.delphi.js"]))
  
      response.view = "delphi/vote.html"
      return dict(rheader=rheader,
                  duser=duser,
                  votes=votes,
                  options=options,
                  rankings=rankings)
  
  # -----------------------------------------------------------------------------
  def save_vote():
      """
          Function accessed by AJAX from vote() to save the results of a Vote
      """
  
      try:
          problem_id = request.args[0]
      except:
          raise HTTP(400)
  
      ptable = s3db.delphi_problem
      query = (ptable.id == problem_id)
      problem = db(query).select(ptable.group_id,
                                 limitby=(0, 1)).first()
      if not problem:
          raise HTTP(404)
  
      # Get this User's permissions for this Group
      duser = s3db.delphi_DelphiUser(problem.group_id)
  
      if not duser.can_vote:
          auth.permission.fail()
  
      # Decode the data
      try:
          rankings = request.post_vars.keys()[0].split(",")
      except IndexError:
          status = current.xml.json_message(False, 400, "No Options Ranked")
          raise HTTP(400, body=status)
  
      # Check the votes are valid
      stable = s3db.delphi_solution
      query = (stable.problem_id == problem_id)
      solutions = db(query).select(stable.id)
      options = []
      for row in solutions:
          options.append(row.id)
      for ranked in rankings:
          if int(ranked) not in options:
              status = current.xml.json_message(False, 400, "Option isn't valid!")
              raise HTTP(400, body=status)
  
      # Convert to a format suitable for comparisons
      votes = []
      count = 1
      for ranked in rankings:
          votes.append(Storage(solution_id=int(ranked), rank=count))
          count += 1
  
      # Read the old votes
      vtable = s3db.delphi_vote
      query = (vtable.problem_id == problem_id) & \
              (vtable.created_by == auth.user.id)
      old_votes = db(query).select(vtable.solution_id,
                                   vtable.rank)
      if old_votes:
          # Calculate changes
          ranks = {}
          old_ranks = {}
          used = []
          for solution in solutions:
              s1 = solution.id
              ranks[s1] = 0
              old_ranks[s1] = 0
              for vote in votes:
                  if vote.solution_id == s1:
                      ranks[s1] = vote.rank
                      continue
              for vote in old_votes:
                  if vote.solution_id == s1:
                      old_ranks[s1] = vote.rank
                      continue
              for sol_2 in solutions:
                  changed = False
                  s2 = sol_2.id
                  if s2 == s1:
                      continue
                  if (s2, s1) in used:
                      # We've already evaluated this pair
                      continue
                  ranks[s2] = 0
                  old_ranks[s2] = 0
                  for vote in votes:
                      if vote.solution_id == s2:
                          ranks[s2] = vote.rank
                          continue
                  for vote in old_votes:
                      if vote.solution_id == s2:
                          old_ranks[s2] = vote.rank
                          continue
                  if (ranks[s1] > ranks[s2]) and \
                     (old_ranks[s1] < old_ranks[s2]):
                      changed = True
                  elif (ranks[s1] < ranks[s2]) and \
                       (old_ranks[s1] > old_ranks[s2]):
                      changed = True
                  elif (ranks[s1] == ranks[s2]) and \
                       (old_ranks[s1] != old_ranks[s2]):
                      changed = True
                  elif (ranks[s1] != ranks[s2]) and \
                       (old_ranks[s1] == old_ranks[s2]):
                      changed = True
                  if changed:
                      # This pair has changed places, so update Solution
                      db(stable.id.belongs((s1, s2))).update(changes=stable.changes + 1)
                  used.append((s1, s2))
  
      # Clear the old votes
      db(query).delete()
  
      # Save the new votes
      count = 1
      for ranked in rankings:
          vtable.insert(problem_id=problem_id, solution_id=ranked, rank=count)
          count += 1
  
      status = current.xml.json_message(True, 200, "Vote saved")
      return status
  
  # -----------------------------------------------------------------------------
  def _getUnitNormalDeviation(zscore):
      """
          Utility function used by Scale of Results
  
          Looks up the Unit Normal Deviation based on the Z-Score (Proportion/Probability)
          http://en.wikipedia.org/wiki/Standard_normal_table
  
          @ToDo: Move to S3Statistics module
      """
  
      UNIT_NORMAL = (
          ( 0.0, .0, .01, .02, .03, .04, .05, .06, .07, .08, .09 ),
          ( .0, .5000, .5040, .5080, .5120, .5160, .5199, .5239, .5279, .5319, .5359 ),
          ( .1, .5398, .5438, .5478, .5517, .5557, .5596, .5636, .5675, .5714, .5753 ),
          ( .2, .5793, .5832, .5871, .5910, .5948, .5987, .6026, .6064, .6103, .6141 ),
          ( .3, .6179, .6217, .6255, .6293, .6331, .6368, .6406, .6443, .6480, .6517 ),
          ( .4, .6554, .6591, .6628, .6664, .6700, .6736, .6772, .6808, .6844, .6879 ),
  
          ( .5, .6915, .6950, .6985, .7019, .7054, .7088, .7123, .7157, .7190, .7224 ),
          ( .6, .7257, .7291, .7324, .7357, .7389, .7422, .7454, .7486, .7517, .7549 ),
          ( .7, .7580, .7611, .7642, .7673, .7703, .7734, .7764, .7794, .7823, .7852 ),
          ( .8, .7881, .7910, .7939, .7967, .7995, .8023, .8051, .8078, .8106, .8133 ),
          ( .9, .8159, .8186, .8212, .8238, .8264, .8289, .8315, .8340, .8365, .8389 ),
  
          ( 1.0, .8415, .8438, .8461, .8485, .8508, .8531, .8554, .8577, .8509, .8621 ),
          ( 1.1, .8643, .8665, .8686, .8708, .8729, .8749, .8770, .8790, .8810, .8830 ),
          ( 1.2, .8849, .8869, .8888, .8907, .8925, .8944, .8962, .8980, .8997, .90147 ),
          ( 1.3, .90320, .90490, .90658, .90824, .90988, .91149, .91309, .91466, .91621, .91774 ),
          ( 1.4, .91924, .92073, .92220, .92364, .92507, .92647, .92785, .92922, .93056, .93189 ),
  
          ( 1.5, .93319, .93448, .93574, .93699, .93822, .93943, .94062, .94179, .94295, .94408 ),
          ( 1.6, .94520, .94630, .94738, .94845, .94950, .95053, .95154, .95254, .95352, .95449 ),
          ( 1.7, .95543, .95637, .95728, .95818, .95907, .95994, .96080, .96164, .96246, .96327 ),
          ( 1.8, .96407, .96485, .96562, .96638, .96712, .96784, .97856, .96926, .96995, .97062 ),
          ( 1.9, .97128, .97193, .97257, .97320, .97381, .97441, .97500, .97558, .97615, .97670 ),
  
          ( 2.0, .97725, .97778, .97831, .97882, .97932, .97982, .98030, .98077, .98124, .98169 ),
          ( 2.1, .98214, .98257, .98300, .98341, .98382, .98422, .98461, .98500, .98537, .98574 ),
          ( 2.2, .98610, .98645, .98679, .98713, .98745, .98778, .98809, .98840, .98870, .98899 ),
          ( 2.3, .98928, .98956, .98983, .990097, .990358, .990613, .990863, .991106, .991344, .991576 ),
          ( 2.4, .991802, .992024, .992240, .992451, .992656, .992857, .993053, .993244, .993431, .993613 ),
  
          ( 2.5, .993790, .993963, .994132, .994297, .994457, .994614, .994766, .994915, .995060, .995201 ),
          ( 2.6, .995339, .995473, .995604, .995731, .995855, .995975, .996093, .996207, .996319, .996427 ),
          ( 2.7, .996533, .996636, .996736, .996833, .996928, .997020, .997110, .997197, .997282, .997365 ),
          ( 2.8, .997445, .997523, .997599, .997673, .997744, .997814, .997882, .997948, .998012, .998074 ),
          ( 2.9, .998134, .998193, .998250, .998305, .998359, .998411, .998460, .998511, .998559, .998605 ),
  
          ( 3.0, .998650, .998694, .998736, .998777, .998817, .998856, .998893, .998930, .998965, .998999 ),
          ( 3.1, .9990324, .9990646, .9990957, .9991260, .9991553, .9991836, .9992112, .9992378, .9992636, .9992886 ),
          ( 3.2, .9993129, .9993363, .9993590, .9993810, .9994024, .9994230, .9994429, .9994623, .9994810, .9994991 ),
          ( 3.3, .9995166, .9995335, .9995499, .9995658, .9995811, .9995959, .9996103, .9996242, .9996376, .9996505 ),
          ( 3.4, .9996631, .9996752, .9996869, .9996982, .9997091, .9997197, .9997299, .9997398, .9997493, .9997585 ),
  
          ( 3.5, .9997674, .9997759, .9997842, .9997922, .9997999, .9998074, .9998146, .9998215, .9998282, .9998347 ),
          ( 3.6, .9998409, .9998469, .9998527, .9998583, .9998637, .9998689, .9998739, .9998787, .9998834, .9998879 ),
          ( 3.7, .9998922, .9998964, .99990039, .99990426, .99990799, .99991158, .99991504, .99991838, .99992159, .99992468 ),
          ( 3.8, .99992765, .99993052, .99993327, .99993593, .99993848, .99994094, .99994331, .99994558, .99994777, .99994988 ),
          ( 3.9, .99995190, .99995385, .99995573, .99995753, .99995926, .99996092, .99996253, .99996406, .99996554, .99996696 ),
  
          ( 4.0, .99996833, .99996964, .99997090, .99997211, .99997327, .99997439, .99997546, .99997649, .99997748, .99997843 ),
          ( 4.1, .99997934, .99998022, .99998106, .99998186, .99998263, .99998338, .99998409, .99998477, .99998542, .99998605 ),
          ( 4.2, .99998665, .99998723, .99998778, .99998832, .99998882, .99998931, .99998978, .999990226, .999990655, .999991066 ),
          ( 4.3, .999991460, .999991837, .999992199, .999992545, .999992876, .999993193, .999993497, .999993788, .999994066, .999994332 ),
          ( 4.4, .999994587, .999994831, .999995065, .999995288, .999995502, .999995706, .999995902, .999996089, .999996268, .999996439 ),
  
          ( 4.5, .999996602, .999996759, .999996908, .999997051, .999997187, .999997318, .999997442, .999997561, .999997675, .999997784 ),
          ( 4.6, .999997888, .999997987, .999998081, .999998172, .999998258, .999998340, .999998419, .999998494, .999998566, .999998634 ),
          ( 4.7, .999998699, .999998761, .999998821, .999998877, .999998931, .999998983, .9999990320, .9999990789, .9999991235, .9999991661 ),
          ( 4.8, .9999992067, .9999992453, .9999992822, .9999993173, .9999993508, .9999993827, .9999994131, .9999994420, .9999994696, .9999994958 ),
          ( 4.9, .9999995208, .9999995446, .9999995673, .9999995889, .9999996094, .9999996289, .9999996475, .9999996652, .9999996821, .9999996981 )
      )
  
      # Assume indifference
      unitDeviation = 0.0
  
      for j in range(1, 50):
          if zscore == UNIT_NORMAL[j][1]:
              unitDeviation = UNIT_NORMAL[j][0]
          elif (UNIT_NORMAL[j][1] < zscore) and (zscore < UNIT_NORMAL[j + 1][1]):
              for i in range(2, 10):
                  if (UNIT_NORMAL[j][i - 1] < zscore) and (zscore <= UNIT_NORMAL[j][i]):
                      unitDeviation = UNIT_NORMAL[j][0] + UNIT_NORMAL[0][i]
              if zscore > UNIT_NORMAL[j][10]:
                  unitDeviation = UNIT_NORMAL[j + 1][0]
  
      if zscore > UNIT_NORMAL[50][10]:
          # maximum value
          unitDeviation = 5.0
  
      return unitDeviation
  
  # -----------------------------------------------------------------------------
  def online_variance(data):
      """
          A numerically stable algorithm for calculating variance
          http://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#On-line_algorithm
      """
  
      n = 0
      mean = 0
      M2 = 0
  
      for x in data:
          n = n + 1
          delta = x - mean
          mean = mean + delta/n
          M2 = M2 + delta*(x - mean)
  
      variance_n = M2/n
      variance = M2/(n - 1)
      return (variance, variance_n)
  
  # -----------------------------------------------------------------------------
  def results(r, **attr):
      """
          Custom Method to show the Scale of Results
      """
  
      def NBSP():
          return XML("&nbsp;")
  
      # Add the RHeader to maintain consistency with the other pages
      rheader = problem_rheader(r)
  
      response.view = "delphi/results.html"
  
      empty = dict(rheader=rheader,
                   num_voted=0,
                   chart="",
                   table_color="",
                   grids="",
                   summary=""
                   )
  
      problem = r.record
  
      # Lookup Votes
      if problem:
          vtable = s3db.delphi_vote
          query = (vtable.problem_id == problem.id)
          votes = db(query).select(vtable.solution_id,
                                   vtable.rank,
                                   vtable.created_by)
      else:
          votes = None
      if not votes:
          return empty
  
      # Lookup Solutions
      stable = s3db.delphi_solution
      query = (stable.problem_id == problem.id)
      solutions = db(query).select(stable.id,
                                   stable.name,
                                   stable.problem_id, # Needed for Votes virtual field
                                   stable.changes)
  
      if not solutions:
          return empty
  
      # Initialise arrays of pairwise comparisons
      arrayF = {}
      arrayP = {}
      arrayX = {}
      arrayP2 = {}
      arrayU = {}
      for solution in solutions:
          s1 = solution.id
          for sol_2 in solutions:
              s2 = sol_2.id
              if s1 == s2:
                  arrayF[(s1, s2)] = None
                  arrayP[(s1, s2)] = None
                  arrayX[(s1, s2)] = None
                  arrayP2[(s1, s2)] = None
                  arrayU[(s1, s2)] = None
                  continue
              arrayF[(s1, s2)] = 0
              # Allow new solutions to start at an indifferent probability
              arrayP[(s1, s2)] = 0.5
              arrayX[(s1, s2)] = 0
              arrayP2[(s1, s2)] = 0.5
              arrayU[(s1, s2)] = 0.5
  
      # List of Voters
      voters = []
      for vote in votes:
          voter = vote.created_by
          if voter not in voters:
              voters.append(voter)
      num_voted = len(voters)
  
      # Update array of pairwise comparisons based on votes
      # Create array F which is the number of time a solution has been preferred compared to it'a partner
      for voter in voters:
          ranks = {}
          for vote in votes:
              if vote.created_by != voter:
                  continue
              ranks[vote.rank] = vote.solution_id
          for rank_1 in range(1, len(ranks)):
              for rank_2 in range(rank_1 + 1, len(ranks) + 1):
                  arrayF[(ranks[rank_1], ranks[rank_2])] += 1
  
      grids = DIV()
      header = TR(TD())
      rows = TBODY()
      for solution in solutions:
          header.append(TH(solution.name))
          s1 = solution.id
          row = TR(TH(solution.name))
          for sol_2 in solutions:
              s2 = sol_2.id
              # Preferred should be the columns
              value = arrayF[(s2, s1)]
              if value is None:
                  row.append(TD("-"))
              else:
                  row.append(TD(value))
          rows.append(row)
      output = TABLE(THEAD(header), rows,
                     _class="delphi_wide")
      output = DIV(H4("Array F: # times that solution in column is preferred over it's partner in row"),
                   output)
      grids.append(output)
      grids.append(NBSP())
  
      # Use the pairwise comparisons to build a Dynamic Thurstone scale of results
      # http://en.wikipedia.org/wiki/Thurstone_scale
      # http://www.brocku.ca/MeadProject/Thurstone/Thurstone_1927a.html
      # http://www.brocku.ca/MeadProject/Thurstone/Thurstone_1927f.html
      # @ToDo: For incomplete data, the calculation is more complex: Gulliksen
      # Convert array F to array P by converting totals to proportions
      # Convert array P to array X, which is the unit normal deviate
      for solution in solutions:
          s1 = solution.id
          for sol_2 in solutions:
              s2 = sol_2.id
              if s1 == s2:
                  continue
              total = float(arrayF[(s1, s2)] + arrayF[(s2, s1)])
              # Preferred should be the columns
              if total:
                  proportion = arrayF[(s2, s1)] / total
              else:
                  # No votes yet, so assume indifference
                  proportion = 0.5
              arrayP[(s2, s1)] = proportion
              # Cannot do unit normal deviation for 0/1 so approximate in order to not have to do the incomplete data maths
              if proportion == 0.0:
                  arrayX[(s2, s1)] = _getUnitNormalDeviation(0.01)
              elif proportion == 1.0:
                  arrayX[(s2, s1)] = _getUnitNormalDeviation(0.99)
              else:
                  arrayX[(s2, s1)] = _getUnitNormalDeviation(proportion)
              # Now calculate the uncertainty scale
              # i.e. assume that every user who didn't vote on a particular pair drags that back towards indifference
              novotes = num_voted - total
              if proportion == 0.5:
                  pass
              elif proportion > 0.5:
                  # Assume the novotes vote against
                  proportion = (arrayF[s2, s1] - novotes) / num_voted
              else:
                  # Assume the novotes vote for
                  proportion = (arrayF[s2, s1] + novotes) / num_voted
              arrayP2[(s2, s1)] = proportion
              # Cannot do unit normal deviation for 0/1 so approximate in order to not have to do the incomplete data maths
              if proportion == 0.0:
                  arrayU[(s2, s1)] = _getUnitNormalDeviation(0.01)
              elif proportion == 1.0:
                  arrayU[(s2, s1)] = _getUnitNormalDeviation(0.99)
              else:
                  arrayU[(s2, s1)] = _getUnitNormalDeviation(proportion)
  
      header = TR(TD())
      rows = TBODY()
      for solution in solutions:
          header.append(TH(solution.name))
          s1 = solution.id
          row = TR(TH(solution.name))
          for sol_2 in solutions:
              s2 = sol_2.id
              # Preferred should be the columns
              value = arrayP[(s2, s1)]
              if value is None:
                  row.append(TD("-"))
              else:
                  row.append(TD(value))
  
          rows.append(row)
      output = TABLE(THEAD(header), rows,
                     _class="delphi_wide")
      output = DIV(H4("Array P: proportion of times that solution in column is preferred over it's partner in row, assuming that pairs not ranked start at the level of indifference (0.5)"),
                   output)
      grids.append(output)
      grids.append(NBSP())
  
      header = TR(TD())
      rows = TBODY()
      footer = TR(TH("Total"))
      footer2 = TR(TH("Scale"))
      totals = {}
      counts = {}
      for solution in solutions:
          s1 = solution.id
          totals[s1] = 0
          counts[s1] = 0
      for solution in solutions:
          header.append(TH(solution.name))
          s1 = solution.id
          row = TR(TH(solution.name))
          for sol_2 in solutions:
              s2 = sol_2.id
              # Preferred should be the columns
              value = arrayX[(s2, s1)]
              if value is None:
                  row.append(TD("-"))
              else:
                  row.append(TD(value))
              if value is not None:
                  totals[s2] += value
                  counts[s2] += 1
          rows.append(row)
  
      # Least-squares estimate of the scale values
      # Average of the columns
      for solution in solutions:
          s1 = solution.id
          footer.append(TH(totals[s1]))
          if counts[s1]:
              solution.scale = totals[s1]/counts[s1]
              footer2.append(TH(solution.scale))
          else:
              solution.scale = 0
              footer2.append(TH())
  
      output = TABLE(THEAD(header), rows, footer, footer2,
                     _class="delphi_wide")
      output = DIV(H4("Array X: unit normal deviate"),
                   output)
      grids.append(output)
      grids.append(NBSP())
  
      header = TR(TD())
      rows = TBODY()
      for solution in solutions:
          header.append(TH(solution.name))
          s1 = solution.id
          row = TR(TH(solution.name))
          for sol_2 in solutions:
              s2 = sol_2.id
              # Preferred should be the columns
              value = arrayP2[(s2, s1)]
              if value is None:
                  row.append(TD("-"))
              else:
                  row.append(TD(value))
  
          rows.append(row)
      output = TABLE(THEAD(header), rows,
                     _class="delphi_wide")
      output = DIV(H4("Array P2: proportion of times that solution in column is preferred over it's partner in row, assuming that non-votes move towards indifference"),
                   output)
      grids.append(output)
      grids.append(NBSP())
  
      header = TR(TD())
      rows = TBODY()
      footer = TR(TH("Total"))
      footer2 = TR(TH("Scale"))
      totals = {}
      counts = {}
      for solution in solutions:
          s1 = solution.id
          totals[s1] = 0
          counts[s1] = 0
      for solution in solutions:
          header.append(TH(solution.name))
          s1 = solution.id
          row = TR(TH(solution.name))
          for sol_2 in solutions:
              s2 = sol_2.id
              # Preferred should be the columns
              value = arrayU[(s2, s1)]
              if value is None:
                  row.append(TD("-"))
              else:
                  row.append(TD(value))
              if value is not None:
                  totals[s2] += value
                  counts[s2] += 1
          rows.append(row)
  
      # Least-squares estimate of the uncertainty values
      # Average of the columns
      for solution in solutions:
          s1 = solution.id
          footer.append(TH(totals[s1]))
          if counts[s1]:
              solution.uncertainty = totals[s1]/counts[s1]
              footer2.append(TH(solution.uncertainty))
          else:
              solution.uncertainty = 0
              footer2.append(TH())
  
      output = TABLE(THEAD(header), rows, footer, footer2,
                     _class="delphi_wide")
      output = DIV(H4("Array U: unit normal deviate of the uncertainty value (assuming that all unvoted items return the probability towards indifference"),
                   output)
      grids.append(output)
  
      # Sort the Solutions by Scale
      def scale(solution):
          return float(solution.scale)
  
      solutions = solutions.sort(scale, reverse=True)
  
      n = len(solutions)
  
      # @ToDo: deployment_setting
      image = ""
      if image:
          # Canvas of 900x600
          from s3chart import S3Chart
          chart = S3Chart(9, 6)
          fig = chart.fig
          # Add Axes with padding of 10px for the labels (fractional left, bottom, width, height)
          ax = fig.add_axes([0.35, 0.1, 0.6, 0.8])
  
          problem = r.record
          ax.set_title(problem.name)
  
          labels = []
          scales = []
          uncertainties = []
          for solution in solutions:
              labels.append(solution.name)
              scales.append(solution.scale)
              uncertainties.append(solution.uncertainty)
          from numpy import arange
          ind = arange(n)
          width = .35
          ax.set_yticks(ind + width)
          ax.set_yticklabels(labels)
          labels = ax.get_yticklabels()
          for label in labels:
              label.set_size(8)
  
          ax.set_xlabel("Scale") # rotation="vertical" or rotation = 45
          ax.xaxis.grid(True)
  
          rects1 = ax.barh(ind, scales, width, linewidth=0) # color="blue"
          rects2 = ax.barh(ind + width, uncertainties, width, linewidth=0, color="red")
  
          ax.legend( (rects1[0], rects2[0]), ("Scale", "Uncertainty") )
  
          image = chart.draw()
  
      # Colour the rows
      # Calculate Breaks
      classes = 5
      q = []
      qappend = q.append
      for i in range(classes - 1):
          qappend(1.0 / classes * (i + 1))
      values = [float(solution.scale) for solution in solutions]
      breaks = s3db.stats_quantile(values, q)
      # Make mutable
      breaks = list(breaks)
      values_min = min(values)
      values_max = max(values)
      breaks.insert(0, values_min)
      breaks.append(values_max)
      # Apply colours
      # 5-class BuGn from ColorBrewer.org
      colours = ["edf8fb",
                 "b2e2e2",
                 "66c2a4",
                 "2ca25f",
                 "006d2c",
                 ]
      for solution in solutions:
          for i in range(classes):
              value = solution.scale
              if value >= breaks[i] and \
                 value <= breaks[i + 1]:
                  solution.color = colours[i]
                  break
  
      # A table showing overall rankings
      thead = THEAD(
                  TR(
                      TH(T("Solution Item"), _rowspan="2"),
                      TH(T("Scale"), _rowspan="2"),
                      TH(T("Uncertainty"), _rowspan="2"),
                      TH(T("Activity Level"), _colspan="3"),
                  ),
                  TR(
                      TH(T("Voted on")),
                      TH(T("Times Changed")),
                      TH(T("Comments")),
                  ),
              )
      tbody = TBODY()
      for solution in solutions:
          rows = True
          tbody.append(
                  TR(
                      TD(solution.name),
                      TD(solution.scale,
                         _class="taright"),
                      TD(solution.uncertainty,
                        _class="taright"),
                      TD(solution.votes(),
                         _class="tacenter"),
                      TD(solution.changes,
                         _class="tacenter"),
                      TD(solution.comments(),
                         _class="tacenter"),
                      _style="background:#%s" % solution.color
                      )
              )
      summary = TABLE(thead,
                      tbody,
                      _class="delphi_wide")
  
      # Add Custom CSS from Static (cacheable)
      s3.stylesheets.append("S3/delphi.css")
  
      return dict(rheader=rheader,
                  num_voted=num_voted,
                  chart=image,
                  summary=summary,
                  grids=grids
                  )
  
  # =============================================================================
  # Discussions
  # =============================================================================
  def discuss(r, **attr):
      """ Custom Method to manage the discussion of a Problem or Solution """
  
      if r.component:
          resourcename = "solution"
          id = r.component_id
      else:
          resourcename = "problem"
          id = r.id
  
      # Add the RHeader to maintain consistency with the other pages
      rheader = problem_rheader(r)
  
      ckeditor = URL(c="static", f="ckeditor", args="ckeditor.js")
      s3.scripts.append(ckeditor)
      adapter = URL(c="static", f="ckeditor", args=["adapters",
                                                    "jquery.js"])
      s3.scripts.append(adapter)
  
      # Toolbar options: http://docs.cksource.com/CKEditor_3.x/Developers_Guide/Toolbar
      js = "".join((
  '''i18n.reply="''', str(T("Reply")), '''"
  var img_path=S3.Ap.concat('/static/img/jCollapsible/')
  var ck_config={toolbar:[['Bold','Italic','-','NumberedList','BulletedList','-','Link','Unlink','-','Smiley','-','Source','Maximize']],toolbarCanCollapse:false,removePlugins:'elementspath'}
  function comment_reply(id){
   $('#delphi_comment_solution_id__row').hide()
   $('#delphi_comment_solution_id__row1').hide()
   $('#comment-title').html(i18n.reply)
   var ed = $('#delphi_comment_body').ckeditorGet()
   ed.destroy()
   $('#delphi_comment_body').ckeditor(ck_config)
   $('#comment-form').insertAfter($('#comment-'+id))
   $('#delphi_comment_parent').val(id)
   var solution_id=$('#comment-'+id).attr('solution_id')
   if(undefined!=solution_id){
    $('#delphi_comment_solution_id').val(solution_id)
   }
  }'''))
  
      s3.js_global.append(js)
  
      response.view = "delphi/discuss.html"
      return dict(rheader=rheader,
                  resourcename=resourcename,
                  id=id)
  
  # -----------------------------------------------------------------------------
  def comment_parse(comment, comments, solution_id=None):
      """
          Parse a Comment
  
          @param: comment - a gluon.sql.Row: the current comment
          @param: comments - a gluon.sql.Rows: full list of comments
          @param: solution_id - a reference ID: optional solution commented on
      """
  
      author = B(T("Anonymous"))
      if comment.created_by:
          utable = s3db.auth_user
          ptable = s3db.pr_person
          ltable = s3db.pr_person_user
          query = (utable.id == comment.created_by)
          left = [ltable.on(ltable.user_id == utable.id),
                  ptable.on(ptable.pe_id == ltable.pe_id)]
          row = db(query).select(utable.email,
                                 ptable.first_name,
                                 ptable.middle_name,
                                 ptable.last_name,
                                 left=left, limitby=(0, 1)).first()
          if row:
              person = row.pr_person
              user = row[utable._tablename]
              username = s3_fullname(person)
              email = user.email.strip().lower()
              import hashlib
              hash = hashlib.md5(email).hexdigest()
              url = "http://www.gravatar.com/%s" % hash
              author = B(A(username, _href=url, _target="top"))
      if not solution_id and comment.solution_id:
          solution = "re: %s" % s3db.delphi_solution_represent(comment.solution_id)
          header = DIV(author, " ", solution)
          solution_id = comment.solution_id
      else:
          header = author
      thread = LI(DIV(s3base.s3_avatar_represent(comment.created_by),
                      DIV(DIV(header,
                              _class="comment-header"),
                          DIV(XML(comment.body)),
                          _class="comment-text"),
                          DIV(DIV(comment.created_on,
                                  _class="comment-date"),
                              DIV(A(T("Reply"),
                                    _class="action-btn"),
                                  _onclick="comment_reply(%i);" % comment.id,
                                  _class="comment-reply"),
                              _class="fright"),
                      _id="comment-%i" % comment.id,
                      _solution_id=solution_id,
                      _class="comment-box"))
  
      # Add the children of this thread
      children = UL(_class="children")
      id = comment.id
      count = 0
      for comment in comments:
          if comment.parent == id:
              count = 1
              child = comment_parse(comment, comments, solution_id=solution_id)
              children.append(child)
      if count == 1:
          thread.append(children)
  
      return thread
  
  # -----------------------------------------------------------------------------
  def comments():
      """ Function accessed by AJAX from discuss() to handle Comments """
  
      try:
          resourcename = request.args[0]
      except:
          raise HTTP(400)
  
      try:
          id = request.args[1]
      except:
          raise HTTP(400)
  
      if resourcename == "problem":
          problem_id = id
          solution_id = None
      elif resourcename == "solution":
          stable = s3db.delphi_solution
          query = (stable.id == id)
          solution = db(query).select(stable.problem_id,
                                      limitby=(0, 1)).first()
          if solution:
              problem_id = solution.problem_id
              solution_id = id
          else:
              raise HTTP(400)
      else:
          raise HTTP(400)
  
      table = s3db.delphi_comment
      field = table.problem_id
      field.default = problem_id
      field.writable = field.readable = False
      sfield = table.solution_id
      if solution_id:
          sfield.default = solution_id
          sfield.writable = sfield.readable = False
      else:
          sfield.label = T("Related to Solution (optional)")
          sfield.requires = IS_EMPTY_OR(
                              IS_ONE_OF(db, "delphi_solution.id",
                                        s3.delphi_solution_represent,
                                        filterby="problem_id",
                                        filter_opts=(problem_id,)
                                        ))
  
      # Form to add a new Comment
      from gluon.tools import Crud
      form = Crud(db).create(table, formname="delphi_%s/%s" % (resourcename, id))
  
      # List of existing Comments
      if solution_id:
          comments = db(sfield == solution_id).select(table.id,
                                                      table.parent,
                                                      table.body,
                                                      table.created_by,
                                                      table.created_on)
      else:
          comments = db(field == problem_id).select(table.id,
                                                    table.parent,
                                                    table.solution_id,
                                                    table.body,
                                                    table.created_by,
                                                    table.created_on)
  
      output = UL(_id="comments")
      for comment in comments:
          if not comment.parent:
              # Show top-level threads at top-level
              thread = comment_parse(comment, comments, solution_id=solution_id)
              output.append(thread)
  
      # Also see the outer discuss()
      script = \
  '''$('#comments').collapsible({xoffset:'-5',yoffset:'50',imagehide:img_path+'arrow-down.png',imageshow:img_path+'arrow-right.png',defaulthide:false})
  $('#delphi_comment_parent__row1').hide()
  $('#delphi_comment_parent__row').hide()
  $('#delphi_comment_body').ckeditor(ck_config)
  $('#submit_record__row input').click(function(){$('#comment-form').hide();$('#delphi_comment_body').ckeditorGet().destroy();return true;})'''
  
      # No layout in this output!
      #s3.jquery_ready.append(script)
  
      output = DIV(output,
                   DIV(H4(T("New Post"),
                          _id="comment-title"),
                       form,
                       _id="comment-form",
                       _class="clear"),
                   SCRIPT(script))
  
      return XML(output)
  
  # END =========================================================================
  