Sync Workflows
Synchronize Automail with Autoform in a controlled way
Sync workflows make use of the generic do_bulk_sync
function, which was designed to:
Generate new Autoform links from Automail.
Update existing Autoform links by doing a smart merge of its content with what exists in Automail. The merging algorithm compares the Autoform creation timestamp with the update timestamps of each data point to know whether to keep what exists in Autoform or to overwrite with Automail data.
Load filled Autoform links back into Automail. This works for data in form, table, and file formats.
Send emails to recipient email addresses based on a flag logic.
All configurations for a specific workflow must be within the same py file.
For example, all configurations related to the Material Delivery process must be inside autoform_materialdelivery.py)
All models used for monitoring the sync process must contain the word Monitor contained in their class name and inherit the abstract mixin parent class polygon.apps.app_autoform.models.AutoformRequestMixin.
How to use it?
Example
try:
from _init import * # DEV env
except ImportError:
from configurations.workflow_env import * # PROD env
from polygon.apps.app_autoform.sync import do_bulk_sync
def get_field_definitions_ecopack_primary_packaging():
field_definitions_ecopack_primary_packaging = global_variables.field_definitions_ecopack_primary_packaging
valid_material_list = []
for cookie_category in custom_models.Cookie.objects.all():
valid_material_list.append(
{
"conditions": [
{
"function": "eq",
"params": {
"value": "category__label",
"target": cookie_category.label,
},
},
{
"function": "in",
"params": {
"value": "material__name",
"list": list(
custom_models.CookieMaterial.objects.filter(
material_type=cookie_category.material_type,
).values_list(
"name",
flat=True,
)
.distinct()
),
},
},
],
"value": True,
},
)
field_definitions_ecopack_primary_packaging["material__name"]["configured_validations"] = [
{
"function": "valid",
"message": "The selected material is not valid for the packaging type of the same row.",
"params": {
"if": valid_material_list,
"else": {
"value": False,
},
},
}
]
return field_definitions_ecopack_primary_packaging
def get_autoform_navs(monitor):
autoform_navs = []
def get_hanger_card_elements(card_number):
card_config = []
hanger_reference_config = {
"item_name": "reference",
"class": "col-6",
"type": "select",
"options_lists": "hanger_reference_options_config",
"attributes": {
"required": True,
"data-live-search": "true",
"title_map": {"en": "Select One..."},
"focusout": "update_options",
},
"related_item_ref": f"hanger_supplier_size_{card_number}",
"model": custom_models.EcoPackMonitorPrimaryPackaging,
}
hanger_supplier_size_config = {
"item_ref": f"hanger_supplier_size_{card_number}",
"item_name": "supplier_size",
"class": "col-6",
"type": "select",
"attributes": {
"required": True,
"data-live-search": "true",
"title_map": {"en": "Select One..."},
},
"model": custom_models.EcoPackMonitorPrimaryPackaging,
}
card_config.append(hanger_reference_config)
card_config.append(hanger_supplier_size_config)
return card_config
def add_primary_packaging_tab(monitor):
def get_hanger_group_children():
hanger_children = []
hanger_children.append(
{
"item_name": "has_hanger",
"label_map": {
"en": "Packaging that is part of the product<br><strong>Is there a referenced hanger?</strong>"
},
"attributes": {
"focusout": "append",
},
"model": custom_models.EcoPackMonitorPrimaryPackaging,
"children": [
{
"parent_value": True,
"category": "card",
"class": "mb-3",
"title_map": {"en": f"Select your Hanger Configurations"},
"description_map": {
"en": "Please select in the order of <strong>Reference > Supplier (Size)</strong>"
},
"children": [
{
"category": "group",
"class": "card-body mr-0 ml-0 form-row my-0 py-0 border-top pt-3",
"children": get_hanger_card_elements(1),
}
],
"model": custom_models.EcoPackMonitorPrimaryPackaging,
"load_method": {
"function": "upsert",
"params": {
"key_fields": {
"ecopackmonitor_id": lambda monitor: monitor.id,
},
"defaults": {
"reference": {
"function": "get_child_item_value",
},
"supplier_size": {
"function": "get_child_item_value",
},
},
},
},
},
],
},
)
return hanger_children
autoform_navs.append(
{
"label_map": {"en": "Primary Packaging"},
"children": [
{
"item_ref": "g_hanger_append",
"category": "group",
"class": "group_to_append",
"children": get_hanger_group_children(),
},
{
"item_name": "primary_packaging",
"label_map": {
"en": """
<strong>Instructions</strong>
<ul>
<li><small>Please note that you should declare your hanger (the plastic hanger and the metal hook if there is) in this table ONLY if your hanger ref + supplier is not present in the above hanger section </small></li>
<li><small>If there is an inner polybag (inner packing), DO NOT declare it in the table below</small></li>
</ul>
<strong>Declare OTHER Primary Packaging Categories in the Table Below (like hangtags, plastic ties etc. )</strong>
"""
},
"attributes": {"required": True},
"table_props": {
"model": custom_models.EcoPackForm,
"queryset": custom_models.EcoPackForm.objects.filter(
ecopackmonitor=monitor
),
"field_definitions": get_field_definitions_ecopack_primary_packaging(),
"target_rows": 20,
"table_settings": {
"permissions": {
"delete": {
"allowed": True,
}
},
},
},
},
],
},
)
def add_secondary_packaging_tab():
def get_secondary_packaging_questions():
questions = []
questions.append(
{
"item_name": "has_inner_polybag",
"label_map": {
"en": "Secondary packaging is the packaging of the master & inner cartons<br><strong>Do you use an inner polybag (not master carton polybag) for the SPCB & inner packing?</strong>"
},
"model": custom_models.EcoPackMonitorSecondaryPackaging,
}
)
questions.append(
{
"item_name": "master_carton_weight",
"model": custom_models.EcoPackMonitorSecondaryPackaging,
"type": "text",
"validations": {
"datatype": "float",
"numeric_filter": {
"gte": 50,
"lte": 50000,
},
},
"helper_text_map": {"en": "Numbers only"},
}
)
return questions
autoform_navs.append(
{
"item_name": "secondary_packaging",
"label_map": {"en": "Secondary Packaging"},
"children": [
{
"category": "group",
"children": get_secondary_packaging_questions(),
"model": custom_models.EcoPackMonitorSecondaryPackaging,
"load_method": {
"function": "upsert",
"params": {
"key_fields": {
"ecopackmonitor_id": lambda monitor: monitor.id,
},
"defaults": {
"has_inner_polybag": {
"function": "get_child_item_value",
},
"master_carton_weight": {
"function": "get_child_item_value",
},
},
},
},
},
],
}
)
add_primary_packaging_tab(monitor)
add_secondary_packaging_tab()
# etc.
return autoform_navs
if __name__ == "__main__": # Check if script is being run directly and execute the following block if true
do_bulk_sync(
params={
"queryset": custom_models.Monitor.objects.all().order_by("id"), # queryset always on the Monitor model and used for both push and pull!
"language": lambda monitor: monitor.autoform_language if monitor.autoform_language else "en",
"sender_email": settings.EMAIL_USERNAME_SEND,
"workflows": {
"push": {
"keep_wip": True, # powerful feature to keep the work in progress (WIP) of the Autoform
"conditions": {
"start": lambda monitor.is_in_queue,
"refresh": lambda monitor: monitor.autoform_hash is None or monitor.to_be_refreshed,
"email": True, # high-level condition to control whether any email is sent (email-specific conditions are defined at email level below)
},
"emails": {
"initial_email": {
"condition": lambda monitor: monitor.sent is False,
"email_receivers": {
"to": get_email_receivers,
"cc": get_email_receivers_additional_cc,
},
"email_content": {
"subject": {
"en": lambda monitor: f"{settings.COMPANY_NAME} {monitor.suppliergroup.supplier_name} - {monitor.season} - {monitor.product.product_code} Cookie",
},
"body": {
"en": lambda monitor: f"""
<p>Dear <strong>{monitor.suppliergroup.supplier_name}</strong>,</p>
<br />
<p>You are expected to provide the information for your primary, secondary and tertiary packaging for reference <strong>{monitor.product.product_code}</strong> of <strong>{monitor.season}</strong>.</p>
<br />
<p>Please click on the link to open the form</p>
<p>{monitor.autoform_link_button}</p>
<br />
<p>We are expecting the form to be submitted before <strong style="color:#ff6384">{monitor.deadline_verbose}</strong>.</p>
<br />
<p>Thanks for your cooperation.</p>
<br />
<p>Best Regards,</p>
<p>Sourcing Team</p>
<p>{settings.COMPANY_NAME}</p>
""",
},
},
"procedures": [
lambda monitor: setattr(
monitor, "sent", True
),
set_sent_timestamp,
lambda monitor: setattr(
monitor,
"status",
"Pending",
),
],
},
"reminder_email": {
"condition": lambda monitor: monitor.reminder_sent is False,
"email_receivers": {
"to": get_email_receivers,
"cc": get_email_receivers_additional_cc,
},
"email_content": {
"subject": {
"en": lambda monitor: f"{monitor.suppliergroup.supplier_name} - {monitor.season} - {monitor.product.product_code} Cookie Reminder",
},
"body": {
"en": lambda monitor: f"""
<p>Dear <strong>{monitor.suppliergroup.supplier_name}</strong>,</p>
<br />
<p>You are reminded to provide the information for your primary, secondary and tertiary packaging for reference <strong>{monitor.product.product_code}</strong> of <strong>{monitor.season}</strong>.</p>
<br />
<p>Please click on the link to open the form</p>
<p>{monitor.autoform_link_button}</p>
<br />
<p>We are expecting the form to be submitted before <strong style="color:#ff6384">{monitor.deadline_verbose}</strong>.</p>
<br />
<p>Thank you for your cooperation.</p>
<br />
<p>Best Regards,</p>
<p>Sourcing Team</p>
<p>{settings.COMPANY_NAME}</p>
""",
},
},
"procedures": [
lambda monitor: setattr(
monitor, "reminder_sent ", False
),
set_sent_timestamp,
],
},
},
"procedures": {
"before_refresh": set_deadline,
"during_refresh": translate_autoform_config,
"after_refresh": [
lambda monitor: setattr(
monitor, "to_be_refreshed", False
),
lambda monitor: setattr(monitor, "autoform_loaded", False),
],
"before_email": set_monitor_email_variables,
"after_email": [
lambda monitor: setattr(
monitor,
"email_description_prefix",
"Email sent to",
),
create_ecopackmonitorevent,
],
"before_save": lambda monitor: setattr(
monitor, "is_in_queue", False
),
"after_save": None,
},
},
"pull": {
"conditions": {
"start": True,
"load": True,
"refresh": True, # to refresh the Autoform link right after loading
"opened_check": False,
"email": False,
},
"emails": {
"success": {
"condition": lambda monitor: monitor.autoform_loaded is True
and monitor.success_email_sent is False,
"email_content": {
"subject": {
"en": lambda monitor: f"Successful Submission of {monitor.season.name} Cookie Form - {monitor.suppliergroup.supplier_name} (ref:{monitor.product.product_code})",
},
"body": {
"en": lambda monitor: f"""
<p>Dear {monitor.suppliergroup.supplier_name},</p>
<p>Thank you for you submission.</p>
<p>
We have received the <strong>{monitor.season}</strong> <strong>Cookie Form</strong> for reference <strong>{monitor.product.product_code}</strong>
on <u>{monitor.sent_d}</u> at <u>{monitor.sent_t}</u> ({settings.TIME_ZONE} time)
for the <strong>{monitor.period}</strong>.
</p>
<p>{monitor.autoform_link_button_print}</p>
<p style="color:#36ebaa;">
No further action is required.
</p>
<p>Thank you!</p>
""",
},
},
}
},
"procedures": {
"before_load": None,
"during_load_label_value_list": None,
"during_load_table": None,
"after_load": [
lambda monitor: setattr(monitor, "autoform_loaded", True),
lambda monitor: setattr(monitor, "is_in_queue", True),
lambda monitor: setattr(
monitor,
"status",
"Accepted",
),
],
"before_email": None,
"after_email": [
lambda monitor: setattr(
monitor,
"email_description_prefix",
"Success email sent to",
),
create_ecopackmonitorevent,
],
"before_opened_check": None,
"after_opened_check": [
lambda monitor: setattr(
monitor,
"autoform_opened",
True,
),
lambda monitor: setattr(
monitor,
"status",
"Link Opened",
),
],
"before_save": [
set_monitor_status_and_stats,
],
"after_save": None,
},
},
},
"autoform_config": {
"autoform_frame": {
"chat": {
"active": True,
"application": "automail-lv-cookie",
"module": "Cookie Module",
"context": "This is for suppliers to declare all the packaging they have used in the 1 specified product.",
},
"stage": "prod",
"keep_open": True,
"grid": "neat",
"logo": "static/uploads/Lineverge/logo.png",
"logo_width": "120px",
"title_map": {
"en": "Cookie",
},
"subtitle": lambda monitor: f"""
<strong>{monitor.suppliergroup.supplier_name}</strong><br />
{monitor.season} - {monitor.product.product_code}
""",
"description_map": {"en": f"Please select all the items you use in your packaging."},
"successful_submit_subtitle_map": {
"en": "Your answers have been submitted, the form will stay open in case you need to make further changes. <br/><br/>Redirecting in 10 seconds... <meta http-equiv='refresh' content='10'>",
},
"successful_submit_description_map": {
"en": f"For questions or support please contact your contact point.",
},
"languages": [
{"code": "en", "label": "English"},
{"code": "cn", "label": "简体ä¸æ–‡"},
{"code": "tr", "label": "Türk"},
],
"downloads": [],
},
"autoform_lists": {
"hanger_reference_options_config": get_hanger_reference_options_config(),
},
"autoform_navs": get_autoform_navs,
},
},
)
Important design considerations
do_bulk_sync
has 1 input parameter params, which is a dictionary.
The sync feature has a validation function that will prevent do_bulk_sync
from running if it detects anomalies (i.e. the execution is exited through asserts).
The value of each key and sub-key in params can either be any non-callable Python object (e.g. int, str, dict, list, etc.) or a function.
All functions must have the input parameter monitor, which refers to the current monitor row from the queryset.
Lambda functions are short, inline, unnamed functions defined using the lambda
keyword, whereas regular functions are defined using the def
keyword and can have multiple statements.
For simple cases such as getting or setting a monitor field, use an inline Python lambda function. For more complex cases, use regular Python functions.
In most cases, it is better to have your custom functions executed within sync. To do so, pass the function as a parameter without compiling it in the py file.
In the example code, get_hanger_reference_options_config
will execute directly in the py file as the function is provided using the parenthesis:
"hanger_reference_options_config": get_hanger_reference_options_config(),
Whereas in the following code, get_hanger_reference_options_config
would be executed later in the sync as the function is provided without the parenthesis:
"hanger_reference_options_config": get_hanger_reference_options_config,
get_hanger_reference_options_config
would then be defined within the py file with the input param monitor:
def get_hanger_reference_options_config(monitor):
hanger_reference_list = [
value for value in list(
custom_models.Hanger.objects.filter(material__material_type="PLASTIC")
.values_list("reference", flat=True)
.distinct()
)
]
hanger_reference_options_config = []
for reference in hanger_reference_list:
option = {
"item_value": reference,
"label_map": {"en": reference},
"options": [
{"item_value": value, "label": value}
for value in custom_models.Hanger.objects.filter(reference=reference, material__material_type="PLASTIC")
.values_list("search_reference_suggestion_text", flat=True)
.distinct()
],
}
hanger_reference_options_config.append(option)
return hanger_reference_options_config
monitor refers to a given row of queryset. The input parameter of any provided function would always be monitor even if the model was called MonitorEcoPack.
If additional variables are needed within the workflow, you can add them to the monitor object even if they are not fields defined in the corresponding Monitor model. This is done in the example above for deadline_verbose
, which is set in set_monitor_email_variables to be used as email variables, but it is not a field in monitor.
Last updated