Browse Source

Resolved merge conflicts with master

SkylarHew 2 years ago
parent
commit
7cb200ab04

+ 14 - 13
README.md

@@ -1,23 +1,25 @@
 # Modpol for Minetest
 
-Modpol, short for "modular politics," is an extension that enables diverse governance processes on multi-user platforms. It offers a library that enables users to create or adapt their own modules that add specific governance functionalities. 
+Modpol, short for "modular politics," enables diverse governance processes on multi-user platforms. It offers a library with which users can choose, modify, and create modules that add specific governance functionalities.
 
-This implementation is a mod for [Minetest](https://minetest.net), a free/open-source voxel game. It is designed to be easily adapted to other multi-user platforms that also employ Lua as an extension language.
+This implementation is a mod for [Minetest](https://minetest.net), a free/open-source voxel game. It is designed to be adapted to other multi-user platforms that also employ Lua as an extension language.
 
 ## How to use it
 
-Modpol is built around groups called *orgs*. At the base is an org with all users in it, called `Root` by default.
+Modpol is built around groups called *orgs*. At the base is an org with all users in it, called `Root` by default. *Modules* enable people to do things within orgs, such as decide on membership, grant powers to the org, and much more. To get started in Minetest:
 
-*Modules* enable people to do things within orgs, such as decide on membership, grant powers to the org, and much more. Modules can be added and modified by users to meet their needs. Modules can also be nested in each other, so one module can rely on another module to accomplish a process. Within an org, choose the module that you want to use:
+* Type the command `/mp`
+* Select the org `Root`
+* Choose one of its modules to make new orgs and craft their behavior
 
 ![](lib/module_list.png)
 
-Modules might simply carry out actions in the game, or they might require a group decision to do so. They might also change the modules available to users of a given org. There are currently two ways of doing this:
+Modules can be nested in each other, so one module can rely on another module to accomplish a process. Users might use a module to unilaterally carry out actions in the game, or the module might require a group decision to do so. Users can also change the modules available to users of a given org. There are currently two ways of doing this:
 
-* Remove modules from the list of modules loaded in `modpol_core/api.lua` and `modpol_minetest/api.lua`. This will make those modules no longer available to any user.
-* Remove modules for a given org from within the program using the `Change modules` module. The removed modules can be re-added in any org by using `Change modules` again.
+* Admins can remove modules from the list of modules loaded in `modpol_core/api.lua` and `modpol_minetest/api.lua`. This will make those modules no longer available to any user.
+* Players can change the modules available in a given org from within the program using the `Change modules` module. Removed modules can be re-added in any org by using `Change modules` again.
 
-The point is that Modpol should give you the ability to do whatever kind of politics you want with your modules. If there is something you would like to do that is not available, develop a module for it (or ask us for help!).
+Modpol should give you the ability to do whatever kind of politics you want with your modules. If there is something you would like to do that is not available, [develop a module for it](https://gitlab.com/medlabboulder/modpol/-/wikis/Module-Writing-Guide) (or ask us for help!).
 
 
 ## Installation in Minetest
@@ -39,24 +41,23 @@ The command-line version is in the `modpol` subdirectory. To run the program on
 $ lua[jit] login.lua
 ```
 
-You can also interact with the interpreter by starting it this way:
+Alternatively, to test arbitrary functions in the interpreter outside of the interactive dashboards, load Modpol's library with:
 
 ```
 $ lua[jit]
-> dofile("login.lua")
+> dofile("modpol_core/modpol.lua")
 ```
 
 In the interpreter, for a list of global functions and tables, use `modpol.menu()`.
 
 ## Storage
 
-By default, a  data  directory  named  "data" will  be created in  this directory. `/data` will contain a log file and serialized program data files.
+The persistent storage method may be chosen in `modpol.lua`. If available, Modpol uses Minetest's built-in StorageRef system for Minetest 5.*. If that is not available, or in CLI mode, data will be stored in a data directory at `modpol_core/data/`. This will contain a log file and serialized program data files.
 
-Another storage method may be chosen in `modpol.lua`. A StorageRef-based method for Minetest 5.* is included: `storage-mod_storage.lua`.
 
 ## Design philosophy
 
-Modpol seeks to implement a theoretical framework, also called "[modular politics](https://metagov.org/modpol)," which proposes these design goals:
+Modpol seeks to implement a theoretical framework called "[modular politics](https://metagov.org/modpol)," which proposes these design goals:
 
 * *Modularity*: Platform operators and community members should have the ability to construct systems by creating, importing, and arranging composable parts together as a coherent whole.
 * *Expressiveness*: The governance layer should be able to implement as wide a range of processes as possible.

+ 0 - 2
login.lua

@@ -1,7 +1,5 @@
 dofile("modpol_core/modpol.lua")
 
-modpol.orgs.reset()
-
 print("Log in as which user?")
 local username = io.read()
 

+ 1 - 1
mod.conf

@@ -1,7 +1,7 @@
 name = modpol
 title = Modpol
 author = ntnsndr <n@nathanschneider.info>
-description = Framework that enables diverse governance processes
+description = A framework that enables diverse governance processes
 license = MIT
 forum = https://forum.minetest.net/viewtopic.php?f=9&t=27598
 version = 0.1

+ 8 - 0
modpol_core/api.lua

@@ -5,6 +5,7 @@ local localdir = modpol.topdir
 --orgs
 dofile (localdir .. "/orgs/base.lua")
 dofile (localdir .. "/orgs/process.lua")
+dofile (localdir .. "/orgs/users.lua")
 
 --interactions
 dofile (localdir .. "/interactions/interactions.lua")
@@ -14,11 +15,18 @@ dofile (localdir .. "/interactions/interactions.lua")
 dofile (localdir .. "/modules/add_child_org_consent.lua")
 dofile (localdir .. "/modules/change_modules.lua")
 dofile (localdir .. "/modules/consent.lua")
+dofile (localdir .. "/modules/create_token.lua")
+dofile (localdir .. "/modules/defer_consent.lua")
+dofile (localdir .. "/modules/display_processes.lua")
 dofile (localdir .. "/modules/join_org_consent.lua")
 dofile (localdir .. "/modules/leave_org.lua")
 dofile (localdir .. "/modules/message_org.lua")
+dofile (localdir .. "/modules/randomizer.lua")
 dofile (localdir .. "/modules/remove_child_consent.lua")
 dofile (localdir .. "/modules/remove_member_consent.lua")
 dofile (localdir .. "/modules/remove_org_consent.lua")
 dofile (localdir .. "/modules/remove_org.lua")
+dofile (localdir .. "/modules/remove_process.lua")
 dofile (localdir .. "/modules/rename_org_consent.lua")
+dofile (localdir .. "/modules/send_token.lua")
+dofile (localdir .. "/modules/tokenomics.lua")

+ 211 - 36
modpol_core/interactions/interactions.lua

@@ -38,8 +38,10 @@ function modpol.interactions.dashboard(user)
    local user_pending_count = 0
    for k,v in ipairs(modpol.orgs.array) do
       if v.pending and v.pending[user] then
-         table.insert(user_pending, v.name)
-         user_pending_count = user_pending_count + 1
+         if modpol.util.num_pairs(v.pending[user]) ~= 0 then
+            table.insert(user_pending, v.name)
+            user_pending_count = user_pending_count + 1
+         end
       end
    end
    
@@ -48,18 +50,53 @@ function modpol.interactions.dashboard(user)
    print('All users: ' .. table.concat(all_users, ', '))
    print()
 
-   print('Access which org id?')
+   print("Commands: (O)rg, (U)ser, (R)eset, (Q)uit")
+
    local sel = io.read()
-   print()
 
-   if modpol.orgs.array[tonumber(sel)] then
-      local sel_org = modpol.orgs.array[tonumber(sel)].name
-      modpol.interactions.org_dashboard(user, sel_org)
-   else 
-      print("Org id not found.")
-   end   
-end
+   if sel == "O" or sel == "o" then
+      print('Access which org id?')
+      sel = io.read()
+      print()
+      if modpol.orgs.array[tonumber(sel)] then
+         local sel_org = modpol.orgs.array[tonumber(sel)].name
+         modpol.interactions.org_dashboard(user, sel_org)
+      else
+         print("Org id not found")
+         modpol.interactions.dashboard(user)
+      end
+
+   elseif sel == "U" or sel == "u" then
+      print("Access which user?")
+      sel = io.read()
+      print()
+      if modpol.instance:has_member(sel) then
+         modpol.interactions.user_dashboard(
+            user, sel,
+            function()
+               modpol.interactions.dashboard(user)
+            end
+         )         
+      else
+         print("User name not found")
+         modpol.interactions.dashboard(user)
+      end
+
+   elseif sel == "R" or sel == "r" then
+      modpol.instance.members = {}
+      modpol.orgs.reset()
+      print("Orgs and users reset")
+      modpol.interactions.dashboard(user)
+
+   elseif sel == "Q" or "q" then
+      return
 
+   else
+      print("Invalid input, try again")
+      modpol.interactions.dashboard(user)
+   end
+end
+   
 --- Output: Displays a menu of org-specific commands to the user
 -- @function modpol.interactions.org_dashboard
 -- @param user (string)
@@ -109,7 +146,7 @@ function modpol.interactions.org_dashboard(user, org_string)
    print("Org: " .. org.name)
    print("Parent: " .. parent)
    print("Members: " .. table.concat(org.members, ", "))
-   print("Children: " .. table.concat(children, ", "))
+   print("Child orgs: " .. table.concat(children, ", "))
    print("Modules: " .. table.concat(modules, ", "))
    print("Pending: " .. process_msg)
    print()
@@ -132,25 +169,33 @@ function modpol.interactions.org_dashboard(user, org_string)
          org:call_module(module_sel, user)
       else
          print("Error: Module not found.")
+         modpol.interactions.org_dashboard(user, org.id)
       end
    
    elseif sel == 'p' or sel == 'P' then
       local processes = {}
       print("All processes: (* indicates pending)")
       for i,v in ipairs(org.processes) do
-         local active = ''
-         if org.pending[user] then
-            if org.pending[user][v.id] then
-               active = '*'
+         if v ~= "deleted" then
+            local active = ''
+            if org.pending[user] then
+               if org.pending[user][v.id] then
+                  active = '*'
+               end
             end
+            print("["..v.id.."] "..v.slug..active)
          end
-         print("["..v.id.."] "..v.slug..active)
       end
       print()
       print("Interact with which one (use [id] number)?")
       local to_interact = io.read()
       local process = org.processes[tonumber(to_interact)]
-      if not process then return end
+      if not process then
+         modpol.interactions.message(
+            user, "Not a pending process")
+         modpol.interactions.org_dashboard(user, org.id)
+         return
+      end
       if org:has_pending_actions(user) then
          if org.pending[user][process.id] then
             org:interact(process.id, user)
@@ -164,13 +209,46 @@ function modpol.interactions.org_dashboard(user, org_string)
    end
 end
 
--- Function: modpol.interactions.policy_dashboard
--- input: user (string), org_id (int), policy (string)
--- if policy is nil, enables creating a new policy
--- output: opens a dashboard for viewing/editing policy details
--- TODO
+--- Function: modpol.interactions.user_dashboard
+-- Displays a dashboard about a particular user
+-- @param viewer Name of user viewing the dashboard (string)
+-- @param user Name of user being viewed (string)
+-- @param completion Optional function to call on Done button
+function modpol.interactions.user_dashboard(viewer, user, completion)
+   local user_orgs = {}
+   local user_modules = {}
+
+   print("\n-=< USER DASHBOARD: "..user.." >=-")
+   print("User's orgs:")
+   for id, org in ipairs(modpol.orgs.array) do
+      if type(org) == "table" then
+         if org:has_member(user) then 
+         print(org.name)
+         end
+      end
+   end
+
+   print()
+   print("Commands: (M)essage user, Enter when done")
+   local sel = io.read()
 
---- Output: Prints message to CLI
+   if sel == "M" or sel == "m" then
+      modpol.interactions.message_user(
+         viewer, user)
+      completion()
+   else
+      completion()
+   end
+end
+
+
+
+
+-- INTERACTION PRIMITIVES
+-- ======================
+
+--- Prints message to CLI.
+-- Buttons: message, done
 -- @function modpol.interactions.message
 -- @param user (string)
 -- @param message (string)
@@ -178,11 +256,56 @@ function modpol.interactions.message(user, message)
    print(user .. ": " .. message)
 end
 
---- Output: Applies "func" to user input.
+--- Function: modpol.interactions.message_user
+-- Gets and sends a message from one user to another
+-- @param sender Name of user sending (string)
+-- @param recipient Name of user receiving (string)
+function modpol.interactions.message_user(sender, recipient)
+   print("Enter your message for "..recipient..":")
+   local sel = io.read()
+   modpol.interactions.message(
+      recipient,
+      sel.." [from "..sender.."]")
+end
+
+--- Function: modpol.interactions.display
+-- Displays complex data to a user
+-- @param user Name of target user (string)
+-- @param title Title of display (string)
+-- @param message Content of message (string or table of strings)
+-- @param done Optional function for what happens when user is done
+function modpol.interactions.display(user, title, message, completion)
+   local output = ""
+   output = "\n-=< "..title.." >=-\n\n"
+   if type(message) == "table" then
+      output = table.concat(message,"\n")
+   elseif type(message) == "string" then
+      output = message
+   elseif type(message) == "number" then
+      output = message
+   else
+      modpol.interactions.message(
+         self.initiator, "Error: message not typed for display")
+      modpol.interactions.message(
+         self.initiator, "Error: input not typed for display")
+      if completion then completion() else
+         modpol.intereactions.dashboard(user)
+      end
+   end
+   print(message)
+   print("\nEnter to continue")
+   io.read()
+   if completion then completion() else
+      modpol.intereactions.dashboard(user)
+   end
+end
+
+
+-- Applies "func" to user input
 -- Func input: user input (string)
 -- @function modpol.interactions.text_query
--- @param user (string)
--- @param query (string)
+-- @param User (string)
+-- @param Query (string)
 -- @param func (function)
 function modpol.interactions.text_query(user, query, func)
    print(user .. ": " .. query)
@@ -197,8 +320,6 @@ end
 -- @param label (string)
 -- @param options (table of strings)
 -- @param func (choice) (function)
-
-
 function modpol.interactions.dropdown_query(user, label, options, func)
    -- set up options
    local options_display = ""
@@ -211,7 +332,7 @@ function modpol.interactions.dropdown_query(user, label, options, func)
    options_display = options_display .. "Select number:"
    if options_number == 0 then
       print("Error: No options given for dropdown")
-      return nil      
+      return nil
    end
    -- begin displaying
    print(user .. ": " .. label)
@@ -234,12 +355,60 @@ function modpol.interactions.dropdown_query(user, label, options, func)
    end
 end
 
---- Output: Applies "func" to user input.
--- Func input: user input (string: y/n)
--- @function modpol.binary_poll_user(user, question)
--- @param user (string)
--- @param question (string)
--- @param func (function)
+
+--- Allows user to select from a set of options
+-- @function modpol.interactions.checkbox_query
+-- @param user Name of user (string)
+-- @param label Query for user before options (string)
+-- @param options Table of options and their checked status in the form {{"option_1_string", true}, {"option_2_string", false}}
+-- @param func Function to be called with param "input", made up of the corrected table in the same format as the param options
+function modpol.interactions.checkbox_query(
+      user, label, options, func)
+   -- set up options
+   local options_display = ""
+   local options_number = 0
+   for i,v in ipairs(options) do
+      local checked = false
+      if v[2] then checked = true end
+      if checked then
+         checked = "x"
+      else
+         checked = " "
+      end
+      options_display = options_display..i..". ["..
+         checked.."] "..v[1].."\n"
+      options_number = options_number + 1
+   end
+   if options_number == 0 then
+      print("Error: No options given for dropdown")
+      return nil
+   end
+   options_display = options_display..
+      "List comma-separated options to flip (e.g., 1,2,5):"
+   -- begin displaying
+   print(user .. ": " .. label)
+   print(options_display)
+   -- read input and produce output
+   local answer = io.read()
+   local answer_table = {}
+   for match in (answer..","):gmatch("(.-)"..",") do
+      table.insert(answer_table, tonumber(match))
+   end
+   local result_table = modpol.util.copy_table(options)
+   for i,v in ipairs(answer_table) do
+      if result_table[v] then
+         -- flip the boolean on selected options
+         result_table[v][2] = not result_table[v][2]
+      end
+   end
+   func(result_table)
+end
+
+
+-- Function: modpol.interactions.binary_poll_user
+-- Params: user (string), question (string), func (function)
+--   func input: user input (string: y/n)
+-- Output: Applies "func" to user input
 function modpol.interactions.binary_poll_user(user, question, func)
    local query = "Poll for " .. user .. " (y/n): ".. question
    local answer
@@ -280,3 +449,9 @@ end
 -- output: gets question from initiator, asks all org members, broadcasts answers
 
 
+-- TESTING
+
+--testing command
+function modpol.msg(text)
+   modpol.interactions.message("TEST MSG",text)
+end

+ 3 - 3
modpol_core/modules/add_child_org_consent.lua

@@ -28,7 +28,6 @@ function add_child_org_consent:initiate(result)
             modpol.interactions.org_dashboard(
                self.initiator, self.org.name)
             self.org:delete_process(self.id)            
-            if result then result() end
             return
          elseif modpol.orgs.get_org(input) then
             modpol.interactions.message(
@@ -45,7 +44,7 @@ function add_child_org_consent:initiate(result)
                self.initiator,
                "Proposed child org: " .. input)
          -- initiate consent process
-         self.org:call_module(
+         self:call_module(
             "consent",
             self.initiator, 
             {
@@ -70,7 +69,8 @@ function add_child_org_consent:create_child_org()
    modpol.interactions.message_org(
       self.initiator,
       self.org.name,
-      "Child org created: "..self.data.child_name)
+      "Consent reached: created child org "
+      ..self.data.child_name)
    if self.data.result then self.data.result() end
    self.org:delete_process(self.id)
 end

+ 169 - 0
modpol_core/modules/change_modules-dropdown.lua

@@ -0,0 +1,169 @@
+--- change_modules
+-- @module change_modules
+-- Depends on consent
+
+local change_modules = {
+    name = "Change modules (consent)",
+    slug = "change_modules",
+    desc = "Add or remove modules from the org with member consent",
+    hide = false;
+}
+
+change_modules.data = {
+   result = nil
+}
+
+change_modules.config = {
+}
+
+function change_modules:initiate(result)
+   self.data.result = result
+   -- Step 1: add or remove?
+   modpol.interactions.dropdown_query(
+      self.initiator, "Module change options:",
+      {"Add module","Remove module"},
+      function(input)
+         if input == "Add module" then
+            self:add_module()
+         elseif input == "Remove module" then
+            self:remove_module()
+         end
+      end
+   )
+end
+
+function change_modules:add_module()
+   -- prepare module options
+   local available_modules = modpol.util.copy_table(modpol.modules)
+   for k,org_mod in pairs(self.org.modules) do
+      if available_modules[org_mod.slug] then
+            available_modules[org_mod.slug] = nil
+   end end
+   -- present module options
+   local modules_list = {}
+   for k,v in pairs(available_modules) do
+      table.insert(modules_list,v.name)
+   end
+   if #modules_list == 0 then
+      modpol.interactions.message(
+         self.initiator, "Org has all modules")
+      modpol.interactions.org_dashboard(
+         self.initiator, self.org.id)
+      if self.data.result then self.data.result() end
+      self.org:delete_process(self.id)
+      return
+   end
+   table.sort(modules_list)
+   -- now ask which to add
+   modpol.interactions.dropdown_query(
+      self.initiator, "Choose a module to add:",
+      modules_list,
+      function(mod_choice)
+         -- confirm choice
+         modpol.interactions.binary_poll_user(
+            self.initiator,
+            "Confirm: propose to add module \"" ..
+            mod_choice .. "\"?",
+            function(input)
+               if input == "Yes" then
+                  self:propose_change("add",mod_choice)
+                  modpol.interactions.org_dashboard(
+                     self.initiator, self.org.id)
+               else
+                  self:add_module()
+               end
+            end
+         )
+      end
+   )
+end
+
+function change_modules:remove_module()
+   -- prepare module options
+   local available_modules = {}
+   for k,org_mod in pairs(self.org.modules) do
+      if not org_mod.hide then
+         available_modules[org_mod.slug] = modpol.util.copy_table(org_mod)
+   end end
+   local modules_list = {}
+   local modules_count = 0
+   for k,v in pairs(available_modules) do
+      table.insert(modules_list,v.name)
+      modules_count = modules_count + 1
+   end
+   -- abort if no modules to remove
+   if modules_count == 0 then
+      modpol.interactions.message(
+         self.initiator, "Org has no modules")
+      modpol.interactions.org_dashboard(
+         self.initiator, self.org.id)
+      if self.data.result then self.data.result() end
+      self.org:delete_process(self.id)
+      return
+   end
+   table.sort(modules_list)
+   -- now ask which to remove
+   modpol.interactions.dropdown_query(
+      self.initiator, "Choose a module to remove:",
+      modules_list,
+      function(mod_choice)
+         -- confirm choice
+         modpol.interactions.binary_poll_user(
+            self.initiator,
+            "Confirm: propose to remove module \"" .. mod_choice .. "\"?",
+            function(input)
+               if input == "Yes" then
+                  self:propose_change("remove",mod_choice)
+                  modpol.interactions.org_dashboard(
+                     self.initiator, self.org.id)
+               else
+                  self:remove_module()
+               end
+            end
+         )
+      end
+   )
+end
+
+--- propose_change
+-- @field type "add" or "remove"
+function change_modules:propose_change(type, mod_text)
+   self:call_module(
+      "consent",self.initiator,
+      {
+         prompt = "Do you consent to "..type..
+            " this module in org "..self.org.name..
+            ":\n"..mod_text,
+         votes_required = #self.org.members
+      },
+      function()
+         if type == "add" then
+            for k,v in pairs(modpol.modules) do
+               if v.name == mod_text then
+                  table.insert(self.org.modules,v)
+               end
+            end
+            modpol.interactions.message_org(
+               self.initiator,self.org.id,
+               "Consent reached:\nAdding \""
+               ..mod_text.."\" to org "..self.org.name)
+         elseif type == "remove" then
+            local i = 0
+            for k,v in pairs(self.org.modules) do
+               i = i + 1
+               if v.name == mod_text then
+                  self.org.modules[k] = nil
+               end                  
+            end
+            modpol.interactions.message_org(
+               self.initiator,self.org.id,
+               "Consent reached:\nRemoving \""
+               ..mod_text.."\" from org "..self.org.name)
+         end
+   end)
+   if self.data.result then self.data.result() end
+   self.org:delete_process(self.id)
+end
+   
+--- (Required) Add to module table
+modpol.modules.change_modules = change_modules

+ 81 - 136
modpol_core/modules/change_modules.lua

@@ -10,7 +10,10 @@ local change_modules = {
 }
 
 change_modules.data = {
-   result = nil
+   result = nil,
+   modules_before = {},
+   modules_after = {},
+   summary = "",
 }
 
 change_modules.config = {
@@ -21,151 +24,93 @@ change_modules.config = {
 -- @function change_modules:initiate
 function change_modules:initiate(result)
    self.data.result = result
-   -- Step 1: add or remove?
-   modpol.interactions.dropdown_query(
-      self.initiator, "Module change options:",
-      {"Add module","Remove module"},
-      function(input)
-         if input == "Add module" then
-            self:add_module()
-         elseif input == "Remove module" then
-            self:remove_module()
+   self.data.add_modules = {}
+   self.data.remove_modules = {}
+   local modules_before = {}
+   local modules_after = {}
+   -- generate self.config.modules table
+   for k, module in pairs(modpol.modules) do
+      if not modpol.modules[module.slug].hide then
+         local in_org = false
+         if self.org.modules[module.slug] then
+            in_org = true
          end
+         table.insert(
+            modules_before,
+            {module.name.." ["..module.slug.."]", in_org})
       end
-   )
-end
-
-function change_modules:add_module()
-   -- prepare module options
-   local available_modules = modpol.util.copy_table(modpol.modules)
-   for k,org_mod in pairs(self.org.modules) do
-      if available_modules[org_mod.slug] then
-            available_modules[org_mod.slug] = nil
-   end end
-   -- present module options
-   local modules_list = {}
-   for k,v in pairs(available_modules) do
-      table.insert(modules_list,v.name)
    end
-   if #modules_list == 0 then
-      modpol.interactions.message(
-         self.initiator, "Org has all modules")
-      modpol.interactions.org_dashboard(
-         self.initiator, self.org.id)
-      if self.data.result then self.data.result() end
-      self.org:delete_process(self.id)
-      return
-   end
-   table.sort(modules_list)
-   -- now ask which to add
-   modpol.interactions.dropdown_query(
-      self.initiator, "Choose a module to add:",
-      modules_list,
-      function(mod_choice)
-         -- confirm choice
-         modpol.interactions.binary_poll_user(
-            self.initiator,
-            "Confirm: propose to add module \"" ..
-            mod_choice .. "\"?",
-            function(input)
-               if input == "Yes" then
-                  self:propose_change("add",mod_choice)
-                  modpol.interactions.org_dashboard(
-                     self.initiator, self.org.id)
+   -- send query to user
+   modpol.interactions.checkbox_query(
+      self.initiator,
+      "Check the modules to activate in this org:",
+      modules_before,
+      function(input)
+         -- identify changes
+         modules_after = input
+         for i,v in ipairs(modules_after) do
+            if v[2] ~= modules_before[i][2] then
+               if v[2] then
+                  table.insert(self.data.add_modules, v[1])
                else
-                  self:add_module()
+                  table.insert(self.data.remove_modules, v[1])
                end
             end
-         )
-      end
-   )
+         end
+         -- abort if no changes
+         if #self.data.add_modules == 0
+            and #self.data.remove_modules == 0 then
+            modpol.interactions.message(
+               self.initiator, "No module changes proposed")
+            modpol.interactions.org_dashboard(
+               self.initiator, self.org.id)
+            self.org:delete_process(self.id)
+            return
+         end
+         -- proceed with consent
+         local query = "Accept module changes in org "..
+            self.org.name.."?"
+         self.data.summary = ""
+         if #self.data.add_modules > 0 then
+            self.data.summary = self.data.summary.."\nAdd: "..
+               table.concat(self.data.add_modules,", ")
+         elseif #self.data.remove_modules > 0 then
+            self.data.summary = "\nRemove: "..
+               table.concat(self.data.remove_modules,", ")
+         end
+         self:call_module(
+            "consent",
+            self.initiator,
+            {
+               prompt = query..self.data.summary,
+               votes_required = #self.org.members
+            },
+            function()
+               self:implement_change()
+         end)
+         modpol.interactions.org_dashboard(
+            self.initiator, self.org.id)
+   end)
 end
 
-function change_modules:remove_module()
-   -- prepare module options
-   local available_modules = {}
-   for k,org_mod in pairs(self.org.modules) do
-      if not org_mod.hide then
-         available_modules[org_mod.slug] = modpol.copy_table(org_mod)
-   end end
-   local modules_list = {}
-   local modules_count = 0
-   for k,v in pairs(available_modules) do
-      table.insert(modules_list,v.name)
-      modules_count = modules_count + 1
+function change_modules:implement_change()
+   for i,v in ipairs(self.data.add_modules) do
+      local slug = string.match(v,"%[(.+)%]")
+      self.org.modules[slug] =
+         modpol.util.copy_table(modpol.modules[slug])
+      table.sort(self.org.modules)
    end
-   -- abort if no modules to remove
-   if modules_count == 0 then
-      modpol.interactions.message(
-         self.initiator, "Org has no modules")
-      modpol.interactions.org_dashboard(
-         self.initiator, self.org.id)
-      if self.data.result then self.data.result() end
-      self.org:delete_process(self.id)
-      return
+   for i,v in ipairs(self.data.remove_modules) do
+      local slug = string.match(v,"%[(.+)%]")
+      self.org.modules[slug] = nil
+      table.sort(self.org.modules)
    end
-   table.sort(modules_list)
-   -- now ask which to remove
-   modpol.interactions.dropdown_query(
-      self.initiator, "Choose a module to remove:",
-      modules_list,
-      function(mod_choice)
-         -- confirm choice
-         modpol.interactions.binary_poll_user(
-            self.initiator,
-            "Confirm: propose to remove module \"" .. mod_choice .. "\"?",
-            function(input)
-               if input == "Yes" then
-                  self:propose_change("remove",mod_choice)
-                  modpol.interactions.org_dashboard(
-                     self.initiator, self.org.id)
-               else
-                  self:remove_module()
-               end
-            end
-         )
-      end
-   )
-end
-
---- propose_change
--- @field type "add" or "remove"
-function change_modules:propose_change(type, mod_text)
-   self.org:call_module(
-      "consent",self.initiator,
-      {
-         prompt = "Do you consent to "..type..
-            " this module in org "..self.org.name..
-            ":\n"..mod_text,
-         votes_required = #self.org.members
-      },
-      function()
-         if type == "add" then
-            for k,v in pairs(modpol.modules) do
-               if v.name == mod_text then
-                  table.insert(self.org.modules,v)
-               end
-            end
-            modpol.interactions.message_org(
-               self.initiator,self.org.id,
-               "Consent reached:\nAdding \""
-               ..mod_text.."\" to org "..self.org.name)
-         elseif type == "remove" then
-            modpol.msg("Removing!")
-            local i = 0
-            for k,v in pairs(self.org.modules) do
-               i = i + 1
-               if v.name == mod_text then
-                  modpol.msg("got it!")
-                  self.org.modules[k] = nil
-               end                  
-            end
-            modpol.interactions.message_org(
-               self.initiator,self.org.id,
-               "Consent reached:\nRemoving \""
-               ..mod_text.."\" from org "..self.org.name)
-         end
-   end)
+   -- announce and shut down
+   modpol.interactions.message_org(
+      self.initiator,
+      self.org.id,
+      "Module changes applied to org "..self.org.name..":"..
+      self.data.summary)
    if self.data.result then self.data.result() end
    self.org:delete_process(self.id)
 end

+ 10 - 5
modpol_core/modules/consent.lua

@@ -2,9 +2,9 @@
 -- @module consent
 
 local consent = {
-    name = "Consent process utility",
+    name = "Consent process",
     slug = "consent",
-    desc = "A module other modules use for consent decisions",
+    desc = "A utility module other modules use for consent decisions",
     hide = true
 }
 
@@ -26,7 +26,7 @@ function consent:initiate(result)
    if self.org:get_member_count() == 0 then
       if self.data.result then
          self.data.result() end
-      self.org:wipe_pending_actions(self.id)
+      self.org:delete_process(self.id)
    else
       -- otherwise, create poll
       for id, member in pairs(self.org.members) do
@@ -47,14 +47,19 @@ function consent:callback(member)
            if resp == "Yes" then
               self.data.votes = self.data.votes + 1
            end
+           modpol.interactions.message_org(
+              "consent", self.org.id,
+              member.." decided "..resp.." on: "..
+              self.config.prompt.." ("..self.data.votes..
+              "/"..self.config.votes_required..")"
+           )
            if self.data.votes >= self.config.votes_required then
               if self.data.result then
                  self.data.result() end
-              self.org:wipe_pending_actions(self.id)
               self.org:delete_process(self.id)
            end
            modpol.interactions.org_dashboard(
-              member, self.org.name)
+              member, self.org.id)
         end
     )
 end

+ 53 - 0
modpol_core/modules/create_token.lua

@@ -0,0 +1,53 @@
+--- create_token
+-- @module create_token
+-- depends on tokenomics
+
+local create_token = {
+    name = "Create a token (consent)",
+    slug = "create_token",
+    desc = "With org consent, creates an org token",
+    hide = false;
+}
+
+--- (Required) Data for module
+-- Variables that module uses during the course of a process
+-- Can be blank
+create_token.data = {
+}
+
+create_token.config = {
+   token_name = ""
+}
+
+--- (Required): initiate function
+-- @param result (optional) Callback if this module is embedded in other modules
+-- @function initiate
+function create_token:initiate(result)
+   modpol.interactions.text_query(
+      self.initiator,
+      "Token name (alpha-numeric, no spaces):",
+      function(input)
+         self.config.token_name = input
+         self:call_module(
+            "tokenomics",
+            self.initiator,
+            {
+               consent = true,
+               token_slug = self.config.token_name
+            },
+            function(input2)
+               modpol.interactions.org_dashboard(
+                  self.initiator, self.org.name)
+               if result then result() end
+               -- call this wherever process might end:
+               self.org:delete_process(self.id)
+            end
+         )
+         modpol.interactions.org_dashboard(
+            self.initiator, self.org.name)
+      end
+   )
+end
+
+--- (Required) Add to module table
+modpol.modules.create_token = create_token

+ 57 - 0
modpol_core/modules/defer_consent.lua

@@ -0,0 +1,57 @@
+--- defer_consent
+-- @module defer_consent
+
+--- (Required): data table containing name and description of the module
+-- @field name "Human-readable name (parens OK, no brackets)"
+-- @field slug "Same as module class name"
+-- @field desc "Description of the module"
+-- @field hide "Whether this is a hidden utility module"
+local defer_consent = {
+    name = "Defer consent",
+    slug = "defer_consent",
+    desc = "Defers consent on a decision to another org",
+    hide = true;
+}
+
+--- (Required) Data for module
+-- Variables that module uses during the course of a process
+-- Can be blank
+defer_consent.data = {
+}
+
+--- (Required): config for module 
+-- @field defer_org Name or ID of target org
+-- @field votes_required Threshold passed on to `consent`
+-- @field prompt String passed on to `consent`
+defer_consent.config = {
+   defer_org = "Root",
+    votes_required = 1,
+    prompt = "Do you consent?"
+}
+
+--- (Required): initiate function
+-- @param result (optional) Callback if this module is embedded in other modules
+-- @function initiate
+function defer_consent:initiate(result)
+   local defer_org = modpol.orgs.get_org(self.config.defer_org)
+   if not defer_org then
+      modpol.interactions.message(
+         self.initiator, "Target org not found, aborting")
+      self.org:delete_process(self.id)
+   else
+      defer_org:call_module(
+         "consent", self.initiator,
+         {
+            votes_required = self.config.votes_required,
+            prompt = self.config.prompt
+         },
+         function()
+            if result then result() end
+      end)
+   end
+   if result then result() end
+   self.org:delete_process(self.id)
+end
+
+--- (Required) Add to module table
+modpol.modules.defer_consent = defer_consent

+ 70 - 0
modpol_core/modules/display_processes.lua

@@ -0,0 +1,70 @@
+--- display_processes
+-- @module display_processes
+
+local display_processes = {
+    name = "Display processes",
+    slug = "display_processes",
+    desc = "Presents a detailed list of org processes",
+    hide = false;
+}
+
+--- (Required) Data for module
+-- Variables that module uses during the course of a process
+-- Can be blank
+display_processes.data = {
+}
+
+display_processes.config = {
+}
+
+--- (Required): initiate function
+-- @param result (optional) Callback if this module is embedded in other modules
+-- @function initiate
+function display_processes:initiate(result)
+   local display_table = {}
+   for k,v in pairs(self.org.processes) do
+      if v ~= "deleted" then
+         local input = v.id..": "..v.slug
+         table.insert(display_table, input)
+         input = "Org: "..v.org.name..
+            ", initiator: "..v.initiator
+         table.insert(display_table, input)
+         if v.config
+            and modpol.util.num_pairs(v.config) > 0 then
+            table.insert(display_table, "Policies:")
+            for k2,v2 in pairs(v.config) do
+               local v2_string = ""
+               if type(v2) ~= "string"
+                  and type(v2) ~= "table" then
+                  v2_string = tostring(v2)
+               elseif type(v2) == "table" then
+                  v2_string = tostring(v2)
+               else
+                  v2_string = "Could not render"
+               end
+               input = k2..": "..v2_string
+               table.insert(display_table, input)
+            end
+         end
+         table.insert(display_table, "\n")
+      end
+   end
+   local output = table.concat(display_table,"\n")
+   if #display_table == 0 then
+      output = "No processes found"
+   end
+   modpol.interactions.display(
+      self.initiator,
+      "Processes in org "..self.org.name,
+      output,
+      function()
+         modpol.interactions.org_dashboard(
+            self.initiator, self.org.id)
+         if result then result() end
+         self.org:delete_process(self.id)
+      end
+   )
+end
+
+--- (Required) Add to module table
+modpol.modules.display_processes = display_processes

+ 1 - 1
modpol_core/modules/join_org_consent.lua

@@ -28,7 +28,7 @@ function join_org_consent:initiate(result)
       self.org:delete_process(self.id)
    else
       self.data.result = result
-      self.org:call_module(
+      self:call_module(
          "consent", 
          self.initiator, 
         {

+ 49 - 0
modpol_core/modules/randomizer.lua

@@ -0,0 +1,49 @@
+--- @module randomizer
+-- A utility module that outputs a random result from a set of options
+
+local randomizer = {
+    name = "Randomizer",
+    slug = "randomizer",
+    desc = "A utility module other modules use for random decisions",
+    hide = true
+}
+
+randomizer.data = {
+}
+
+-- options_table should be a table of strings
+randomizer.config = {
+    options_table = {},
+    num_results = 1,
+    result_table = {}
+}
+
+function randomizer:initiate(result)
+   self.data.result = result
+   self.data.options_table = modpol.util.copy_table(self.config.options_table)
+   -- if options table is empty, randomizer returns that
+   if #self.data.options_table == 0 or self.config.num_results == 0 then
+      if self.data.result then
+         self.data.result({}) end
+      self.org:delete_process(self.id)
+   else
+      -- otherwise, choose a random result
+      self.random_loop()
+   end
+end
+
+-- returns result_table
+function randomizer:random_loop()
+   self.data.results = 0
+   if results == self.config.num_results then
+      self.data.result(self.data.result_table)
+   else
+      math.randomseed(os.time())
+      local index = math.random(self.data.options_table)
+      table.insert(self.data.result_table, self.data.options_table[index])
+      table.remove(self.data.options_table, index)
+      self.data.results = self.data.results + 1
+   end
+end
+   
+modpol.modules.randomizer = randomizer

+ 1 - 1
modpol_core/modules/remove_child_consent.lua

@@ -42,7 +42,7 @@ function remove_child_consent:initiate(result)
          children,
          function(input)
             self.data.child_to_remove = modpol.orgs.get_org(input)
-            self.org:call_module(
+            self:call_module(
                "consent",
                self.initiator,
                {

+ 1 - 1
modpol_core/modules/remove_member_consent.lua

@@ -35,7 +35,7 @@ function remove_member_consent:initiate(result)
          self.org.members,
          function(input)
             self.data.member_to_remove = input
-            self.org:call_module(
+            self:call_module(
                "consent",
                self.initiator,
                {

+ 2 - 2
modpol_core/modules/remove_org_consent.lua

@@ -28,14 +28,14 @@ function remove_org_consent:initiate(result)
       self.org:delete_process(self.id)
    else
       self.data.result = result
-      self.org:call_module(
+      self:call_module(
         "consent", 
         self.initiator, 
         {
            prompt = "Remove org " .. self.org.name .. "?",
            votes_required = #self.org.members
         },
-        function ()
+        function()
            self:complete()
         end
       )

+ 113 - 0
modpol_core/modules/remove_process.lua

@@ -0,0 +1,113 @@
+--- remove_process
+-- @module remove_process
+
+local remove_process = {
+    name = "Remove process",
+    slug = "remove_process",
+    desc = "User can remove own processes, consent required for those of others",
+    hide = false;
+}
+
+--- (Required) Data for module
+-- Variables that module uses during the course of a process
+-- Can be blank
+remove_process.data = {
+}
+
+remove_process.config = {
+}
+
+--- (Required): initiate function
+-- @param result (optional) Callback if this module is embedded in other modules
+-- @function initiate
+function remove_process:initiate(result)
+   -- prepare process options
+   local available_processes = {}
+   for k,process in pairs(self.org.processes) do
+      if process ~= "deleted" then
+         available_processes[process.id] = process
+      end
+   end
+   local process_list = {}
+   local process_count = 0
+   for k,v in pairs(available_processes) do
+      local mine = ""
+      if v.initiator == self.initiator then mine = "*" end
+      table.insert(process_list,"["..v.id.."] "..v.slug..mine)
+      process_count = process_count + 1
+   end
+   -- abort if no processes to remove
+   if process_count == 0 then
+      modpol.interactions.message(
+         self.initiator, "Org has no modules")
+      modpol.interactions.org_dashboard(
+         self.initiator, self.org.id)
+      if result then result() end
+      self.org:delete_process(self.id)
+      return
+   end
+   table.sort(process_list)
+   -- now ask which to remove
+   modpol.interactions.dropdown_query(
+      self.initiator, "Choose a process to remove (* marks yours, no consent required):",
+      process_list,
+      function(process_choice)
+         -- confirm choice
+         local process_id = tonumber(
+            string.match(process_choice, "%d+"))
+         local process_mine = string.match(process_choice,
+                                           "%*")
+         modpol.interactions.binary_poll_user(
+            self.initiator,
+            "Confirm: Remove process \""..
+            process_choice .. "\"?",
+            function(input)
+               if input == "Yes" then
+                  if process_mine then
+                     self.org:delete_process_tree(process_id)
+                     modpol.interactions.message(
+                        self.initiator,
+                        "Removed process: "..process_choice)
+                     modpol.interactions.org_dashboard(
+                        self.initiator, self.org.id)
+                     if result then result() end
+                     self.org:delete_process(self.id)
+                  else
+                     self:call_module(
+                        "consent",
+                        self.initiator,
+                        {
+                           prompt = "Approve removal of process "..process_choice.."?",
+                           votes_required = #self.org.members
+                        },
+                        function(input)
+                           modpol.interactions.message_org(
+                              self.initiator,
+                              self.org.id,
+                              "Removing process: "..
+                              process_choice)
+                           self.org:delete_process_tree(process_id)
+                           modpol.interactions.org_dashboard(
+                              self.initiator, self.org.id)
+                           if result then result() end
+                           self.org:delete_process(self.id)
+                        end
+                     )
+                  end
+                  modpol.interactions.org_dashboard(
+                     self.initiator, self.org.id)
+               else
+                  modpol.interactions.org_dashboard(
+                     self.initiator, self.org.id)
+                  if result then result() end
+                  self.org:delete_process(self.id)
+               end
+            end
+         )
+      end
+   )
+end
+
+
+--- (Required) Add to module table
+modpol.modules.remove_process = remove_process

+ 1 - 1
modpol_core/modules/rename_org_consent.lua

@@ -49,7 +49,7 @@ function rename_org_consent:initiate(result)
                "Proposed to change name of org " ..
                self.org.name .. " to " .. input)
          -- initiate consent process
-         self.org:call_module(
+         self:call_module(
             "consent",
             self.initiator, 
             {

+ 77 - 0
modpol_core/modules/send_token.lua

@@ -0,0 +1,77 @@
+--- send_token
+-- @module send_token
+-- depends on tokenomics
+
+local send_token = {
+    name = "Send tokens",
+    slug = "send_token",
+    desc = "Send tokens to another user",
+    hide = false;
+}
+
+--- (Required) Data for module
+-- Variables that module uses during the course of a process
+-- Can be blank
+send_token.data = {
+}
+
+send_token.config = {
+   token_name = ""
+}
+
+--- (Required): initiate function
+-- @param result (optional) Callback if this module is embedded in other modules
+-- @function initiate
+function send_token:initiate(result)
+   local token_list = {}
+   if self.org.tokens then
+      for k,v in pairs(self.org.tokens) do
+         table.insert(token_list, k)
+      end
+   end
+   if token_list == {} then
+      modpol.interactions.message(
+         self.initiator,
+         "No tokens in org")
+      modpol.interactions.org_dashboard(
+         self.initiator, self.org.name)
+      self.org:delete_process(self.id)
+      return
+   else
+      modpol.interactions.dropdown_query(
+         self.initiator,
+         "Which token do you want to send?",
+         token_list,
+         function(input_token)
+            modpol.interactions.dropdown_query(
+               self.initiator,
+               "Who do you want to send to?",
+               modpol.util.copy_table(self.org.members),
+               function(input_recipient)
+                  modpol.interactions.text_query(
+                     self.initiator,
+                     "How much do you want to give (a number)?",
+                     function(input_amount)
+                        modpol.modules.tokenomics.transfer(
+                           self.org.id,
+                           input_token,
+                           self.initiator,
+                           input_recipient,
+                           input_amount
+                        )
+                        modpol.interactions.org_dashboard(
+                           self.initiator, self.org.name)
+                        -- close process
+                        if result then result() end
+                        self.org:delete_process(self.id)
+                     end
+                  )
+               end
+            )
+         end
+      )
+   end
+end
+
+--- (Required) Add to module table
+modpol.modules.send_token = send_token

+ 5 - 2
modpol_core/modules/template.lua

@@ -43,10 +43,13 @@ function module_template:initiate(result)
    -- call interaction functions here!
 
    -- concluding functions:
-   -- call these wherever process might end;
+   -- first, where appropriate, return users to dashboards.
+   -- second, result:
    -- may need to put result in self.data.result
-   --  if process ends in another function
+   -- call this when module is successful (not for abort):
    if result then result() end
+   -- third, delete the process
+   -- call this wherever process might end:
    self.org:delete_process(self.id)
 end
 

+ 213 - 0
modpol_core/modules/tokenomics.lua

@@ -0,0 +1,213 @@
+--- tokenomics
+-- @module tokenomics
+-- Depends on consent
+
+local tokenomics = {
+    name = "Tokenomics",
+    slug = "tokenomics",
+    desc = "A simple library for token economics",
+    hide = true;
+}
+
+--- (Required) Data for module
+-- Variables that module uses during the course of a process
+-- Can be blank
+tokenomics.data = {
+   result = nil
+}
+
+--- (Required): config for module 
+-- @field consent Require consent to create?
+-- @field token_variables the data that goes into the token
+-- @field token_slug A no-spaces slug for the token
+-- @field initial_treasury Quantity in org treasury
+-- @field negative_spend Boolean: can users spend negative tokens? (for mutual credit)
+-- @field balances Table of user balances
+tokenomics.config = {
+   consent = false,
+   token_slug = "token",
+   token_variables = {
+      treasury = 0,
+      negative_spend = true,
+      balances = {}
+   }
+}
+
+--- (Required): initiate function: creates a token in an org
+-- set up the token data structure
+-- create an org treasury
+-- @param result (optional) Callback if this module is embedded in other modules
+-- @function initiate
+function tokenomics:initiate(result)
+   -- TODO need to create a series of interactions to get the info from users
+   self.data.result = result
+   if self.org.tokens and self.org.tokens[slug] then
+      modpol.interactions.message(
+         self.initiator, "Token slug taken, aborting")
+      self.org:delete_process(self.id)
+   else
+      if self.config.consent then
+         self:call_module(
+            "consent",
+            self.initiator,
+            {
+               prompt = "Create token "..
+                  self.config.token_slug.."?",
+               votes_required = #self.org.members
+            },
+            function()
+               modpol.interactions.message_org(
+                  self.initiator, self.org.id,
+                  "Consent reached: creating token "..
+                  self.config.token_slug)
+               self:create_token()
+            end
+         )
+      else
+         self:create_token()
+      end
+   end
+end
+
+function tokenomics:create_token()
+   if not self.org.tokens then self.org.tokens = {} end
+   self.org.tokens[self.config.token_slug] =
+      self.config.token_variables
+   self.org:record("Created token "..self.config.token_slug,
+                   self.slug)
+   modpol.interactions.message_org(
+      self.initiator, self.org.id,
+      "Created token "..self.config.token_slug)
+   if self.data.result then self.data.result() end
+   -- call this wherever process might end:
+   self.org:delete_process(self.id)
+end
+
+-----------------------------------------
+-- Utility functions
+-- all need to account for the fact that some users may not yet have balances
+-- all need to write to persistent data
+-- amount can be positive or negative (except transfer)
+
+-- returns balance
+-- if no user, get treasury balance
+-- @param org Name (string) or id (num)
+-- @param token Slug (string)
+-- @param user Name (string)
+function tokenomics.balance(org, token, user)
+   local this_org = modpol.orgs.get_org(org)
+   if not this_org[token] then return nil, "Token not found" end
+   if not user then
+      return this_org[token].treasury
+   end
+   if not this_org[user] then
+      return nil, "User not found"
+   else
+      local user_balance = this_org[token].balances[user]
+      if user_balance then
+         return user_balance
+      else
+         return 0
+      end
+   end
+end
+
+-- @param org Org name (string) or id (number)
+-- @param token Token slug (string)
+function tokenomics.change_balance(org, token, user, amount)
+   local this_org = modpol.orgs.get_org(org)
+   if not this_org then
+      return nil, "Cannot change balance: Org not found"
+   elseif not this_org.tokens then
+      return nil, "Cannot change balance: no tokens found"
+   elseif not this_org.tokens[token] then
+      return nil, "Cannot change balance: token not found"
+   elseif not this_org.members[user] then
+      return nil, "Cannot change balance: user not in org"
+   elseif not tonumber(amount) then
+      return nil, "Cannot change balance: invalid amount"
+   else
+      local old_balance = this_org.tokens[token].balances[user]
+      if not old_balance then old_balance = 0 end
+      local new_balance = old_balance + amount
+      this_org.tokens[token].balances[user] = new_balance
+      local msg = "Your balance of token "..token..
+         " changed from "..old_balance.." to "..new_balance
+      modpol.interactions.message(user, msg)
+      self.org:record(
+         "Changed token balance for "..user,self.slug)
+   end
+end
+
+-- @param amount Positive number
+function tokenomics.transfer(org, token, sender, recipient, amount)
+   local sender_balance = tokenomics.balance(org, token, sender)
+   local recipient_balance = tokenomics.balance(org, token, recipient)
+   if not sender_balance or recipient_balance then
+      return nil, "Transfer failed, user not found"
+   else
+      sender_balance = sender_balance - amount
+      recipient_balance = recipient_balance + amount
+      local neg_spend = modpol.orgs.get_org(org).tokens[token].negative_spend
+      if sender_balance < 0 and not neg_spend then
+         return nil, "Transfer failed, negative spend not allowed"
+      else
+         tokenomics.change_balance(
+            org, token, sender, sender_balance)
+         tokenomics.change_balance(
+            org, token, recipient, recipient_balance)
+         return "Transfer complete"
+      end
+   end
+end
+
+-- @param amount Can be positive or negative, assumes flow from treasury to recipient
+function tokenomics.treasury_transfer(org, token, recipient, amount)
+   local this_org = modpol.orgs.get_org(org)
+   if not this_org then
+      return nil, "Cannot transfer treasury: Org not found"
+   elseif not this_org.tokens then
+      return nil, "Cannot transfer treasury: no tokens found"
+   elseif not this_org.tokens[token] then
+      return nil, "Cannot transfer treasury: token not found"
+   elseif not this_org.members[user] then
+      return nil, "Cannot transfer treasury: user not in org"
+   elseif not tonumber(amount) then
+      return nil, "Cannot change balance: invalid amount"
+   else
+      local new_treasury = this_org.tokens[token].treasury - amount
+      local new_recipient_balance = tokenomics.balance(org, token, recipient) + amount
+      if new_treasury < 0 and not this_org.tokens[token].negative_spend then
+         return nil, "Transfer failed, negative spend not allowed"
+      elseif new_recipient_balance < 0 and not this_org.tokens[token].negative_spend then
+         return nil, "Transfer failed, negative spend not allowed"
+      else
+         this_org.tokens[token].treasury = new_treasury
+         self.org:record("Treasury payment",self.slug)
+         tokenomics.change_balance(org, token, recipient, amount)
+      end
+   end
+end
+
+-- creates new tokens in the org treasury
+function tokenomics.issue(org, token, amount)
+   local this_org = modpol.orgs.get_org(org)
+   if not this_org then
+      return nil, "Cannot transfer treasury: Org not found"
+   elseif not this_org.tokens then
+      return nil, "Cannot transfer treasury: no tokens found"
+   elseif not this_org.tokens[token] then
+      return nil, "Cannot transfer treasury: token not found"
+   elseif not tonumber(amount) then
+      return nil, "Cannot change balance: invalid amount"
+   else
+      this_org.tokens[token].treasury =
+         this_org.tokens[token].treasury + amount
+      self.org:record("Issued tokes to treasury","tokenomics")
+   end
+end
+
+------------------------------------------
+
+--- (Required) Add to module table
+modpol.modules.tokenomics = tokenomics

+ 4 - 2
modpol_core/orgs/base.lua

@@ -76,6 +76,8 @@ end
 --- Deletes all orgs except for the 
 -- @function modpol.orgs.reset
 function modpol.orgs.reset()
+   local instance_members =
+      modpol.util.copy_table(modpol.instance.members)
     for id, org in ipairs(modpol.orgs.array) do
         if id > 1 then
             modpol.orgs.array[id] = "removed"
@@ -84,9 +86,9 @@ function modpol.orgs.reset()
 
     modpol.orgs.array[1] = nil
     modpol.instance = modpol.orgs.init_instance()
-    
+    modpol.instance.members = instance_members
 
-    modpol.ocutil.log('Reset all orgs')
+    modpol.ocutil.log('All orgs reset')
     modpol.orgs:record('Resetting all orgs', 'org_reset')
 end
 

+ 54 - 25
modpol_core/orgs/process.lua

@@ -7,33 +7,18 @@
 -- @param intiator Initiator for module
 -- @param config Config for module
 -- @param result
-function modpol.orgs:call_module(module_slug, initiator, config, result) 
+function modpol.orgs:call_module(module_slug, initiator, config, result, parent_id) 
     if not modpol.modules[module_slug] then
         modpol.ocutil.log('Error in ' .. self.name .. ':call_module -> module "' .. module_slug .. '" not found')
         return
     end
 
-    local empty_index = nil
-    -- linear search for empty process slots (lazy deletion)
-    for k, v in ipairs(self.processes) do
-        if v == 'deleted' then
-            empty_index = k
-            break
-        end
-    end
-
-    local index
-    -- attempts to fill empty spots in list, otherwise appends to end
-    if empty_index then
-        index = empty_index
-    else
-        index = #self.processes + 1
-    end
+    local index = #self.processes + 1
 
     local module = modpol.modules[module_slug]
 
     -- sets default values for undeclared config variables
-    if #module.config > 0 then
+    if modpol.util.num_pairs(module.config) > 0 and config then
         for k, v in pairs(module.config) do
             if config[k] == nil then
                 config[k] = v
@@ -47,24 +32,61 @@ function modpol.orgs:call_module(module_slug, initiator, config, result)
         initiator = initiator,
         org = self,
         id = index,
+        parent_id = parent_id,
+        children = {},
         config = config,
-        data = module.data,
+        data = modpol.util.copy_table(module.data),
         slug = module_slug
     }
 
+    -- call module wrapper for modules, passes its id to child process when called
+    function new_process:call_module(module_slug, initiator, config, result)
+        local child_id = self.org:call_module(module_slug, initiator, config, result, self.id)
+        table.insert(self.children, child_id)
+    end
+
     setmetatable(new_process, new_process.metatable)
 
     self.processes[index] = new_process
     self.processes[index]:initiate(result)
+    local msg = "Initiating "..module_slug..
+      " process id "..index.." in org "..self.name
 
     return index
 end
 
---- Delete process by id
--- @function modpol.orgs:delete_process
--- @param id Id of process
-function modpol.orgs:delete_process(id) 
-    self.processes[id] = 'deleted'
+
+function modpol.orgs:get_root_process(id)
+    local process = self.processes[id]
+    while (process.parent_id) do
+        process = self.processes[process.parent_id]
+    end
+    return process
+end
+
+function modpol.orgs:delete_process(id)
+    local process = self.processes[id]
+    if process and process ~= "deleted" then
+        -- recursively deletes any children
+        if #process.children > 0 then
+            for i, child_id in pairs(process.children) do
+                self:delete_process(child_id)
+            end
+        end
+        local msg = "Deleting " .. self.processes[id].slug .. " process id "..id.." in org "..self.name
+        modpol.ocutil.log(msg)
+        self:record(msg, self.processes[id].slug)
+        self:wipe_pending_actions(id)
+        -- sets process to 'deleted' in process table
+        self.processes[id] = 'deleted'
+    end
+end
+
+--- Delete process tree by id
+-- @function modpol.orgs:delete_process_tree
+-- @param id Id of process tree
+function modpol.orgs:delete_process_tree(id)
+    self:delete_process(self:get_root_process(id).id)
 end
 
 --- Add a new pending action
@@ -124,8 +146,15 @@ function modpol.orgs:interact(process_id, user)
    local process = self.processes[process_id]
    if self.pending[user] then
       local callback = self.pending[user][process_id]
-      if callback then
+      if callback and process ~= "deleted" then
+         -- get data in case callback ends process
+         local slug = self.processes[process_id].slug
+         -- run callback
          process[callback](process, user)
+         -- record org data
+         local msg = "Updating "..slug..
+            " process id "..process_id.." in org "..self.name
+         self:record(msg, slug)
       end
    end
 end

+ 27 - 0
modpol_core/orgs/users.lua

@@ -0,0 +1,27 @@
+-- /users.lua
+-- User-related functions for Modular Politics
+
+-- ===================================================================
+-- Function: modpol.list_users
+-- Params: org
+-- Outputs: Table of user names
+--
+-- This may be overwritten by the platform-specific interface
+
+modpol.list_users = function(org)
+   local users = {}
+   if (org == nil) then -- no specified org; all players
+      if modpol.orgs["instance"]
+         and modpol.orgs["instance"]["members"] then
+         -- if instance exists and has membership
+         users = modpol.orgs["instance"]["members"]
+      else
+         users = {}
+      end
+   else -- if an org is specified
+      if (modpol.orgs[org] ~= nil) then -- org exists
+         users = modpol.orgs[org]["members"]
+      end
+   end
+   return users
+end

+ 8 - 2
modpol_core/util/misc.lua

@@ -9,8 +9,14 @@ modpol.util = {}
 -- @return copy of table
 function modpol.util.copy_table(t)
    local t2 = {}
-   for k,v in pairs(t) do
-      t2[k] = v
+   if pairs(t) then
+      for k,v in pairs(t) do
+         if type(v) == "table" then
+            t2[k] = modpol.util.copy_table(v)
+         else
+            t2[k] = v
+         end
+      end
    end
    return t2
 end

+ 1 - 5
modpol_minetest/api.lua

@@ -9,11 +9,7 @@ local localdir = minetest.get_modpath("modpol") .. "/modpol_minetest"
 
 --overrides
 dofile (localdir .. "/overrides/interactions.lua")
-
---testing command for "singleplayer"
-function modpol.msg(text)
-   modpol.interactions.message("singleplayer",text)
-end
+dofile (localdir .. "/overrides/users.lua")
 
 -- ===================================================================
 -- Minetest Chatcommands

+ 1 - 1
modpol_minetest/chatcommands.lua

@@ -34,8 +34,8 @@ regchat(
    "mptest", {
       privs = {privs=true},
       func = function(user)
+         modpol.instance.members = modpol.list_users()
          modpol.orgs.reset()
-         modpol.instance:add_member(user)
          modpol.interactions.dashboard(user)
          return true, "Reset orgs"
       end,

+ 1 - 1
modpol_minetest/modules/priv_to_org.lua

@@ -20,7 +20,7 @@ priv_to_org.config = {
 function priv_to_org:initiate(result) 
    local player_privs = minetest.get_player_privs(self.initiator)
    -- construct table for display
-   local player_privs_table = {"View..."}
+   local player_privs_table = {}
    for k,v in pairs(player_privs) do
       if player_privs[k] then
          table.insert(player_privs_table,k)

+ 265 - 47
modpol_minetest/overrides/interactions.lua

@@ -49,13 +49,14 @@ function modpol.interactions.dashboard(user)
    local user_orgs = modpol.orgs.user_orgs(user)
    local all_users = modpol.instance:list_members()
    -- pending list
-   local user_pending = {"View..."}
    local user_pending_count = 0
+   local user_pending = {}
    for k,v in ipairs(modpol.orgs.array) do
       if v.pending and v.pending[user] then
-         modpol.msg(v.name)
-         table.insert(user_pending, v.name)
-         user_pending_count = user_pending_count + 1
+         if modpol.util.num_pairs(v.pending[user]) ~= 0 then
+            table.insert(user_pending, v.name)
+            user_pending_count = user_pending_count + 1
+         end
       end
    end
 
@@ -63,15 +64,15 @@ function modpol.interactions.dashboard(user)
     local formspec = {
        "formspec_version[4]",
        "size[10,8]",
-       "label[0.5,0.5;M O D P O L]",
+       "hypertext[0.5,0.5;9,1;title;<big>Org dashboard</big>]",
        "label[0.5,2;All orgs:]",
-       "dropdown[2,1.5;7,0.8;all_orgs;"..formspec_list(all_orgs)..";;]",
+       "dropdown[2.5,1.5;7,0.8;all_orgs;View...,"..formspec_list(all_orgs)..";;]",
        "label[0.5,3;Your orgs:]",
-       "dropdown[2,2.5;7,0.8;user_orgs;"..formspec_list(user_orgs)..";;]",
+       "dropdown[2.5,2.5;7,0.8;user_orgs;View...,"..formspec_list(user_orgs)..";;]",
        "label[0.5,4;All users:]",
-       "dropdown[2,3.5;7,0.8;all_users;"..formspec_list(all_users)..";;]",
+       "dropdown[2.5,3.5;7,0.8;all_users;View...,"..formspec_list(all_users)..";;]",
        "label[0.5,5;Pending ("..user_pending_count.."):]",
-       "dropdown[2,4.5;7,0.8;pending;"..formspec_list(user_pending)..";;]",
+       "dropdown[2.5,4.5;7,0.8;pending;View...,"..formspec_list(user_pending)..";;]",
        "button[0.5,7;1,0.8;refresh;Refresh]",
        "button_exit[8.5,7;1,0.8;close;Close]",
     }
@@ -88,10 +89,22 @@ minetest.register_on_player_receive_fields(function (player, formname, fields)
             minetest.close_formspec(pname, formname)
          elseif fields.refresh then
             modpol.interactions.dashboard(pname)
-         -- Put all dropdowns at the end
+          -- Put all dropdowns at the end
+         elseif fields.all_users
+            and fields.all_users ~= "View..." then
+            modpol.interactions.user_dashboard(
+               pname,
+               fields.all_users,
+               function()
+                  modpol.interactions.dashboard(pname)
+               end
+            )
          elseif fields.all_orgs or fields.user_orgs or fields.pending then
             local org_name = fields.all_orgs or fields.user_orgs or fields.pending
-            modpol.interactions.org_dashboard(pname, org_name)
+            if org_name ~= "View..." then
+               modpol.interactions.org_dashboard(
+                  pname, org_name)
+            end
          end
       end
 end)
@@ -114,12 +127,15 @@ function modpol.interactions.org_dashboard(user, org_string)
       end
       return ""
    end
-   
+
    -- identify parent
    local parent = modpol.orgs.get_org(org.parent)
    if parent then parent = parent.name
    else parent = "none" end
 
+   -- prepare members menu
+   local members = org.members
+
    -- prepare children menu
    local children = {}
    for k,v in ipairs(org.children) do
@@ -127,7 +143,6 @@ function modpol.interactions.org_dashboard(user, org_string)
       table.insert(children, this_child.name)
    end
    table.sort(children)
-   table.insert(children,1,"View...")
 
    -- prepare modules menu
    local modules = {}
@@ -140,21 +155,21 @@ function modpol.interactions.org_dashboard(user, org_string)
       end
    end
    table.sort(modules)
-   table.insert(modules,1,"View...")
 
    -- prepare pending menu
    local pending = {}
    local num_pending = 0
    if org.pending[user] then
       for k,v in pairs(org.pending[user]) do
-         local pending_string = org.processes[k].name
-            .." ["..k.."]"
-         table.insert(pending, pending_string)
-         num_pending = num_pending + 1
+         if org.processes[k] ~= "deleted" then
+            local pending_string = org.processes[k].name
+               .." ["..k.."]"
+            table.insert(pending, pending_string)
+            num_pending = num_pending + 1
+         end
       end
    end
    table.sort(pending)
-   table.insert(pending,1,"View...")
 
    -- set player context
    local user_context = {}
@@ -164,17 +179,17 @@ function modpol.interactions.org_dashboard(user, org_string)
     local formspec = {
        "formspec_version[4]",
        "size[10,8]",
-       "label[0.5,0.5;Org: "..
-          minetest.formspec_escape(org.name)..membership_toggle(org.name).."]",
-       "label[0.5,1;Parent: "..parent..membership_toggle(parent).."]",
+       "hypertext[0.5,0.5;9,1;title;<big>Org: <b>"..
+          minetest.formspec_escape(org.name).."</b>"..membership_toggle(org.name).."</big>]",
+       "label[0.5,1.25;Parent: "..parent..membership_toggle(parent).."]",
        "label[0.5,2;Members:]",
-       "dropdown[2,1.5;7,0.8;user_orgs;"..formspec_list(org.members)..";;]",
-       "label[0.5,3;Children:]",
-       "dropdown[2,2.5;7,0.8;children;"..formspec_list(children)..";;]",
+       "dropdown[2.5,1.5;7,0.8;members;View...,"..formspec_list(members)..";;]",
+       "label[0.5,3;Child orgs:]",
+       "dropdown[2.5,2.5;7,0.8;children;View...,"..formspec_list(children)..";;]",
        "label[0.5,4;Modules:]",
-       "dropdown[2,3.5;7,0.8;modules;"..formspec_list(modules)..";;]",
+       "dropdown[2.5,3.5;7,0.8;modules;View...,"..formspec_list(modules)..";;]",
        "label[0.5,5;Pending ("..num_pending.."):]",
-       "dropdown[2,4.5;7,0.8;pending;"..formspec_list(pending)..";;]",
+       "dropdown[2.5,4.5;7,0.8;pending;View...,"..formspec_list(pending)..";;]",
        "button[0.5,7;1,0.8;refresh;Refresh]",
        "button[8.5,7;1,0.8;back;Back]",
     }
@@ -197,10 +212,20 @@ minetest.register_on_player_receive_fields(function (player, formname, fields)
          elseif fields.back then
             modpol.interactions.dashboard(pname)
          elseif fields.refresh then
-            modpol.interactions.org_dashboard(pname,org.name)
-            
+            modpol.interactions.org_dashboard(pname, org.name)
+
          -- Put all dropdowns at the end
-         -- Receiving modules
+            -- Receiving modules
+         elseif fields.members
+            and fields.members ~= "View..." then
+            modpol.interactions.user_dashboard(
+               pname,
+               fields.members,
+               function()
+                  modpol.interactions.org_dashboard(
+                     pname, org.name)
+               end
+            )
          elseif fields.modules
             and fields.modules ~= "View..." then
             local module = nil
@@ -217,10 +242,13 @@ minetest.register_on_player_receive_fields(function (player, formname, fields)
                   function(input)
                      if input == "Yes" then
                         org:call_module(module.slug, pname)
+                     elseif input == "No" then
+                        modpol.interactions.org_dashboard(
+                           pname, org.id)
                      end
                end)
             end
-            
+
          -- Receiving pending
          elseif fields.pending
             and fields.pending ~= "View..." then
@@ -240,23 +268,66 @@ minetest.register_on_player_receive_fields(function (player, formname, fields)
       end
 end)
 
+--- Function: modpol.interactions.user_dashboard
+-- Displays a dashboard about a particular user
+-- @param viewer Name of user viewing the dashboard (string)
+-- @param user Name of user being viewed (string)
+-- @param completion Optional function to call on Done button
+function modpol.interactions.user_dashboard(viewer, user, completion)
+   local user_orgs = modpol.orgs.user_orgs(user)
 
--- Function: modpol.interactions.policy_dashboard
--- input: user (string), org_id (int), policy (string)
--- output: opens a dashboard for viewing/editing policy details
--- TODO
-function modpol.interactions.policy_dashboard(
-      user, org_id, policy)
-   modpol.interactions.message(
-      user,
-      "Not yet implemented: " .. policy)
+   -- set player context
+   local user_context = {}
+   user_context["viewer"] = viewer
+   user_context["user"] = user
+   user_context["completion"] = completion
+   _contexts[viewer] = user_context
+   -- set up formspec
+    local formspec = {
+       "formspec_version[4]",
+       "size[10,8]",
+       "hypertext[0.5,0.5;9,1;title;<big>User: <b>"..user.."</b></big>]",
+       "label[0.5,2;User's orgs:]",
+       "dropdown[2.5,1.5;7,0.8;user_orgs;View...,"..formspec_list(user_orgs)..";;]",
+       "button[0.5,7;1.5,0.8;message;Message]",
+       "button_exit[8.5,7;1,0.8;close;Close]",
+    }
+    local formspec_string = table.concat(formspec, "")
+    -- present to player
+    minetest.show_formspec(viewer, "modpol:user_dashboard", formspec_string)
 end
+-- receive input
+minetest.register_on_player_receive_fields(function (player, formname, fields)
+      if formname == "modpol:user_dashboard" then
+         local contexts = _contexts[player:get_player_name()]
+         -- check fields
+         if nil then
+         elseif fields.message then
+            modpol.interactions.message_user(
+               contexts.viewer, contexts.user
+            )
+         elseif fields.back then
+            if contexts.completion then
+               completion()
+            else
+               modpol.interactions.dashboard(
+                  contexts.viewer)
+            end
+         -- dropdown fields
+         elseif fields.user_orgs
+            and fields.user_orgs ~= "View..." then
+            modpol.interactions.org_dashboard(
+               contexts.viewer, fields.user_orgs)
+         end
+      end
+end)
 
 
--- INTERACTION FUNCTIONS
--- =====================
+-- INTERACTION PRIMITIVES
+-- ======================
 
 -- Function: modpol.interactions.message
+-- Produces a brief message to a user
 -- input: user (string), message (string)
 -- output: displays message to specified user
 function modpol.interactions.message(user, message)
@@ -265,6 +336,72 @@ function modpol.interactions.message(user, message)
    end
 end
 
+
+--- Function: modpol.interactions.message_user
+-- Gets and sends a message from one user to another
+-- @param sender Name of user sending (string)
+-- @param recipient Name of user receiving (string)
+function modpol.interactions.message_user(sender, recipient)
+   modpol.interactions.text_query(
+      sender,
+      "Message for "..recipient..":",
+      function(input)
+         modpol.interactions.message(
+            recipient,
+            input.." [from "..sender.."]")
+      end
+   )
+end
+
+--- Function: modpol.interactions.display
+-- Displays complex data to a user
+-- @param user Name of target user (string)
+-- @param title Title of display (string)
+-- @param message Content of message (string or table of strings)
+-- @param completion Optional function for what happens when user is done
+function modpol.interactions.display(
+      user, title, message, completion)
+   -- set up contexts
+   _contexts[user]["completion"] = completion
+   -- set up output
+   local output = ""
+   if type(message) == "table" then
+      output = table.concat(message,"\n")
+   elseif type(message) == "string" then
+      output = message
+   elseif type(message) == "number" then
+      output = message
+   else
+      modpol.interactions.message(
+         self.initiator, "Error: message not typed for display")
+      if completion then completion() else
+         modpol.intereactions.dashboard(user)
+      end
+      return
+   end
+   -- set up formspec
+   local formspec = {
+      "formspec_version[4]",
+      "size[10,8]",
+      "label[0.5,0.5;"..title.."]",
+      "hypertext[0.5,1;9,5.5;display;<global background=black margin=10>"..output.."]",
+      "button_exit[8.5,7;1,0.8;done;Done]",
+    }
+    local formspec_string = table.concat(formspec, "")
+    -- present to player
+    minetest.show_formspec(user, "modpol:display", formspec_string)
+end
+-- receive fields
+minetest.register_on_player_receive_fields(function (player, formname, fields)
+      local pname = player:get_player_name()
+      if formname == "modpol:display" then
+         if fields.done and _contexts[pname].completion then
+            minetest.close_formspec(pname, formname)
+            _contexts[pname].completion()
+         end
+      end
+end)
+
 -- Function: modpol.interactions.text_query
 -- Overrides function at modpol/interactions.lua
 -- input: user (string), query (string), func (function)
@@ -275,7 +412,7 @@ function modpol.interactions.text_query(user, query, func)
        "formspec_version[4]",
        "size[10,4]",
        "label[0.5,1;", minetest.formspec_escape(query), "]",
-       "field[0.5,1.25;9,0.8;input;;]",       
+       "field[0.5,1.25;9,0.8;input;;]",
        "button[0.5,2.5;1,0.8;yes;OK]",
     }
     local formspec_string = table.concat(formspec, "")
@@ -309,14 +446,12 @@ end)
 --    func input: choice (string)
 -- output: calls func on user choice
 function modpol.interactions.dropdown_query(user, label, options, func)
-   -- Add "View..." to the top of the list
-   table.insert(options,1,"View...")
    -- set up formspec
    local formspec = {
       "formspec_version[4]",
       "size[10,4]",
       "label[0.5,1;"..minetest.formspec_escape(label).."]",
-      "dropdown[0.5,1.25;9,0.8;input;"..formspec_list(options)..";;]",
+      "dropdown[0.5,1.25;9,0.8;input;View...,"..formspec_list(options)..";;]",
       "button[0.5,2.5;1,0.8;cancel;Cancel]",
    }
    local formspec_string = table.concat(formspec, "")
@@ -329,7 +464,7 @@ end
 -- receive fields
 minetest.register_on_player_receive_fields(function (player, formname, fields)
       if formname == "modpol:dropdown_query" then
-         local pname = player:get_player_name()            
+         local pname = player:get_player_name()
          if fields.cancel then
             minetest.close_formspec(pname, formname)
          elseif fields.input == "View..." then
@@ -351,6 +486,82 @@ minetest.register_on_player_receive_fields(function (player, formname, fields)
       end
 end)
 
+
+--- Function: modpol.interactions.checkbox_query
+-- Allows user to select from a set of options
+-- @param user Name of user (string)
+-- @param label Query for user before options (string)
+-- @param options table of options and their checked status in the form {{"option_1_string", true}, {"option_2_string", false}}
+-- @param func function to be called with param "input", made up of the corrected table in the same format as the param options
+function modpol.interactions.checkbox_query(
+      user, label, options, func)
+   -- set up formspec
+   -- prepare options
+   local vertical = 0
+   local checkbox_options = {}
+   for i,v in ipairs(options) do
+      local fs_line = ""
+      vertical = i * .5
+      fs_line = "checkbox[0,"..vertical..";checkbox_"..i..";"..
+         minetest.formspec_escape(v[1])..";"..
+         tostring(v[2]).."]"
+      table.insert(checkbox_options, fs_line)
+   end
+   local max = vertical * 4
+   local bar_height = vertical / 2
+   local formspec = {
+      "formspec_version[4]",
+      "size[10,8]",
+      "label[0.5,0.5;"..label.."]",
+      "scrollbaroptions[arrows=default;max="..max..";smallstep=10;largestep=100;thumbsize="..bar_height.."]",
+      "scrollbar[9,1;0.3,5.5;vertical;scroller;0]",
+      "scroll_container[0.5,1;9,5.5;scroller;vertical]",
+   }
+   -- insert options
+   for i,v in ipairs(checkbox_options) do
+      table.insert(formspec, v)
+   end
+   table.insert(formspec,"scroll_container_end[]")
+   table.insert(formspec,"button[0.5,7;1.5,0.8;submit;Submit]")
+   table.insert(
+      formspec,"button_exit[8,7;1.5,0.8;cancel;Cancel]")
+   local formspec_string = table.concat(formspec, "")
+   -- present to players
+   minetest.show_formspec(user, "modpol:checkbox_query", formspec_string)
+   -- put func in _contexts
+   if _contexts[user] == nil then _contexts[user] = {} end
+   _contexts[user]["checkbox_query_func"] = func
+   _contexts[user]["checkbox_query_result"] =
+      modpol.util.copy_table(options)
+end
+-- receive fields
+minetest.register_on_player_receive_fields(function (player, formname, fields)
+      if formname == "modpol:checkbox_query" then
+         local pname = player:get_player_name()
+         -- start checking fields
+         if fields.cancel then
+            minetest.close_formspec(pname, formname)
+         elseif fields.submit then
+            -- send in result
+            minetest.close_formspec(pname, formname)
+            _contexts[pname].checkbox_query_func(
+               _contexts[pname].checkbox_query_result)
+         else
+            for k,v in pairs(fields) do
+               -- identify checkbox actions and flip bool
+               if string.find(k,"checkbox_") then
+                  local index = tonumber(
+                     string.match(k,"%d+"))
+                  _contexts[pname].checkbox_query_result[index][2] =
+                     not _contexts[pname].checkbox_query_result[index][2]
+               end
+            end
+         end
+      end
+end)
+
+
+
 -- Function: modpol.binary_poll_user(user, question, function)
 -- Overrides function at modpol/interactions.lua
 -- Params: user (string), question (string), func (function)
@@ -388,3 +599,10 @@ minetest.register_on_player_receive_fields(function (player, formname, fields)
          minetest.close_formspec(pname, formname)
       end
 end)
+
+-- TESTING
+
+--testing command for "singleplayer"
+function modpol.msg(text)
+   modpol.interactions.message("singleplayer",text)
+end

+ 20 - 0
modpol_minetest/overrides/users.lua

@@ -0,0 +1,20 @@
+-- ===================================================================
+--- Overwrites function at /users.lua.
+-- if nil, lists instance members; if an org name, lists its members
+-- @function modpol.list_users
+-- @param org
+-- @return a table with names of players currently in the game
+modpol.list_users = function(org)
+    local users = {}
+    if (org == nil) then -- no specified org; all players
+       for _,player in ipairs(minetest.get_connected_players()) do
+          local name = player:get_player_name()
+          table.insert(users,name)
+       end
+    else -- if an org is specified
+       if (modpol.orgs[org] ~= nil) then -- org exists
+          users = modpol.orgs[org]["members"]
+       end
+    end
+    return users
+ end