瀏覽代碼

Merge branch 'policy_table' into 'master'

Policy table

See merge request medlabboulder/modpol!21
Nathan Schneider 3 年之前
父節點
當前提交
aa6bb06d5c

+ 6 - 2
modpol/api.lua

@@ -6,10 +6,14 @@ local localdir = modpol.topdir
 dofile (localdir .. "/users/users.lua")
 
 --orgs
-dofile (localdir .. "/orgs/orgs.lua")
+dofile (localdir .. "/orgs/base.lua")
+dofile (localdir .. "/orgs/requests.lua")
 
 --interactions
 dofile (localdir .. "/interactions/interactions.lua")
 
 -- messaging functions
-dofile (localdir .. "/processes/processes.lua")
+dofile (localdir .. "/processes/processes.lua")
+
+--modules
+dofile (localdir .. "/modules/consent.lua")

+ 11 - 3
modpol/modpol.lua

@@ -71,9 +71,17 @@ dofile (topdir .. "/api.lua")
 
 -- ===================================================================
 -- Final checks
-for id, org in ipairs(modpol.orgs.array) do
-    if type(org) == 'table' then 
-        setmetatable(org, modpol.orgs)
+
+-- sets org metatable on load
+if (modpol.orgs.array) then
+    for id, org in ipairs(modpol.orgs.array) do
+        if type(org) == 'table' then 
+            setmetatable(org, modpol.orgs)
+            -- sets process metatable on load
+            for id, process in ipairs(org.processes) do
+                setmetatable(process, modpol.modules[process.type])
+            end
+        end
     end
 end
 

+ 72 - 0
modpol/modules/consent.lua

@@ -0,0 +1,72 @@
+modpol.modules = modpol.modules or {}
+
+modpol.modules.consent = {
+    type = "consent",
+}
+
+-- sets consent to its own callback
+modpol.modules.consent.__index = modpol.modules.consent
+
+function temp_consent_process()
+    return {
+        org_id = nil,
+        request_id = nil,
+        total_votes = 0,
+        majority_to_pass = 0.51,
+        votes_yes = {},
+        votes_no = {}
+    }
+end
+
+-- ===============================================
+-- function to create a new consent process to resolve a pending process
+function modpol.modules.consent:new_process(request_id, org_id)
+    local process = temp_consent_process()
+    process.request_id = request_id
+    process.org_id = org_id
+
+    setmetatable(process, modpol.modules.consent)
+    modpol.ocutil.log('Created new process for request id #' .. request_id)
+
+    return process
+end
+
+-- ======================================================
+-- function for users to vote on a pending request
+function modpol.modules.consent:approve(user, decision)
+    if not modpol.orgs.get_org(self.org_id):has_member(user) then
+        modpol.ocutil.log('Error in consent:approve -> user not a member of the org')
+        return
+    end
+
+    if decision then
+        table.insert(self.votes_yes, user)
+        modpol.ocutil.log('User ' .. user .. ' voted yes on request #' .. self.request_id)
+    else
+        table.insert(self.votes_no, user)
+        modpol.ocutil.log('User ' .. user .. ' voted no on request #' .. self.request_id)
+    end
+
+    self.total_votes = self.total_votes + 1
+
+    self:update_status()
+end
+
+-- ===================================================
+-- determines whether process has finished and resolves request if it has (unfinished)
+function modpol.modules.consent:update_status()
+    local process_org = modpol.orgs.get_org(self.org_id)
+    local eligible_voters = process_org:get_member_count()
+    local votes_needed = math.ceil(self.majority_to_pass * eligible_voters)
+    
+    if #self.votes_yes >= votes_needed then
+        modpol.ocutil.log('Request #' .. self.request_id .. ' passes')
+        process_org:resolve_request(self.request_id, true)
+    elseif #self.votes_no >= votes_needed then
+        modpol.ocutil.log('Request #' .. self.request_id .. ' fails to pass')
+        process_org:resolve_request(self.request_id, false)
+    else
+        modpol.ocutil.log('Waiting for more votes...')
+    end
+end
+

+ 0 - 0
modpol/modules/defer_to.lua


+ 47 - 69
modpol/orgs/orgs.lua → modpol/orgs/base.lua

@@ -14,20 +14,12 @@ function temp_org()
         policies = {},
         processes = {},
         requests = {},
-        request_count = 0,
         members = {},
         parent = nil,
         children = {}
     }
 end
 
-modpol.orgs.request_params = {
-    add_org = 1,
-    delete = 0,
-    add_member = 1,
-    remove_member = 1
-}
-
 -- ==================================================
 -- returns org when given its id or name
 function modpol.orgs.get_org(arg)
@@ -79,10 +71,11 @@ end
 function modpol.orgs.reset()
     for id, org in ipairs(modpol.orgs.array) do
         if id > 1 then
-            modpol.orgs.array[id] = nil
+            modpol.orgs.array[id] = "removed"
         end
     end
 
+    modpol.ocutil.log('Reset all orgs')
     modpol.orgs:record('Resetting all orgs', 'org_reset')
 end
 
@@ -92,7 +85,7 @@ end
 function modpol.orgs.init_instance()
     local error_msg
     if modpol.orgs.array[1] then
-        modpol.ocutil.log('Error: instance has already been initialized')
+        modpol.ocutil.log('Error in orgs.init_instance -> instance has already been initialized')
         return false
     end
 
@@ -105,6 +98,7 @@ function modpol.orgs.init_instance()
     -- adding instance to org list
     modpol.orgs.array[1] = instance
 
+    modpol.ocutil.log('Initialized the instance org')
     modpol.orgs:record('Initialized the instance org', 'create_instance')
 
     return instance
@@ -127,15 +121,15 @@ function modpol.orgs:record(msg, entry_type)
     if type(msg) == 'string' and not(modpol.ocutil.str_empty(msg)) then
         entry.action_msg = msg
     else
-        modpol.ocutil.log('Error: msg must be a non empty string')
+        modpol.ocutil.log('Error in ' .. self.name .. ':record -> msg must be a non empty string')
         return false
     end
 
     if type(entry_type) == 'string' and not(modpol.ocutil.str_empty(entry_type)) then
         entry.entry_type = entry_type
     else
-        modpol.ocutil.log('Error: entry_type must be a non empty string')
-        print(msg, entry_type)
+        modpol.ocutil.log('Error in ' .. self.name .. ':record -> entry_type must be a non empty string')
+        modpol.ocutil.log(msg, entry_type)
         return false
     end
 
@@ -152,17 +146,17 @@ end
 -- ex: instance:add_org('town hall')
 function modpol.orgs:add_org(name)
     if self.id == nil then
-        modpol.ocutil.log('Error: add_org can only be called by another org')
+        modpol.ocutil.log('Error in ' .. self.name .. ':add_org -> add_org can only be called by another org')
         return false
     end
 
     if modpol.ocutil.str_empty(name) then
-        modpol.ocutil.log('Error: org name is required')
+        modpol.ocutil.log('Error in ' .. self.name .. ':add_org -> org name is required')
         return false
     end
 
     if modpol.orgs.get_org(name) then
-        modpol.ocutil.log('Error: org name is already being used')
+        modpol.ocutil.log('Error in ' .. self.name .. ':add_org -> org name is already being used')
         return false
     end
 
@@ -182,7 +176,7 @@ function modpol.orgs:add_org(name)
     modpol.orgs.array[child_org.id] = child_org
 
     self:record('created sub org ' .. name, 'add_org')
-    modpol.ocutil.log('Created sub org ' .. name)
+    modpol.ocutil.log('Created ' .. name .. ' (suborg of ' .. self.name .. ')')
 
     return child_org
 end
@@ -193,20 +187,20 @@ end
 -- note: "reason" param was removed, can be added back
 function modpol.orgs:delete()
     if self.id == 1 then
-        modpol.ocutil.log('Error: cannot delete instance')
+        modpol.ocutil.log('Error in ' .. self.name .. ':delete -> cannot delete instance')
         return false
     end
     
     if #self.children > 0 then
         for i, child_id in pairs(self.children) do
             local child = modpol.orgs.get_org(child_id)
-            print(child_id, child)
+            modpol.ocutil.log(child_id, child)
             child:delete()
         end
     end
 
     modpol.orgs.array[self.id] = 'removed'
-    modpol.ocutil.log('Removed ' .. self.name .. ': ' .. self.id)
+    modpol.ocutil.log('Deleted org ' .. self.name .. ': ' .. self.id)
 
     self:record('Deleted ' .. self.name .. ' and all child orgs', 'del_org')
 
@@ -226,6 +220,12 @@ end
 -- ===========================================
 -- adds a user to an org
 function modpol.orgs:add_member(user)
+    for id, name in ipairs(self.members) do
+        if user == name then
+            modpol.ocutil.log('Error in ' .. self.name .. ':add_member -> user already in org')
+            return false
+        end
+    end
     -- trys to fill in empty spots first
     local empty_index = self:get_member_index('')
     if empty_index then
@@ -234,6 +234,8 @@ function modpol.orgs:add_member(user)
         -- adds to end if no empty spots
         table.insert(self.members, user)
     end
+    
+    modpol.ocutil.log('Added member ' .. user .. ' to ' .. self.name)
     self:record('Added member ' .. user, 'add_member')
 
 end
@@ -246,7 +248,10 @@ function modpol.orgs:remove_member(user)
     user_index = self:get_member_index(user)
     if user_index then
         self.members[user_index] = ''
+    else
+        modpol.ocutil.log('Error in ' .. self.name .. ':remove_member -> user not in org')
     end
+    modpol.ocutil.log('Removed member ' .. user .. ' from ' .. self.name)
     self:record('Removed member ' .. user, 'del_member')
 end
 
@@ -266,7 +271,8 @@ end
 function modpol.orgs:list_member()
     local str
     for k, v in ipairs(self.members) do
-        if str then 
+        -- checking to see if member name is valid
+        if str and str ~= '' then 
             str = str .. '\n' .. v
         else 
             str = v
@@ -275,56 +281,28 @@ function modpol.orgs:list_member()
     return str
 end
 
--- ===========================
--- compares to requests to see if they are identical
-function modpol.orgs.comp_req(req1, req2)
-    -- compares request type
-    if req1.type ~= req2.type then
-        return false
-    else
-        -- comparing parameters
-        -- we can assume the number of params is the same as this is checked in the make_request func
-        for k, v in ipairs(req1.params) do
-            if v ~= req2.params[k] then
-                return false
-            end
+-- ==============================
+-- because member list uses lazy deletion, using #org.members will not show an accurate number
+function modpol.orgs:get_member_count()
+    local count = 0
+    for k, v in ipairs(self.members) do
+        -- the empty string represents a deleted member in the members list
+        if v ~= '' then
+            count = count + 1
         end
     end
-    return true
+    return count
+end
+-- ====================================
+-- adds a new policy to the policy table
+-- must define the policy type, process associated with it, and whether the request must be made by an org member
+function modpol.orgs:set_policy(policy_type, process_type, must_be_member)
+    local new_policy = {
+        process_type = process_type,
+        must_be_member = must_be_member
+    }
+    self.policies[policy_type] = new_policy
+    modpol.ocutil.log('Added policy for ' .. policy_type .. ' in ' .. self.name)
 end
 
--- ================================
--- tries to make a request to the org
-function modpol.orgs:make_request(request)
-    -- makes sure the request has the valid number of parameters
-    local num_params = modpol.orgs.request_params[request.type]
-
-    if num_params == nil then 
-        modpol.ocutil.log("Error: request type is invalid")
-        return false 
-    end
-   
-    for k, v in ipairs(request.params) do
-        num_params = num_params - 1
-    end 
-
-    if num_params ~= 0 then
-        modpol.ocutil.log("Error: request has invalid number of parameters") 
-        return false 
-    end
-
-    -- checking to see if identical request already exists
-    for k, v in ipairs(self.requests) do
-        if self.comp_req(request, v) == true then
-            modpol.ocutil.log("Error: request has already been made")
-            return false
-        end
-    end
-
-    -- use lazy deletion here, not very clean
-    -- table.insert(self.requests, request)
-    self.request_count = self.request_count + 1
-    self.requests[self.request_count] = request
-    return self.request_count
 
-end

+ 179 - 0
modpol/orgs/requests.lua

@@ -0,0 +1,179 @@
+modpol.orgs.request_params = {
+    add_org = 1,
+    delete = 0,
+    add_member = 1,
+    remove_member = 1
+}
+
+-- ================================
+-- creates a new process linked to a request id
+function modpol.orgs:create_process(process_type, request_id)
+    if not modpol.modules[process_type] then
+        modpol.ocutil.log('Process type "' .. process_type .. '" does not exist')
+        return
+    end
+
+    -- retrieving requested module
+    local module = modpol.modules[process_type]
+    local new_process = module:new_process(request_id, self.id)
+
+    -- linear search for empty process slots (lazy deletion)
+    for k, v in ipairs(self.processes) do
+        if v == 'deleted' then
+            local empty_index = k
+            break
+        end
+    end
+
+    -- attempts to fill empty spots in list, otherwise appends to end
+    if empty_index then
+        self.processes[empty_index] = new_process
+        return empty_index
+    else
+        table.insert(self.processes, new_process)
+        return #self.processes
+    end
+end
+
+-- ===========================
+-- compares to requests to see if they are identical
+function modpol.orgs.comp_req(req1, req2)
+    -- compares request type
+    if req1.type ~= req2.type then
+        return false
+    else
+        -- comparing parameters
+        -- we can assume the number of params is the same as this is checked in the make_request func
+        for k, v in ipairs(req1.params) do
+            if v ~= req2.params[k] then
+                return false
+            end
+        end
+    end
+    return true
+end
+
+-- ===============================
+-- returns string of all active requests
+function modpol.orgs:list_request()
+    local str
+    for id, req in ipairs(self.requests) do
+        if req ~= "deleted" then
+            if str then 
+                str = str .. '\n' .. req.type .. ' (' .. req.user .. ') '
+            else 
+                str = req.type .. ' (' .. req.user .. ') '
+            end
+        end
+    end
+    return str
+end
+
+-- ===============================
+-- if the request was approved, the associated function is called, otherwise it is deleted
+function modpol.orgs:resolve_request(request_id, approve)
+    if approve then
+        local request = self.requests[request_id]
+        local p = request.params
+
+        -- there's probably a way to clean this up, the issue is the varying number of commands
+        -- ex: self['add_member'](self, 'member_name')
+        -- not sure if this is safe, more testing to do
+        self[request.type](self, p[1], p[2], p[3])
+        
+        -- if request.type == "add_org" then
+        --     self:add_org(p[1])
+        -- elseif request.type == "delete" then
+        --     self:delete()
+        -- elseif request.type == "add_member" then
+        --     self:add_member(p[1])
+        -- elseif request.type == "remove_member" then
+        --     self:remove_member(p[1])
+        -- end
+        
+    end
+    
+    self.requests[request_id] = "deleted"
+end
+
+-- ================================
+-- tries to make a request to the org
+function modpol.orgs:make_request(request)
+    -- makes sure the request has the valid number of parameters
+    local num_params = modpol.orgs.request_params[request.type]
+
+    if num_params == nil then 
+        modpol.ocutil.log('Error in ' .. self.name .. ':make_request -> request type is invalid')
+        return false 
+    end
+    
+    -- num_params should equal zero at the end if request.params matches the num of params for that type
+    for k, v in ipairs(request.params) do
+        num_params = num_params - 1
+    end 
+
+    if num_params ~= 0 then
+        modpol.ocutil.log('Error in ' .. self.name .. ':make_request -> request has invalid number of parameters') 
+        return false 
+    end
+
+    -- checking to see if identical request already exists
+    for k, v in ipairs(self.requests) do
+        if self.comp_req(request, v) == true then
+            modpol.ocutil.log('Error in ' .. self.name .. ':make_request -> request has already been made')
+            return false
+        end
+    end
+
+    -- checking to see if user is able to make request
+    local requested_policy = self.policies[request.type]
+    local parent_policy = modpol.orgs.get_org(self.parent).policies[request.type]
+
+    -- tries to use org's policy table, defers to parent otherwise
+    if not requested_policy then
+        modpol.ocutil.log(request.type .. ' policy not found, deferring to parent org')
+        requested_policy = parent_policy
+
+        if not parent_policy then
+            modpol.ocutil.log('Error in ' .. self.name .. ':make_request -> parent policy undefined')
+            return false
+        end
+    end
+
+    -- make sure user is allowed to make request
+    if requested_policy.must_be_member and not self:has_member(request.user) then
+        modpol.ocutil.log('Error in ' .. self.name .. ':make_request -> user must be org member to make this request')
+        return false
+    end
+
+    -- linear search for empty process slots (lazy deletion)
+    for k, v in ipairs(self.requests) do
+        if v == 'deleted' then
+            local empty_index = k
+            break
+        end
+    end
+
+    -- attempts to fill empty spots in list, otherwise appends to end
+    local request_id = nil
+    if empty_index then
+        self.requests[empty_index] = request
+        request_id = empty_index
+    else
+        table.insert(self.requests, request)
+        -- finds end of list to return current request's id
+        local count = 0
+        for k, v in ipairs(self.requests) do 
+            count = count + 1 
+        end
+        request_id = count
+    end
+    modpol.ocutil.log("Request made by " .. request.user .. " to " .. request.type)
+
+    -- launching process tied to this request
+    local process_id = self:create_process(requested_policy.process_type, request_id)
+
+    -- returns process id of processes launched by this request
+    return process_id
+end
+

+ 23 - 0
modpol/tests/org_basic_test.lua

@@ -0,0 +1,23 @@
+dofile("../modpol.lua")
+
+print('\nRemoving existing orgs')
+modpol.orgs.reset()
+
+print('\nCreating an org called "test_org"')
+test_org = modpol.instance:add_org('test_org')
+
+print('\nTrying to create an org with the same name')
+duplicate = modpol.instance:add_org('test_org')
+
+print('\nAdding user "luke" to test_org')
+test_org:add_member('luke')
+
+print('\nTrying to add duplicate user to test_org')
+test_org:add_member('luke')
+
+print('\nRemoving user "luke" from test_org')
+test_org:remove_member('luke')
+
+print('\nTrying to remove user "luke" from empty member list')
+test_org:remove_member('luke')
+

+ 26 - 0
modpol/tests/org_req_test.lua

@@ -0,0 +1,26 @@
+dofile('../modpol.lua');
+
+modpol.orgs.reset()
+
+test_org = modpol.instance:add_org('test_org')
+test_org:add_member('luke')
+test_org:add_member('nathan')
+
+test_org:set_policy("add_member", "consent", false);
+
+new_request = {
+    user = "josh",
+    type = "add_member",
+    params = {"josh"}
+}
+
+request_id = test_org:make_request(new_request)
+
+-- process_id = test_org:create_process("consent", request_id)
+for id, process in ipairs(test_org.processes) do
+    process:approve('luke', true)
+    process:approve('nathan', true)
+end
+-- process = test_org.processes[process_id]
+-- process:approve("luke", true)
+-- process:approve("nathan", true)