rename: auto assign -> assignment rule

This commit is contained in:
Rushabh Mehta 2019-02-28 17:37:20 +05:30
parent 02726ba3ed
commit fc5b1d2c3e
13 changed files with 312 additions and 38 deletions

View file

@ -1,7 +1,7 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Auto Assign', {
frappe.ui.form.on('Assignment Rule', {
refresh: function(frm) {
// refresh description
frm.events.rule(frm);

View file

@ -6,7 +6,7 @@
"allow_rename": 1,
"autoname": "Prompt",
"beta": 0,
"creation": "2019-02-27 11:44:44.232703",
"creation": "2019-02-28 17:12:18.815830",
"custom": 0,
"description": "Automatically Assign Documents to Users",
"docstatus": 0,
@ -391,7 +391,7 @@
"label": "Users",
"length": 0,
"no_copy": 0,
"options": "Auto Assign User",
"options": "Assignment Rule User",
"permlevel": 0,
"precision": "",
"print_hide": 0,
@ -449,10 +449,10 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-02-28 16:45:56.645592",
"modified": "2019-02-28 17:12:44.413781",
"modified_by": "Administrator",
"module": "Automation",
"name": "Auto Assign",
"name": "Assignment Rule",
"name_case": "",
"owner": "Administrator",
"permissions": [

View file

@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.desk.form import assign_to
class AssignmentRule(Document):
def on_update(self): # pylint: disable=no-self-use
frappe.cache().delete_value('assignment_rule')
def after_rename(self): # pylint: disable=no-self-use
frappe.cache().delete_value('assignment_rule')
def apply(self, doc):
if self.safe_eval('assign_condition', doc):
self.do_assignment(doc)
return True
# try clearing
if self.unassign_condition:
return self.clear_assignment(doc)
return False
def do_assignment(self, doc):
user = self.get_user()
assign_to.add(dict(
assign_to = user,
doctype = doc.get('doctype'),
name = doc.get('name'),
description = frappe.render_template(self.description, doc)
))
# set for reference in round robin
self.db_set('last_user', user)
def clear_assignment(self, doc):
'''Clear assignments'''
if self.safe_eval('unassign_condition', doc):
return assign_to.clear(doc.get('doctype'), doc.get('name'))
def get_user(self):
'''
Get the next user for assignment
'''
if self.rule == 'Round Robin':
return self.get_user_round_robin()
elif self.rule == 'Load Balancing':
return self.get_user_load_balancing()
def get_user_round_robin(self):
'''
Get next user based on round robin
'''
# first time, or last in list, pick the first
if not self.last_user or self.last_user == self.users[-1].user:
return self.users[0].user
# find out the next user in the list
for i, d in enumerate(self.users):
if self.last_user == d.user:
return self.users[i+1].user
def get_user_load_balancing(self):
'''Assign to the user with least number of open assignments'''
counts = []
for d in self.users:
counts.append(dict(
user = d.user,
count = frappe.db.count('ToDo', dict(
reference_type = self.document_type,
owner = d.user,
status = "Open"))
))
# sort by dict value
sorted_counts = sorted(counts, key = lambda k: k['count'])
# pick the first user
return sorted_counts[0].get('user')
def safe_eval(self, fieldname, doc):
try:
return frappe.safe_eval(self.get(fieldname), None, doc)
except Exception:
# when assignment fails, don't block the document as it may be
# a part of the email pulling
frappe.msgprint(frappe._('Auto assignment failed'), indicator = 'orange')
def apply(doc, method):
if frappe.flags.in_patch or frappe.flags.in_install:
return
assignment_rules = frappe.cache().get_value('assignment_rule', get_assignment_rules)
if doc.doctype in assignment_rules:
# multiple auto assigns
for d in frappe.db.get_all('Assignment Rule', dict(document_type=doc.doctype, disabled = 0), order_by = 'priority desc'):
if frappe.get_doc('Assignment Rule', d.name).apply(doc.as_dict()):
break
def get_assignment_rules():
return [d.document_type for d in frappe.db.get_all('Assignment Rule', fields=['document_type'], filters=dict(disabled = 0))]

View file

@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import random_string
class TestAutoAssign(unittest.TestCase):
def setUp(self):
self.assignment_rule = get_assignment_rule()
clear_assignments()
def test_round_robin(self):
note = make_note(dict(public=1))
# check if auto assigned to first user
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), 'test@example.com')
note = make_note(dict(public=1))
# check if auto assigned to second user
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), 'test1@example.com')
clear_assignments()
note = make_note(dict(public=1))
# check if auto assigned to third user, even if
# previous assignments where closed
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), 'test2@example.com')
# check loop back to first user
note = make_note(dict(public=1))
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), 'test@example.com')
def test_load_balancing(self):
self.assignment_rule.rule = 'Load Balancing'
self.assignment_rule.save()
for _ in range(30):
note = make_note(dict(public=1))
# check if each user has 10 assignments (?)
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'):
self.assertEqual(len(frappe.get_all('ToDo', dict(owner = user, reference_type = 'Note'))), 10)
# clear 5 assignments for first user
# can't do a limit in "delete" since postgres does not support it
for d in frappe.get_all('ToDo', dict(reference_type = 'Note', owner = 'test@example.com'), limit=5):
frappe.db.sql("delete from tabToDo where name = %s", d.name)
# add 5 more assignments
for i in range(5):
make_note(dict(public=1))
# check if each user still has 10 assignments
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'):
self.assertEqual(len(frappe.get_all('ToDo', dict(owner = user, reference_type = 'Note'))), 10)
def test_assign_condition(self):
# check condition
note = make_note(dict(public=0))
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), None)
def test_clear_assignment(self):
note = make_note(dict(public=1))
# check if auto assigned to first user
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), 'test@example.com')
# test auto unassign
note.public = 0
note.save()
# check if cleared
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), None)
def check_multiple_rules(self):
note = make_note(dict(public=1, notify_on_login=1))
# check if auto assigned to test3 (2nd rule is applied, as it has higher priority)
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), 'test@example.com')
def clear_assignments():
frappe.db.sql("delete from tabToDo where reference_type = 'Note'")
def get_assignment_rule():
frappe.delete_doc_if_exists('Assignment Rule', 'For Note 1')
assignment_rule = frappe.get_doc(dict(
name = 'For Note 1',
doctype = 'Assignment Rule',
priority = 0,
document_type = 'Note',
assign_condition = 'public == 1',
unassign_condition = 'public == 0',
rule = 'Round Robin',
users = [
dict(user = 'test@example.com'),
dict(user = 'test1@example.com'),
dict(user = 'test2@example.com'),
]
)).insert()
frappe.delete_doc_if_exists('Assignment Rule', 'For Note 2')
# 2nd rule
frappe.get_doc(dict(
name = 'For Note 2',
doctype = 'Assignment Rule',
priority = 1,
document_type = 'Note',
assign_condition = 'notify_on_login == 1',
unassign_condition = 'notify_on_login == 0',
rule = 'Round Robin',
users = [
dict(user = 'test3@example.com')
]
)).insert()
return assignment_rule
def make_note(values=None):
note = frappe.get_doc(dict(
doctype = 'Note',
title = random_string(10),
content = random_string(20)
))
if values:
note.update(values)
note.insert()
return note

View file

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class AutoAssignUser(Document):
class AssignmentRuleUser(Document):
pass

View file

@ -10,10 +10,10 @@ from frappe.desk.form import assign_to
class AutoAssign(Document):
def on_update(self): # pylint: disable=no-self-use
frappe.cache().delete_value('auto_assign')
frappe.cache().delete_value('assignment_rule')
def after_rename(self): # pylint: disable=no-self-use
frappe.cache().delete_value('auto_assign')
frappe.cache().delete_value('assignment_rule')
def apply(self, doc):
if self.safe_eval('assign_condition', doc):
@ -97,12 +97,12 @@ def apply(doc, method):
if frappe.flags.in_patch or frappe.flags.in_install:
return
auto_assigns = frappe.cache().get_value('auto_assign', get_auto_assigns)
if doc.doctype in auto_assigns:
assignment_rules = frappe.cache().get_value('assignment_rule', get_assignment_rules)
if doc.doctype in assignment_rules:
# multiple auto assigns
for d in frappe.db.get_all('Auto Assign', dict(document_type=doc.doctype, disabled = 0), order_by = 'priority desc'):
if frappe.get_doc('Auto Assign', d.name).apply(doc.as_dict()):
for d in frappe.db.get_all('Assignment Rule', dict(document_type=doc.doctype, disabled = 0), order_by = 'priority desc'):
if frappe.get_doc('Assignment Rule', d.name).apply(doc.as_dict()):
break
def get_auto_assigns():
return [d.document_type for d in frappe.db.get_all('Auto Assign', fields=['document_type'], filters=dict(disabled = 0))]
def get_assignment_rules():
return [d.document_type for d in frappe.db.get_all('Assignment Rule', fields=['document_type'], filters=dict(disabled = 0))]

View file

@ -9,7 +9,7 @@ from frappe.utils import random_string
class TestAutoAssign(unittest.TestCase):
def setUp(self):
self.auto_assign = get_auto_assign()
self.assignment_rule = get_assignment_rule()
clear_assignments()
def test_round_robin(self):
@ -53,8 +53,8 @@ class TestAutoAssign(unittest.TestCase):
), 'owner'), 'test@example.com')
def test_load_balancing(self):
self.auto_assign.rule = 'Load Balancing'
self.auto_assign.save()
self.assignment_rule.rule = 'Load Balancing'
self.assignment_rule.save()
for _ in range(30):
note = make_note(dict(public=1))
@ -121,12 +121,12 @@ class TestAutoAssign(unittest.TestCase):
def clear_assignments():
frappe.db.sql("delete from tabToDo where reference_type = 'Note'")
def get_auto_assign():
frappe.delete_doc_if_exists('Auto Assign', 'For Note 1')
def get_assignment_rule():
frappe.delete_doc_if_exists('Assignment Rule', 'For Note 1')
auto_assign = frappe.get_doc(dict(
assignment_rule = frappe.get_doc(dict(
name = 'For Note 1',
doctype = 'Auto Assign',
doctype = 'Assignment Rule',
priority = 0,
document_type = 'Note',
assign_condition = 'public == 1',
@ -139,12 +139,12 @@ def get_auto_assign():
]
)).insert()
frappe.delete_doc_if_exists('Auto Assign', 'For Note 2')
frappe.delete_doc_if_exists('Assignment Rule', 'For Note 2')
# 2nd rule
frappe.get_doc(dict(
name = 'For Note 2',
doctype = 'Auto Assign',
doctype = 'Assignment Rule',
priority = 1,
document_type = 'Note',
assign_condition = 'notify_on_login == 1',
@ -156,7 +156,7 @@ def get_auto_assign():
)).insert()
return auto_assign
return assignment_rule
def make_note(values=None):
note = frappe.get_doc(dict(

View file

@ -40,7 +40,7 @@ def clear_global_cache():
frappe.cache().delete_value(["app_hooks", "installed_apps",
"app_modules", "module_app", "notification_config", 'system_settings',
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
'active_modules', 'auto_assign'])
'active_modules', 'assignment_rule'])
frappe.setup_module_map()
def clear_defaults_cache(user=None):

View file

@ -169,20 +169,13 @@ def get_data():
"name": "Workflow Action",
"description": _("Actions for workflow (e.g. Approve, Cancel).")
},
{
"type": "doctype",
"name": "Assignment Rule",
"description": _("Set up rules for user assignments.")
}
]
},
{
"label": _("Applications"),
"items":[
{
"type": "page",
"name": "applications",
"label": _("Application Installer"),
"description": _("Install Applications."),
"icon": "fa fa-download"
},
]
}
]
add_setup_section(data, "frappe", "website", _("Website"), "fa fa-globe")
return data

View file

@ -119,7 +119,7 @@ doc_events = {
"frappe.desk.notifications.clear_doctype_notifications",
"frappe.core.doctype.activity_log.feed.update_feed",
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions",
"frappe.automation.doctype.auto_assign.auto_assign.apply"
"frappe.automation.doctype.assignment_rule.assignment_rule.apply"
],
"after_rename": "frappe.desk.notifications.clear_doctype_notifications",
"on_cancel": [