[[ pair.permission.display ]]
@@ -95,6 +95,9 @@
this.permission_to_edit = permission_id
this.permission_mode_from_parent = 'update'
},
+ pair_element_id: function(pk) {
+ return "permission_element_" + pk
+ },
add_condition(permission_id) {
this.clearState() // clear previous state
this.permission_to_condition = permission_id
diff --git a/kybern/groups/templates/groups/manage_condition_include.html b/kybern/groups/templates/groups/manage_condition_include.html
index fc31f17..b7b4236 100644
--- a/kybern/groups/templates/groups/manage_condition_include.html
+++ b/kybern/groups/templates/groups/manage_condition_include.html
@@ -29,7 +29,8 @@
Select condition to add
-
+
@@ -43,9 +44,9 @@
Save condition
+ class="mt-3" @click="add_condition()" id="save_condition_button">Save condition
Save changes
+ class="mt-3" @click="update_condition()" id="save_condition_button">Save changes
diff --git a/kybern/groups/templates/groups/vote_condition_include.html b/kybern/groups/templates/groups/vote_condition_include.html
index 8eb7ae3..c13ef14 100644
--- a/kybern/groups/templates/groups/vote_condition_include.html
+++ b/kybern/groups/templates/groups/vote_condition_include.html
@@ -28,7 +28,7 @@
-
Submit
+
Submit
@@ -124,7 +124,7 @@
axios = this.prep_axios()
url = "{{ base_url }}/groups/get_conditional_data/"
params = { condition_pk: this.condition_pk, condition_type: this.condition_type }
- axios.post(url, params).then(response => {
+ return axios.post(url, params).then(response => {
this.condition_details = response.data.condition_details
this.permission_details = response.data.permission_details
for (field in this.condition_details.fields) {
@@ -145,17 +145,31 @@
params = { condition_pk: this.condition_pk, action_to_take: this.button_selected }
axios.post(url, params).then(response => {
+ new_action_pk = response.data.action_pk
+
// update condition data
- this.get_conditional_data()
this.user_has_taken_action = true
+ this.get_conditional_data().then(response => {
+
+ if (this.condition_details.status == "waiting") {
+
+ // In a vote, *usually* the condition is still waiting after a person's taken action,
+ // because the condition is only resolved after the voting period ends. So we'll need
+ // a separate way to let people know if their vote isn't cast due to a condition.
+
+ } else {
+
+ // update action this was a condition on
+ this.addOrUpdateAction({ action_pk: this.action_details["action_pk"] })
+
+ // also call vuex to record this as an action (need to do this for all actions)
+ this.addOrUpdateAction({ action_pk: new_action_pk })
- // update action this was a condition on
- this.addOrUpdateAction({ action_pk: this.action_details["action_pk"] })
-
- // also call vuex to record this as an action (need to do this for all actions)
- this.addOrUpdateAction({ action_pk: response.data.action_pk })
+ }
- })
+ }).catch(error => { console.log("Error refreshing condition data:", error); this.error_message = error })
+
+ }).catch(error => { console.log("Error updating condition: ", error); this.error_message = error })
}
}
diff --git a/kybern/groups/views.py b/kybern/groups/views.py
index ed74bf2..a64b3bc 100644
--- a/kybern/groups/views.py
+++ b/kybern/groups/views.py
@@ -41,6 +41,21 @@ def get_model(model_name):
pass
+readable_log_dict = {
+ "action did not meet any permission criteria": "You do not have permission to take this action."
+}
+
+
+def make_action_errors_readable(action):
+ """If needed, gets or creates both a developer-friendly (detailed) log and a user-friendly log."""
+
+ if action.resolution.status in ["accepted", "implemented"]:
+ return action.resolution.log, action.resolution.log # unlikely to be displayed/accessed
+ if action.resolution.status == "waiting":
+ return "This action cannot be completed until a condition is passed.", action.resolution.log
+ return readable_log_dict.get(action.resolution.log, "We're sorry, there was an error"), action.resolution.log
+
+
def process_action(action):
if action.resolution.status == "implemented":
@@ -81,14 +96,13 @@ def process_action(action):
def get_action_dict(action):
- action_log = action.resolution.log
- if (not action_log and action.resolution.status == "waiting"):
- action_log = "waiting on condition"
+ display_log, developer_log = make_action_errors_readable(action)
return {
"action_created": True if action.resolution.status in ["implemented", "approved", "waiting", "rejected"] else False,
"action_status": action.resolution.status,
- "action_log": action_log,
"action_pk": action.pk,
+ "action_log": display_log,
+ "action_developer_log": developer_log
}
@@ -472,7 +486,6 @@ def delete_post(request, target):
action_dict = get_action_dict(action)
action_dict["deleted_post_pk"] = pk
- print(action_dict)
return JsonResponse(action_dict)
diff --git a/kybern/mysite/settings.py b/kybern/mysite/settings.py
index a61a062..b1f55af 100644
--- a/kybern/mysite/settings.py
+++ b/kybern/mysite/settings.py
@@ -16,7 +16,7 @@
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-ALLOWED_HOSTS = ['127.0.0.1', 'kybern.herokuapp.com']
+ALLOWED_HOSTS = ['127.0.0.1', 'kybern.herokuapp.com', 'www.kybern.org']
# Application definition
diff --git a/kybern/mysite/urls.py b/kybern/mysite/urls.py
index b70d917..acea755 100644
--- a/kybern/mysite/urls.py
+++ b/kybern/mysite/urls.py
@@ -15,6 +15,9 @@
"""
from django.contrib import admin
from django.urls import path, include
+from django.conf.urls import handler404, handler500
+from .views import error_404, error_500
+
urlpatterns = [
path('', include('accounts.urls')),
@@ -23,3 +26,6 @@
path('groups/', include('groups.urls')),
path('admin/', admin.site.urls),
]
+
+handler404 = error_404
+handler500 = error_500
\ No newline at end of file
diff --git a/kybern/mysite/views.py b/kybern/mysite/views.py
new file mode 100644
index 0000000..7d091cc
--- /dev/null
+++ b/kybern/mysite/views.py
@@ -0,0 +1,11 @@
+from django.shortcuts import render
+
+
+def error_404(request, exception):
+ data = {}
+ return render(request,'accounts/error_404.html', data)
+
+
+def error_500(request):
+ data = {}
+ return render(request,'accounts/error_500.html', data)
\ No newline at end of file
diff --git a/kybern/tests.py b/kybern/tests.py
index 7f7a992..83c30b3 100644
--- a/kybern/tests.py
+++ b/kybern/tests.py
@@ -1,4 +1,4 @@
-import time
+import time, json
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from splinter import Browser
from django.conf import settings
@@ -6,6 +6,9 @@
from django.contrib.auth.models import User
from concord.communities.client import CommunityClient
+from concord.permission_resources.client import PermissionResourceClient
+from concord.conditionals.client import PermissionConditionalClient
+from concord.actions.state_changes import Changes
from groups.models import Group
@@ -56,11 +59,12 @@ def delete_selected_in_multiselect(self, username):
return True
return False
- def select_from_multiselect(self, selection, element_css=".multiselect__element"):
+ def select_from_multiselect(self, selection, element_css=".multiselect__element", search_within=None):
"""Helper method to select options given the custom interface vue-multiselect provides."""
- self.browser.find_by_css(".multiselect__select").first.click()
+ base = search_within if search_within else self.browser
+ base.find_by_css(".multiselect__select").first.click()
time.sleep(.25)
- for item in self.browser.find_by_css(element_css):
+ for item in base.find_by_css(element_css):
if selection in item.text:
item.click()
return True
@@ -224,6 +228,279 @@ def test_adding_permission_changes_site_behavior(self):
self.delete_selected_in_multiselect("crystaldunn")
self.browser.find_by_id('save_member_changes').first.click()
time.sleep(.5)
- self.assertTrue(self.browser.is_text_present('action did not meet any permission criteria'))
+ self.assertTrue(self.browser.is_text_present('You do not have permission to take this action.'))
self.browser.find_by_css(".close").first.click() # close modal
self.assertEquals(self.browser.find_by_id('members_member_count').text, "7 people")
+
+
+class ActionsTestCase(BaseTestCase):
+
+ def setUp(self):
+ self.create_users()
+ self.actor = User.objects.first()
+ self.client = CommunityClient(actor=self.actor)
+ self.client.community_model = Group
+ self.community = self.client.create_community(name="USWNT")
+ self.client.set_target(target=self.community)
+ self.client.add_members(member_pk_list=[user.pk for user in User.objects.all()])
+
+ def test_taking_action_generates_action(self):
+
+ # Add role
+ self.login_user("meganrapinoe", "badlands2020")
+ self.go_to_group("USWNT")
+ self.browser.find_by_css(".role_name_display")[0].scroll_to()
+ self.browser.find_by_id('add_role_button').first.click()
+ self.browser.fill('role_name', 'forwards')
+ self.browser.find_by_id('save_role_button').first.click()
+ self.browser.find_by_css(".close").first.click() # close modal
+
+ # Check for action in action history
+ self.browser.find_by_css("#action_history > span > button")[0].scroll_to()
+ self.browser.find_by_css("#action_history > span > button").first.click()
+ self.assertTrue(self.browser.is_text_present('meganrapinoe added role forwards to USWNT'))
+
+
+class ActionConditionsTestCase(BaseTestCase):
+
+ def setUp(self):
+
+ # Basic setup
+ self.create_users()
+ self.actor = User.objects.first()
+ self.client = CommunityClient(actor=self.actor)
+ self.client.community_model = Group
+ self.community = self.client.create_community(name="USWNT")
+ self.client.set_target(target=self.community)
+ self.client.add_members(member_pk_list=[user.pk for user in User.objects.all()])
+ self.client.add_role(role_name="forwards")
+ pinoe = User.objects.get(username="meganrapinoe")
+ press = User.objects.get(username="christenpress")
+ heath = User.objects.get(username="tobinheath")
+ self.client.add_people_to_role(role_name="forwards", people_to_add=[pinoe.pk, press.pk, heath.pk])
+
+ # Permission setup
+ self.permissionClient = PermissionResourceClient(actor=self.actor, target=self.community)
+ action, self.permission = self.permissionClient.add_permission(
+ permission_type = Changes.Communities.AddRole, permission_roles=["forwards"])
+
+ def test_adding_condition_to_permission_generates_condition(self):
+
+ # Pinoe adds condition to permission
+ self.login_user("meganrapinoe", "badlands2020")
+ self.go_to_group("USWNT")
+ self.browser.find_by_id('forwards_editrole')[0].scroll_to()
+ self.browser.find_by_id('forwards_editrole').first.click()
+ permissions = [item.text for item in self.browser.find_by_css(".permission-display")]
+ self.assertEquals(permissions, ["add role to community"])
+ css_selector = "#permission_element_" + str(self.permission.pk) + " > div > button.btn.btn-secondary"
+ self.browser.find_by_css(css_selector).first.click()
+ self.browser.select("condition_select", "VoteCondition")
+ # TODO: look up - is there really no way to better identify vue-multiselect items?
+ element_containing_role_dropdown = self.browser.find_by_css(".permissionrolefield")[0]
+ self.select_from_multiselect("forwards", search_within=element_containing_role_dropdown)
+ self.browser.find_by_id('save_condition_button').first.click()
+ time.sleep(.25)
+ self.browser.find_by_css(".close").first.click() # close modal
+
+ # Someone with the permission tries to take action (use asserts to check for condition error text)
+ self.login_user("christenpress", "badlands2020")
+ self.go_to_group("USWNT")
+ self.browser.find_by_id('add_role_button').first.click()
+ self.browser.fill('role_name', 'midfielders')
+ self.browser.find_by_id('save_role_button').first.click()
+ time.sleep(.25)
+ self.assertTrue(self.browser.is_text_present('This action cannot be completed until a condition is passed.'))
+ self.browser.find_by_css(".close").first.click() # close modal
+
+ # Go to action history and the condition link is there in the has_condition column
+ self.browser.find_by_css("#action_history > span > button")[0].scroll_to()
+ self.browser.find_by_css("#action_history > span > button").first.click()
+ self.assertTrue(self.browser.is_text_present('christenpress asked to add role midfielders to USWNT'))
+ self.browser.find_by_xpath('//*[@id="action_history_table_element"]/tbody/tr[1]/td[7]/button').first.click()
+ self.assertTrue(self.browser.is_text_present('Please cast your vote'))
+
+
+class ApprovalConditionsTestCase(BaseTestCase):
+
+ def setUp(self):
+
+ # create group, add members, add roles, add members to role
+ self.create_users()
+ self.actor = User.objects.first()
+ self.client = CommunityClient(actor=self.actor)
+ self.client.community_model = Group
+ self.community = self.client.create_community(name="USWNT")
+ self.client.set_target(target=self.community)
+ self.client.add_members(member_pk_list=[user.pk for user in User.objects.all()])
+ self.client.add_role(role_name="forwards")
+ pinoe = User.objects.get(username="meganrapinoe")
+ press = User.objects.get(username="christenpress")
+ heath = User.objects.get(username="tobinheath")
+ self.client.add_people_to_role(role_name="forwards", people_to_add=[pinoe.pk, press.pk, heath.pk])
+
+ # add permission & condition to permission
+ self.permissionClient = PermissionResourceClient(actor=self.actor, target=self.community)
+ action, self.permission = self.permissionClient.add_permission(
+ permission_type = Changes.Communities.AddRole, permission_roles=["forwards"])
+ self.conditionClient = PermissionConditionalClient(actor=self.actor, target=self.permission)
+ action, self.condition = self.conditionClient.add_condition(condition_type="approvalcondition",
+ permission_data=json.dumps({ "approve_roles": ["forwards"], "reject_roles": ["forwards"] }))
+
+ # have person take action that triggers permission/condition
+ self.client.set_actor(heath)
+ self.client.add_role(role_name="midfielders")
+
+ def test_approve_implements_action(self):
+
+ # User navigates to action history and approves action
+ self.login_user("christenpress", "badlands2020")
+ self.go_to_group("USWNT")
+ self.browser.find_by_css("#action_history > span > button")[0].scroll_to()
+ self.browser.find_by_css("#action_history > span > button").first.click()
+ self.assertTrue(self.browser.is_text_present('tobinheath asked to add role midfielders to USWNT'))
+ self.browser.find_by_xpath('//*[@id="action_history_table_element"]/tbody/tr[1]/td[7]/button').first.click()
+ self.assertTrue(self.browser.is_text_present('Please approve or reject this action.'))
+ self.browser.find_by_css("#btn-radios-1 > label:nth-child(1) > span").first.click()
+ time.sleep(.25)
+ self.browser.find_by_id('save_approve_choice').first.click()
+ time.sleep(.25)
+ self.assertTrue(self.browser.is_text_present("You have approved tobinheath's action. Nothing further is needed from you."))
+
+ # Navigate back to action history and check action is implemented
+ xpath_string = '//*[@id="action_history_modal_' + str(self.community.pk) + '_group___BV_modal_footer_"]/button[2]'
+ self.browser.find_by_xpath(xpath_string).first.click()
+ element = self.browser.find_by_css("#action_history_table_element > tbody > tr:nth-child(1) > td:nth-child(4)")[0]
+ self.assertTrue(element.text, "implemented")
+
+ def test_reject_rejects_action(self):
+
+ # User navigates to action history and approves action
+ self.login_user("christenpress", "badlands2020")
+ self.go_to_group("USWNT")
+ self.browser.find_by_css("#action_history > span > button")[0].scroll_to()
+ self.browser.find_by_css("#action_history > span > button").first.click()
+ self.assertTrue(self.browser.is_text_present('tobinheath asked to add role midfielders to USWNT'))
+ self.browser.find_by_xpath('//*[@id="action_history_table_element"]/tbody/tr[1]/td[7]/button').first.click()
+ self.assertTrue(self.browser.is_text_present('Please approve or reject this action.'))
+ self.browser.find_by_css("#btn-radios-1 > label:nth-child(2) > span").first.click()
+ self.browser.find_by_id('save_approve_choice').first.click()
+ time.sleep(.25)
+ self.assertTrue(self.browser.is_text_present("You have rejected tobinheath's action. Nothing further is needed from you."))
+
+ # Navigate back to action history and check action is implemented
+ xpath_string = '//*[@id="action_history_modal_' + str(self.community.pk) + '_group___BV_modal_footer_"]/button[2]'
+ self.browser.find_by_xpath(xpath_string).first.click()
+ element = self.browser.find_by_css("#action_history_table_element > tbody > tr:nth-child(1) > td:nth-child(4)")[0]
+ self.assertTrue(element.text, "rejected")
+
+ def test_person_without_permission_to_approve_cant_approve(self):
+ self.login_user("emilysonnett", "badlands2020")
+ self.go_to_group("USWNT")
+ self.browser.find_by_css("#action_history > span > button")[0].scroll_to()
+ self.browser.find_by_css("#action_history > span > button").first.click()
+ self.assertTrue(self.browser.is_text_present('tobinheath asked to add role midfielders to USWNT'))
+ self.browser.find_by_xpath('//*[@id="action_history_table_element"]/tbody/tr[1]/td[7]/button').first.click()
+ self.assertTrue(self.browser.is_text_present('You do not have permission to approve or reject this action.'))
+
+
+class VotingConditionTestCase(BaseTestCase):
+
+ def setUp(self):
+
+ # create group, add members, add roles, add members to role
+ self.create_users()
+ self.actor = User.objects.first()
+ self.client = CommunityClient(actor=self.actor)
+ self.client.community_model = Group
+ self.community = self.client.create_community(name="USWNT")
+ self.client.set_target(target=self.community)
+ self.client.add_members(member_pk_list=[user.pk for user in User.objects.all()])
+ self.client.add_role(role_name="forwards")
+ pinoe = User.objects.get(username="meganrapinoe")
+ press = User.objects.get(username="christenpress")
+ heath = User.objects.get(username="tobinheath")
+ self.client.add_people_to_role(role_name="forwards", people_to_add=[pinoe.pk, press.pk, heath.pk])
+
+ # add permission & condition to permission
+ self.permissionClient = PermissionResourceClient(actor=self.actor, target=self.community)
+ action, self.permission = self.permissionClient.add_permission(
+ permission_type = Changes.Communities.AddRole, permission_roles=["forwards"])
+ self.conditionClient = PermissionConditionalClient(actor=self.actor, target=self.permission)
+ action, self.condition = self.conditionClient.add_condition(condition_type="votecondition",
+ permission_data=json.dumps({ "vote_roles": ["forwards"] }))
+
+ # have person take action that triggers permission/condition
+ self.client.set_actor(heath)
+ self.client.add_role(role_name="midfielders")
+
+ def test_yea_updates_vote_results(self):
+
+ # User navigates to action history and votes yea
+ self.login_user("christenpress", "badlands2020")
+ self.go_to_group("USWNT")
+ self.browser.find_by_css("#action_history > span > button")[0].scroll_to()
+ self.browser.find_by_css("#action_history > span > button").first.click()
+ self.assertTrue(self.browser.is_text_present('tobinheath asked to add role midfielders to USWNT'))
+ self.browser.find_by_xpath('//*[@id="action_history_table_element"]/tbody/tr[1]/td[7]/button').first.click()
+ self.assertTrue(self.browser.is_text_present('The results so far are 0 yeas and 0 nays with 0 abstentions.'))
+ self.assertTrue(self.browser.is_text_present('Please cast your vote'))
+ self.browser.find_by_css("#btn-radios-1 > label:nth-child(1) > span").first.click()
+ time.sleep(.25)
+ self.browser.find_by_id('save_vote_choice').first.click()
+ time.sleep(.25)
+ time.sleep(2)
+ self.assertTrue(self.browser.is_text_present('The results so far are 1 yeas and 0 nays with 0 abstentions.'))
+ self.assertTrue(self.browser.is_text_present("Thank you for voting! No further action from you is needed."))
+
+ def test_nay_updates_vote_results(self):
+
+ # User navigates to action history and votes nay
+ self.login_user("christenpress", "badlands2020")
+ self.go_to_group("USWNT")
+ self.browser.find_by_css("#action_history > span > button")[0].scroll_to()
+ self.browser.find_by_css("#action_history > span > button").first.click()
+ self.assertTrue(self.browser.is_text_present('tobinheath asked to add role midfielders to USWNT'))
+ self.browser.find_by_xpath('//*[@id="action_history_table_element"]/tbody/tr[1]/td[7]/button').first.click()
+ self.assertTrue(self.browser.is_text_present('The results so far are 0 yeas and 0 nays with 0 abstentions.'))
+ self.assertTrue(self.browser.is_text_present('Please cast your vote'))
+ self.browser.find_by_css("#btn-radios-1 > label:nth-child(2) > span").first.click()
+ time.sleep(.25)
+ self.browser.find_by_id('save_vote_choice').first.click()
+ time.sleep(.25)
+ self.assertTrue(self.browser.is_text_present('The results so far are 0 yeas and 1 nays with 0 abstentions.'))
+ self.assertTrue(self.browser.is_text_present("Thank you for voting! No further action from you is needed."))
+
+ def test_abstain_updates_vote_results(self):
+
+ # User navigates to action history and votes nay
+ self.login_user("christenpress", "badlands2020")
+ self.go_to_group("USWNT")
+ self.browser.find_by_css("#action_history > span > button")[0].scroll_to()
+ self.browser.find_by_css("#action_history > span > button").first.click()
+ self.assertTrue(self.browser.is_text_present('tobinheath asked to add role midfielders to USWNT'))
+ self.browser.find_by_xpath('//*[@id="action_history_table_element"]/tbody/tr[1]/td[7]/button').first.click()
+ self.assertTrue(self.browser.is_text_present('The results so far are 0 yeas and 0 nays with 0 abstentions.'))
+ self.assertTrue(self.browser.is_text_present('Please cast your vote'))
+ self.browser.find_by_css("#btn-radios-1 > label:nth-child(3) > span").first.click()
+ time.sleep(.25)
+ self.browser.find_by_id('save_vote_choice').first.click()
+ time.sleep(.25)
+ self.assertTrue(self.browser.is_text_present('The results so far are 0 yeas and 0 nays with 1 abstentions.'))
+ self.assertTrue(self.browser.is_text_present("Thank you for voting! No further action from you is needed."))
+
+ def test_person_without_permission_to_approve_cant_vote(self):
+
+ self.login_user("emilysonnett", "badlands2020")
+ self.go_to_group("USWNT")
+ self.browser.find_by_css("#action_history > span > button")[0].scroll_to()
+ self.browser.find_by_css("#action_history > span > button").first.click()
+ self.assertTrue(self.browser.is_text_present('tobinheath asked to add role midfielders to USWNT'))
+ self.browser.find_by_xpath('//*[@id="action_history_table_element"]/tbody/tr[1]/td[7]/button').first.click()
+ self.assertTrue(self.browser.is_text_present('You are not eligible to vote.'))
+
+
+ # def test_vote_has_passed(self):
+ # # FIXME: not sure how to do this, given the one hour vote minimum?
+ # # maybe the vote needs a: [close when X people voted option]
+ # ...