django-unicom 25.3.45.dev0__tar.gz → 25.3.45.dev2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/PKG-INFO +44 -2
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/README.md +43 -1
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/django_unicom.egg-info/PKG-INFO +44 -2
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/django_unicom.egg-info/SOURCES.txt +13 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/models.py +11 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/services/llm_handler.py +41 -16
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/_version.py +2 -2
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/models/draft_message.py +2 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/models/message.py +34 -6
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/email/send_email_message.py +46 -0
- django_unicom-25.3.45.dev2/unicom/services/template_renderer.py +190 -0
- django_unicom-25.3.45.dev2/unicom/tests/test_email_tracking_unsubscribe.py +10 -0
- django_unicom-25.3.45.dev2/unicom/tests/test_template_renderer_context.py +35 -0
- django_unicom-25.3.45.dev2/unicom/tests/test_template_renderer_unicom.py +31 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/views/compose_view.py +2 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/views/email_tracking.py +28 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/admin.py +6 -13
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/apps.py +19 -31
- django_unicom-25.3.45.dev2/unicrm/forms.py +267 -0
- django_unicom-25.3.45.dev2/unicrm/migrations/0021_fix_unsubscribe_counts.py +22 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/models.py +9 -11
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/seed_data.py +81 -8
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/services/__init__.py +7 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/services/audience.py +7 -3
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/services/audience_preview.py +7 -2
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/services/communication_scheduler.py +49 -0
- django_unicom-25.3.45.dev2/unicrm/services/disk_usage_monitor.py +84 -0
- django_unicom-25.3.45.dev2/unicrm/services/disk_usage_monitor_runner.py +99 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/services/getprospect_email_poller.py +22 -2
- django_unicom-25.3.45.dev2/unicrm/services/lead_search.py +279 -0
- django_unicom-25.3.45.dev2/unicrm/services/red_alerts.py +243 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/services/template_renderer.py +8 -56
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/tests/test_communication_scheduler.py +53 -9
- django_unicom-25.3.45.dev2/unicrm/tests/test_disk_usage_monitor.py +43 -0
- django_unicom-25.3.45.dev2/unicrm/tests/test_disk_usage_monitor_runner.py +40 -0
- django_unicom-25.3.45.dev2/unicrm/tests/test_red_alerts.py +122 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/tests/test_retry_delivery_api.py +26 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/urls.py +3 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/views_api.py +7 -1
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/views_communication.py +42 -3
- django_unicom-25.3.45.dev2/unicrm/views_lead_search.py +125 -0
- django_unicom-25.3.45.dev0/unicrm/forms.py +0 -139
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/.cursor/rules/match_codebase_style.mdc +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/.cursor/rules/preserve_comments_and_irrelevant_code.mdc +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/.cursor/rules/require_context_before_changes.mdc +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/Dockerfile +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/MANIFEST.in +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/Makefile +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/conftest.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/data/definitions/bots/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/data/definitions/tools/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/django_unicom.egg-info/dependency_links.txt +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/django_unicom.egg-info/requires.txt +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/django_unicom.egg-info/top_level.txt +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/docker-compose.yaml +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/entrypoint.sh +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/internal-channel-modernization.md +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/manage.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/pyproject.toml +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/pytest.ini +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/requirements.txt +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/scripts/reacher_validate.sh +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/setup.cfg +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/setup.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/tests/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/tests/test_cross_platform_buttons.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/tests/test_email_authentication.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/tests/test_email_live.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/tests/test_email_request_processing.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/tests/test_telegram_live.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/tests/test_webchat.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/tests/test_webchat_branching.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/tests/utils.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/todo.md +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/apps.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/migrations/0001_initial.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/migrations/0002_tool_bot_tools.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/migrations/0003_encryptedcredential.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/migrations/0004_credentialfielddefinition_and_more.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/migrations/0005_alter_credentialfielddefinition_key.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/migrations/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/services/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/services/credentials.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/services/tool_exceptions.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/services/tool_result_handler.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/templatetags/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/templatetags/unibot_extras.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/tests.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/urls.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unibot/views.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/admin/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/admin/account_admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/admin/channel_admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/admin/chat_admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/admin/draft_message_admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/admin/email_inline_image_admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/admin/filters.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/admin/member_admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/admin/message_admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/admin/message_template_admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/admin/request_admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/apps.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/consumers/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/consumers/webchat_consumer.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/management/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/management/commands/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/management/commands/mail_inbox_listen.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/management/commands/run_as_llm_chat.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/management/commands/send_scheduled_messages.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/management/commands/start_imap_listeners.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0001_initial.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0002_emailinlineimage.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0003_alter_emailinlineimage_email_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0004_messagetemplateinlineimage.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0005_emailinlineimage_hash_and_more.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0006_channel_created_at_channel_created_by_and_more.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0007_message_imap_uid.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0008_add_all_members_group.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0009_add_tool_call_types.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0010_request_initial_request_request_llm_calls_count_and_more.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0011_toolcall_add_message_reference.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0012_message_response_to_tool_call_and_more.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0013_callbackexecution.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0014_rename_authorized_user_to_intended_account.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0015_simplify_callback_execution.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0016_callbackexecution_tool_call.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0017_add_chat_metadata.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0018_message_bounce_details_message_bounce_reason_and_more.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0019_draftmessage_skip_reacher_validation.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0020_message_provider_message_id.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0021_message_open_count.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0022_toolcall_result_status.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/0023_toolcall_progress_updates_for_user.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/migrations/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/models/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/models/account.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/models/account_chat.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/models/callback_execution.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/models/channel.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/models/chat.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/models/constants.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/models/fields.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/models/member.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/models/member_group.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/models/message_template.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/models/request.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/models/request_category.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/models/tool_call.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/models/update.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/chat_summary.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/crossplatform/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/crossplatform/reply_to_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/crossplatform/scheduler.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/crossplatform/send_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/decode_base64_image.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/email/IMAP_thread_manager.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/email/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/email/email_tracking.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/email/listen_to_IMAP.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/email/quote_filter.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/email/replace_cid_images_with_base64.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/email/save_email_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/email/validate_email_config.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/get_public_origin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/html_inline_images.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/internal/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/internal/generate_text_message_data.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/internal/save_internal_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/internal/send_internal_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/llm/README.md +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/llm/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/llm/tool_calls.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/telegram/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/telegram/answer_callback_query.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/telegram/create_inline_keyboard.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/telegram/download_file.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/telegram/edit_telegram_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/telegram/escape_markdown.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/telegram/get_file_path.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/telegram/handle_telegram_callback.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/telegram/save_telegram_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/telegram/send_telegram_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/telegram/set_telegram_webhook.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/telegram/start_typing_in_telegram.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/telegram/stop_typing_in_telegram.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/webchat/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/webchat/generate_chat_title.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/webchat/get_or_create_account.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/webchat/migrate_guest_to_user.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/webchat/save_webchat_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/webchat/send_webchat_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/whatsapp/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/whatsapp/get_template.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/whatsapp/save_whatsapp_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/whatsapp/save_whatsapp_message_status.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/services/whatsapp/send_whatsapp_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/signals.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/static/unicom/css/bootstrap_scoped.css +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/static/unicom/css/draft_message_mobile.css +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/static/unicom/js/channel_config.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/static/unicom/js/tinymce_ai_template.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/static/unicom/js/tinymce_init.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/static/unicom/webchat/components/chat-list.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/static/unicom/webchat/components/media-preview.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/static/unicom/webchat/components/message-input.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/static/unicom/webchat/components/message-item.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/static/unicom/webchat/components/message-list.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/static/unicom/webchat/components/voice-recorder.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/static/unicom/webchat/utils/api-client.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/static/unicom/webchat/utils/datetime-formatter.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/static/unicom/webchat/utils/font-awesome-loader.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/static/unicom/webchat/utils/realtime-client.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/static/unicom/webchat/webchat-component.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/static/unicom/webchat/webchat-styles.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/static/unicom/webchat/webchat-with-sidebar.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/templates/admin/unicom/chat/change_list.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/templates/admin/unicom/chat/compose.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/templates/admin/unicom/chat_history.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/templates/admin/unicom/forms/email_message_form.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/templates/admin/unicom/forms/text_message_form.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/templates/admin/unicom/includes/ai_template_modal.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/templates/admin/unicom/includes/loading_indicators.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/templates/admin/unicom/includes/message_actions_menu.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/templates/admin/unicom/messagetemplate/change_form.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/templates/code_templates/category_processor.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/templates/unicom/webchat_demo.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/templatetags/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/templatetags/unicom_tags.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/urls.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/views/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/views/chat_history_view.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/views/inline_image.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/views/message_template.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/views/telegram_webhook.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/views/webchat_demo_view.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/views/webchat_views.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom/views/whatsapp_webhook.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom_project/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom_project/apps.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom_project/asgi.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom_project/callback_handlers.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom_project/definitions/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom_project/definitions/bots/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom_project/definitions/bots/assistant_bot.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom_project/definitions/tools/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom_project/definitions/tools/cross_platform_buttons.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom_project/definitions/tools/get_system_info.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom_project/definitions/tools/interactive_menu.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom_project/definitions/tools/interval_alarm.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom_project/definitions/tools/ip_lookup.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom_project/definitions/tools/simple_timer.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom_project/settings.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom_project/test_button_handlers.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom_project/urls.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicom_project/wsgi.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/management/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/management/commands/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/management/commands/refresh_contact_cache.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/management/commands/send_scheduled_communications.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/management/commands/sync_getprospect_saved_contacts.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0001_initial.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0002_alter_communicationmessage_unique_together_and_more.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0003_remove_template_add_content.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0004_alter_contact_email.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0005_contact_user_alter_communication_status_summary.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0006_contact_email_bounce_type_contact_email_bounced_and_more.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0007_auto_enroll_and_status.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0008_rename_unicrm_msg_status_sched_idx_unicrm_comm_status_2f3c8e_idx_and_more.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0009_remove_unused_draft_field.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0010_communication_evergreen_refreshed_at.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0011_add_unique_constraints_to_company.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0012_company_attributes_company_industry.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0013_contact_insight_id.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0014_contact_gp_fields.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0015_contact_history_unsubscribe_all.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0016_add_unsubscribe_template_variable.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0017_unsubscribe_link_html_variable.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0018_communication_mailing_list.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0019_communication_follow_up_for_segment_for_followup.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/0020_communication_skip_antispam_guards.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/migrations/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/services/communication_dispatcher.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/services/communication_enrollment.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/services/communication_runner.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/services/contact_interaction_cache.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/services/save_new_leads_callbacks.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/services/unsubscribe_links.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/services/user_contact_sync.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/signals.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/templatetags/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/templatetags/unicrm_extras.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/tests/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/tests/test_communication_compose_view.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/tests/test_communication_dispatcher.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/tests/test_contact_interactions.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/tests/test_template_renderer.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/tests/test_unsubscribe_view.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/tests/test_user_contact_sync.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/views.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/unicrm/views_public.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-unicom
|
|
3
|
-
Version: 25.3.45.
|
|
3
|
+
Version: 25.3.45.dev2
|
|
4
4
|
Summary: Unified communication layer for Django (Telegram, WhatsApp, Email)
|
|
5
5
|
Home-page: https://github.com/meena-erian/unicom
|
|
6
6
|
Author: Meena (Menas) Erian
|
|
@@ -539,7 +539,7 @@ reply = chat.send_message({
|
|
|
539
539
|
|
|
540
540
|
### Template System
|
|
541
541
|
|
|
542
|
-
Create reusable message templates for consistent communication.
|
|
542
|
+
Create reusable message templates for consistent communication, and optionally render Jinja-style template variables for richer personalization.
|
|
543
543
|
|
|
544
544
|
#### Creating Templates Programmatically
|
|
545
545
|
|
|
@@ -595,6 +595,48 @@ message = channel.send_message({
|
|
|
595
595
|
})
|
|
596
596
|
```
|
|
597
597
|
|
|
598
|
+
#### Template Variables & Rendering
|
|
599
|
+
|
|
600
|
+
There are two layers of templating:
|
|
601
|
+
- **Unicom rendering (standalone messages):** Jinja2 rendering with a safe Unicom context. It is used automatically for ad-hoc/admin email compose and scheduled drafts. Context exposed to templates: `message` (subject/html/text/to/cc/bcc/attachments/chat_id/reply_to_message_id/timestamp), `channel` (id/name/platform), and `sender` (id/username/email when available). You can add more by passing `render_variables` (merged into `variables.*`) or `render_context` when calling `channel.send_message`.
|
|
602
|
+
- **Unicrm rendering (mass-mail):** Jinja2 rendering with CRM data. Context exposed: `contact` (and nested `company`, `subscriptions`), `communication`, and `variables` (all active CRM TemplateVariables), plus `variables.unsubscribe_link` (HTML link) when a Communication is provided. This path is unchanged and remains tied to CRM mailings.
|
|
603
|
+
|
|
604
|
+
Using variables in templates (applies to both renderers):
|
|
605
|
+
```html
|
|
606
|
+
<h1>Hello {{ variables.first_name }}</h1>
|
|
607
|
+
<p>You’re receiving this on {{ now()|datetime("%Y-%m-%d") }}</p>
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
Creating CRM TemplateVariables (only available in CRM Communications):
|
|
611
|
+
```python
|
|
612
|
+
from unicrm.models import TemplateVariable
|
|
613
|
+
|
|
614
|
+
TemplateVariable.objects.create(
|
|
615
|
+
key="contact_first_name",
|
|
616
|
+
label="Contact first name",
|
|
617
|
+
description="Returns the contact's first name",
|
|
618
|
+
code="""
|
|
619
|
+
def compute(contact):
|
|
620
|
+
return (contact.first_name or '').strip() or 'there'
|
|
621
|
+
""",
|
|
622
|
+
is_active=True,
|
|
623
|
+
)
|
|
624
|
+
```
|
|
625
|
+
The callable must be `compute(contact)` and can access `contact`, `contact.company`, and helpers like `build_unsubscribe_link` (for unsubscribe variables). Values become available as `{{ variables.contact_first_name }}` in CRM emails.
|
|
626
|
+
|
|
627
|
+
Opting into rendering for custom sends (programmatic):
|
|
628
|
+
```python
|
|
629
|
+
channel.send_message({
|
|
630
|
+
'to': ['a@example.com'],
|
|
631
|
+
'subject': 'Hello',
|
|
632
|
+
'html': '<p>Hello {{ variables.name }}</p>',
|
|
633
|
+
'render_template': True, # enable rendering (admin email compose & scheduled drafts set this for you)
|
|
634
|
+
'render_variables': {'name': 'Alice'}, # merged into variables.*
|
|
635
|
+
# Optionally pass a custom render_context if you want to add more fields
|
|
636
|
+
})
|
|
637
|
+
```
|
|
638
|
+
If you omit `render_template`/context/variables, the HTML is sent as-is. The Django admin email composer and scheduled drafts already enable rendering by default; you only need to pass the flag when sending programmatically.
|
|
639
|
+
|
|
598
640
|
### Draft Messages & Scheduling
|
|
599
641
|
|
|
600
642
|
Create draft messages and schedule them for later sending.
|
|
@@ -495,7 +495,7 @@ reply = chat.send_message({
|
|
|
495
495
|
|
|
496
496
|
### Template System
|
|
497
497
|
|
|
498
|
-
Create reusable message templates for consistent communication.
|
|
498
|
+
Create reusable message templates for consistent communication, and optionally render Jinja-style template variables for richer personalization.
|
|
499
499
|
|
|
500
500
|
#### Creating Templates Programmatically
|
|
501
501
|
|
|
@@ -551,6 +551,48 @@ message = channel.send_message({
|
|
|
551
551
|
})
|
|
552
552
|
```
|
|
553
553
|
|
|
554
|
+
#### Template Variables & Rendering
|
|
555
|
+
|
|
556
|
+
There are two layers of templating:
|
|
557
|
+
- **Unicom rendering (standalone messages):** Jinja2 rendering with a safe Unicom context. It is used automatically for ad-hoc/admin email compose and scheduled drafts. Context exposed to templates: `message` (subject/html/text/to/cc/bcc/attachments/chat_id/reply_to_message_id/timestamp), `channel` (id/name/platform), and `sender` (id/username/email when available). You can add more by passing `render_variables` (merged into `variables.*`) or `render_context` when calling `channel.send_message`.
|
|
558
|
+
- **Unicrm rendering (mass-mail):** Jinja2 rendering with CRM data. Context exposed: `contact` (and nested `company`, `subscriptions`), `communication`, and `variables` (all active CRM TemplateVariables), plus `variables.unsubscribe_link` (HTML link) when a Communication is provided. This path is unchanged and remains tied to CRM mailings.
|
|
559
|
+
|
|
560
|
+
Using variables in templates (applies to both renderers):
|
|
561
|
+
```html
|
|
562
|
+
<h1>Hello {{ variables.first_name }}</h1>
|
|
563
|
+
<p>You’re receiving this on {{ now()|datetime("%Y-%m-%d") }}</p>
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Creating CRM TemplateVariables (only available in CRM Communications):
|
|
567
|
+
```python
|
|
568
|
+
from unicrm.models import TemplateVariable
|
|
569
|
+
|
|
570
|
+
TemplateVariable.objects.create(
|
|
571
|
+
key="contact_first_name",
|
|
572
|
+
label="Contact first name",
|
|
573
|
+
description="Returns the contact's first name",
|
|
574
|
+
code="""
|
|
575
|
+
def compute(contact):
|
|
576
|
+
return (contact.first_name or '').strip() or 'there'
|
|
577
|
+
""",
|
|
578
|
+
is_active=True,
|
|
579
|
+
)
|
|
580
|
+
```
|
|
581
|
+
The callable must be `compute(contact)` and can access `contact`, `contact.company`, and helpers like `build_unsubscribe_link` (for unsubscribe variables). Values become available as `{{ variables.contact_first_name }}` in CRM emails.
|
|
582
|
+
|
|
583
|
+
Opting into rendering for custom sends (programmatic):
|
|
584
|
+
```python
|
|
585
|
+
channel.send_message({
|
|
586
|
+
'to': ['a@example.com'],
|
|
587
|
+
'subject': 'Hello',
|
|
588
|
+
'html': '<p>Hello {{ variables.name }}</p>',
|
|
589
|
+
'render_template': True, # enable rendering (admin email compose & scheduled drafts set this for you)
|
|
590
|
+
'render_variables': {'name': 'Alice'}, # merged into variables.*
|
|
591
|
+
# Optionally pass a custom render_context if you want to add more fields
|
|
592
|
+
})
|
|
593
|
+
```
|
|
594
|
+
If you omit `render_template`/context/variables, the HTML is sent as-is. The Django admin email composer and scheduled drafts already enable rendering by default; you only need to pass the flag when sending programmatically.
|
|
595
|
+
|
|
554
596
|
### Draft Messages & Scheduling
|
|
555
597
|
|
|
556
598
|
Create draft messages and schedule them for later sending.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-unicom
|
|
3
|
-
Version: 25.3.45.
|
|
3
|
+
Version: 25.3.45.dev2
|
|
4
4
|
Summary: Unified communication layer for Django (Telegram, WhatsApp, Email)
|
|
5
5
|
Home-page: https://github.com/meena-erian/unicom
|
|
6
6
|
Author: Meena (Menas) Erian
|
|
@@ -539,7 +539,7 @@ reply = chat.send_message({
|
|
|
539
539
|
|
|
540
540
|
### Template System
|
|
541
541
|
|
|
542
|
-
Create reusable message templates for consistent communication.
|
|
542
|
+
Create reusable message templates for consistent communication, and optionally render Jinja-style template variables for richer personalization.
|
|
543
543
|
|
|
544
544
|
#### Creating Templates Programmatically
|
|
545
545
|
|
|
@@ -595,6 +595,48 @@ message = channel.send_message({
|
|
|
595
595
|
})
|
|
596
596
|
```
|
|
597
597
|
|
|
598
|
+
#### Template Variables & Rendering
|
|
599
|
+
|
|
600
|
+
There are two layers of templating:
|
|
601
|
+
- **Unicom rendering (standalone messages):** Jinja2 rendering with a safe Unicom context. It is used automatically for ad-hoc/admin email compose and scheduled drafts. Context exposed to templates: `message` (subject/html/text/to/cc/bcc/attachments/chat_id/reply_to_message_id/timestamp), `channel` (id/name/platform), and `sender` (id/username/email when available). You can add more by passing `render_variables` (merged into `variables.*`) or `render_context` when calling `channel.send_message`.
|
|
602
|
+
- **Unicrm rendering (mass-mail):** Jinja2 rendering with CRM data. Context exposed: `contact` (and nested `company`, `subscriptions`), `communication`, and `variables` (all active CRM TemplateVariables), plus `variables.unsubscribe_link` (HTML link) when a Communication is provided. This path is unchanged and remains tied to CRM mailings.
|
|
603
|
+
|
|
604
|
+
Using variables in templates (applies to both renderers):
|
|
605
|
+
```html
|
|
606
|
+
<h1>Hello {{ variables.first_name }}</h1>
|
|
607
|
+
<p>You’re receiving this on {{ now()|datetime("%Y-%m-%d") }}</p>
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
Creating CRM TemplateVariables (only available in CRM Communications):
|
|
611
|
+
```python
|
|
612
|
+
from unicrm.models import TemplateVariable
|
|
613
|
+
|
|
614
|
+
TemplateVariable.objects.create(
|
|
615
|
+
key="contact_first_name",
|
|
616
|
+
label="Contact first name",
|
|
617
|
+
description="Returns the contact's first name",
|
|
618
|
+
code="""
|
|
619
|
+
def compute(contact):
|
|
620
|
+
return (contact.first_name or '').strip() or 'there'
|
|
621
|
+
""",
|
|
622
|
+
is_active=True,
|
|
623
|
+
)
|
|
624
|
+
```
|
|
625
|
+
The callable must be `compute(contact)` and can access `contact`, `contact.company`, and helpers like `build_unsubscribe_link` (for unsubscribe variables). Values become available as `{{ variables.contact_first_name }}` in CRM emails.
|
|
626
|
+
|
|
627
|
+
Opting into rendering for custom sends (programmatic):
|
|
628
|
+
```python
|
|
629
|
+
channel.send_message({
|
|
630
|
+
'to': ['a@example.com'],
|
|
631
|
+
'subject': 'Hello',
|
|
632
|
+
'html': '<p>Hello {{ variables.name }}</p>',
|
|
633
|
+
'render_template': True, # enable rendering (admin email compose & scheduled drafts set this for you)
|
|
634
|
+
'render_variables': {'name': 'Alice'}, # merged into variables.*
|
|
635
|
+
# Optionally pass a custom render_context if you want to add more fields
|
|
636
|
+
})
|
|
637
|
+
```
|
|
638
|
+
If you omit `render_template`/context/variables, the HTML is sent as-is. The Django admin email composer and scheduled drafts already enable rendering by default; you only need to pass the flag when sending programmatically.
|
|
639
|
+
|
|
598
640
|
### Draft Messages & Scheduling
|
|
599
641
|
|
|
600
642
|
Create draft messages and schedule them for later sending.
|
{django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev2}/django_unicom.egg-info/SOURCES.txt
RENAMED
|
@@ -122,6 +122,7 @@ unicom/services/chat_summary.py
|
|
|
122
122
|
unicom/services/decode_base64_image.py
|
|
123
123
|
unicom/services/get_public_origin.py
|
|
124
124
|
unicom/services/html_inline_images.py
|
|
125
|
+
unicom/services/template_renderer.py
|
|
125
126
|
unicom/services/crossplatform/__init__.py
|
|
126
127
|
unicom/services/crossplatform/reply_to_message.py
|
|
127
128
|
unicom/services/crossplatform/scheduler.py
|
|
@@ -197,6 +198,9 @@ unicom/templates/code_templates/category_processor.py
|
|
|
197
198
|
unicom/templates/unicom/webchat_demo.html
|
|
198
199
|
unicom/templatetags/__init__.py
|
|
199
200
|
unicom/templatetags/unicom_tags.py
|
|
201
|
+
unicom/tests/test_email_tracking_unsubscribe.py
|
|
202
|
+
unicom/tests/test_template_renderer_context.py
|
|
203
|
+
unicom/tests/test_template_renderer_unicom.py
|
|
200
204
|
unicom/views/__init__.py
|
|
201
205
|
unicom/views/chat_history_view.py
|
|
202
206
|
unicom/views/compose_view.py
|
|
@@ -236,6 +240,7 @@ unicrm/urls.py
|
|
|
236
240
|
unicrm/views.py
|
|
237
241
|
unicrm/views_api.py
|
|
238
242
|
unicrm/views_communication.py
|
|
243
|
+
unicrm/views_lead_search.py
|
|
239
244
|
unicrm/views_public.py
|
|
240
245
|
unicrm/management/__init__.py
|
|
241
246
|
unicrm/management/commands/__init__.py
|
|
@@ -262,6 +267,7 @@ unicrm/migrations/0017_unsubscribe_link_html_variable.py
|
|
|
262
267
|
unicrm/migrations/0018_communication_mailing_list.py
|
|
263
268
|
unicrm/migrations/0019_communication_follow_up_for_segment_for_followup.py
|
|
264
269
|
unicrm/migrations/0020_communication_skip_antispam_guards.py
|
|
270
|
+
unicrm/migrations/0021_fix_unsubscribe_counts.py
|
|
265
271
|
unicrm/migrations/__init__.py
|
|
266
272
|
unicrm/services/__init__.py
|
|
267
273
|
unicrm/services/audience.py
|
|
@@ -271,7 +277,11 @@ unicrm/services/communication_enrollment.py
|
|
|
271
277
|
unicrm/services/communication_runner.py
|
|
272
278
|
unicrm/services/communication_scheduler.py
|
|
273
279
|
unicrm/services/contact_interaction_cache.py
|
|
280
|
+
unicrm/services/disk_usage_monitor.py
|
|
281
|
+
unicrm/services/disk_usage_monitor_runner.py
|
|
274
282
|
unicrm/services/getprospect_email_poller.py
|
|
283
|
+
unicrm/services/lead_search.py
|
|
284
|
+
unicrm/services/red_alerts.py
|
|
275
285
|
unicrm/services/save_new_leads_callbacks.py
|
|
276
286
|
unicrm/services/template_renderer.py
|
|
277
287
|
unicrm/services/unsubscribe_links.py
|
|
@@ -283,6 +293,9 @@ unicrm/tests/test_communication_compose_view.py
|
|
|
283
293
|
unicrm/tests/test_communication_dispatcher.py
|
|
284
294
|
unicrm/tests/test_communication_scheduler.py
|
|
285
295
|
unicrm/tests/test_contact_interactions.py
|
|
296
|
+
unicrm/tests/test_disk_usage_monitor.py
|
|
297
|
+
unicrm/tests/test_disk_usage_monitor_runner.py
|
|
298
|
+
unicrm/tests/test_red_alerts.py
|
|
286
299
|
unicrm/tests/test_retry_delivery_api.py
|
|
287
300
|
unicrm/tests/test_template_renderer.py
|
|
288
301
|
unicrm/tests/test_unsubscribe_view.py
|
|
@@ -441,6 +441,17 @@ class Bot(models.Model):
|
|
|
441
441
|
Process a single Request object using this bot's code, updating status and error fields as needed.
|
|
442
442
|
Passes the bot instance and its associated tools to the handler.
|
|
443
443
|
"""
|
|
444
|
+
# If this is a user interrupt immediately after a tool_call or tool_response (within 5 minutes), skip LLM processing. (The
|
|
445
|
+
# LLM will respond through the request created when the tool response arrives and this is to avoid double processing.)
|
|
446
|
+
if req.message.is_outgoing is False and req.message.media_type not in ['tool_call', 'tool_response']:
|
|
447
|
+
prev_msg = req.message.chat.messages.filter(timestamp__lt=req.message.timestamp).order_by('-timestamp').first()
|
|
448
|
+
if prev_msg and prev_msg.media_type in ['tool_call', 'tool_response']:
|
|
449
|
+
delta = (req.message.timestamp - prev_msg.timestamp).total_seconds()
|
|
450
|
+
if delta < 300:
|
|
451
|
+
req.status = 'COMPLETED'
|
|
452
|
+
req.save(update_fields=['status'])
|
|
453
|
+
return
|
|
454
|
+
|
|
444
455
|
if hasattr(req, 'parent_request') and req.parent_request:
|
|
445
456
|
print(f"[DEBUG] Processing child request {req.id} from parent {req.parent_request.id}")
|
|
446
457
|
bot_code = self.code or Bot.get_default_code()
|
|
@@ -59,31 +59,56 @@ def build_openai_tools(tools_list: Optional[List[object]], bot=None, message=Non
|
|
|
59
59
|
|
|
60
60
|
if auto_params:
|
|
61
61
|
auto_params_map[name] = auto_params
|
|
62
|
-
props = {}
|
|
63
|
-
required = []
|
|
62
|
+
props: dict = {}
|
|
63
|
+
required: list = []
|
|
64
64
|
for pname, pinfo in raw_params.items():
|
|
65
65
|
ptype = pinfo.get('type', 'string')
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
66
|
+
# Preserve enum/items for the LLM; map common aliases for scalars.
|
|
67
|
+
if isinstance(ptype, str):
|
|
68
|
+
if ptype in ('str', 'string'):
|
|
69
|
+
otype = 'string'
|
|
70
|
+
elif ptype in ('int', 'integer'):
|
|
71
|
+
otype = 'integer'
|
|
72
|
+
elif ptype in ('float', 'number'):
|
|
73
|
+
otype = 'number'
|
|
74
|
+
elif ptype in ('bool', 'boolean'):
|
|
75
|
+
otype = 'boolean'
|
|
76
|
+
else:
|
|
77
|
+
otype = ptype
|
|
74
78
|
else:
|
|
75
|
-
|
|
79
|
+
# Allow JSON Schema style: list/union types
|
|
80
|
+
otype = ptype
|
|
81
|
+
|
|
76
82
|
prop = {'type': otype}
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
# Carry through common JSON Schema fields if present.
|
|
84
|
+
for key in (
|
|
85
|
+
'description',
|
|
86
|
+
'enum',
|
|
87
|
+
'items',
|
|
88
|
+
'properties',
|
|
89
|
+
'additionalProperties',
|
|
90
|
+
'format',
|
|
91
|
+
'minimum',
|
|
92
|
+
'maximum',
|
|
93
|
+
'minItems',
|
|
94
|
+
'maxItems',
|
|
95
|
+
'anyOf',
|
|
96
|
+
'oneOf',
|
|
97
|
+
'allOf',
|
|
98
|
+
):
|
|
99
|
+
if key in pinfo:
|
|
100
|
+
prop[key] = pinfo[key]
|
|
79
101
|
if 'default' in pinfo:
|
|
80
102
|
prop['default'] = pinfo['default']
|
|
81
103
|
else:
|
|
82
104
|
required.append(pname)
|
|
83
105
|
props[pname] = prop
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
106
|
+
|
|
107
|
+
param_schema = {
|
|
108
|
+
'type': 'object',
|
|
109
|
+
'properties': props,
|
|
110
|
+
'additionalProperties': False,
|
|
111
|
+
}
|
|
87
112
|
if required:
|
|
88
113
|
param_schema['required'] = required
|
|
89
114
|
openai_tools.append({
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '25.3.45.
|
|
32
|
-
__version_tuple__ = version_tuple = (25, 3, 45, '
|
|
31
|
+
__version__ = version = '25.3.45.dev2'
|
|
32
|
+
__version_tuple__ = version_tuple = (25, 3, 45, 'dev2')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -180,6 +180,8 @@ class DraftMessage(models.Model):
|
|
|
180
180
|
msg_dict['chat_id'] = self.chat_id
|
|
181
181
|
if self.skip_reacher_validation:
|
|
182
182
|
msg_dict['skip_reacher'] = True
|
|
183
|
+
# Enable template rendering with the built-in Unicom context.
|
|
184
|
+
msg_dict['render_template'] = True
|
|
183
185
|
else:
|
|
184
186
|
msg_dict['chat_id'] = self.chat_id
|
|
185
187
|
msg_dict['text'] = self.text
|
|
@@ -445,6 +445,8 @@ class Message(models.Model):
|
|
|
445
445
|
break
|
|
446
446
|
chain.append(cur)
|
|
447
447
|
cur = cur.reply_to_message
|
|
448
|
+
# Sort chronologically to preserve call/response order
|
|
449
|
+
chain = sorted(chain, key=lambda m: m.timestamp)
|
|
448
450
|
|
|
449
451
|
# Handle user interruption for tool response messages in thread mode
|
|
450
452
|
if self.media_type == "tool_response":
|
|
@@ -465,14 +467,40 @@ class Message(models.Model):
|
|
|
465
467
|
).exclude(
|
|
466
468
|
media_type__in=['tool_call', 'tool_response']
|
|
467
469
|
).order_by('-timestamp').first()
|
|
470
|
+
|
|
471
|
+
# Fallback: any user message in that window, even without reply_to
|
|
472
|
+
if not user_interrupt:
|
|
473
|
+
user_interrupt = self.chat.messages.filter(
|
|
474
|
+
is_outgoing=False,
|
|
475
|
+
timestamp__gt=latest_tool_call_time,
|
|
476
|
+
timestamp__lt=self.timestamp
|
|
477
|
+
).exclude(
|
|
478
|
+
media_type__in=['tool_call', 'tool_response']
|
|
479
|
+
).order_by('-timestamp').first()
|
|
468
480
|
|
|
469
481
|
if user_interrupt:
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
482
|
+
if user_interrupt not in chain:
|
|
483
|
+
chain.append(user_interrupt)
|
|
484
|
+
chain = sorted(chain, key=lambda m: m.timestamp)
|
|
485
|
+
# Ensure tool_call and tool_response are adjacent
|
|
486
|
+
if self.media_type == "tool_response":
|
|
487
|
+
try:
|
|
488
|
+
call_id = self.raw.get('tool_response', {}).get('call_id')
|
|
489
|
+
if call_id:
|
|
490
|
+
tc_idx = next((i for i, m in enumerate(chain)
|
|
491
|
+
if m.media_type == "tool_call"
|
|
492
|
+
and m.raw.get('tool_call', {}).get('id') == call_id), None)
|
|
493
|
+
tr_idx = next((i for i, m in enumerate(chain)
|
|
494
|
+
if m.media_type == "tool_response"
|
|
495
|
+
and m.raw.get('tool_response', {}).get('call_id') == call_id), None)
|
|
496
|
+
if tc_idx is not None and tr_idx is not None and tr_idx != tc_idx + 1:
|
|
497
|
+
# Move tool_response directly after tool_call
|
|
498
|
+
tr = chain.pop(tr_idx)
|
|
499
|
+
chain.insert(tc_idx + 1, tr)
|
|
500
|
+
except Exception:
|
|
501
|
+
pass
|
|
502
|
+
|
|
503
|
+
for m in chain:
|
|
476
504
|
messages.append(msg_to_dict(m))
|
|
477
505
|
else:
|
|
478
506
|
raise ValueError(f"Unknown mode: {mode}")
|
|
@@ -17,6 +17,12 @@ import requests
|
|
|
17
17
|
from urllib.parse import urljoin
|
|
18
18
|
from django.utils import timezone
|
|
19
19
|
from unicom.services.html_inline_images import html_shortlinks_to_base64_images, html_base64_images_to_shortlinks
|
|
20
|
+
from unicom.services.template_renderer import (
|
|
21
|
+
render_template as render_unicom_template,
|
|
22
|
+
build_unicom_message_context,
|
|
23
|
+
extract_variable_keys,
|
|
24
|
+
compute_crm_variables,
|
|
25
|
+
)
|
|
20
26
|
|
|
21
27
|
logger = logging.getLogger(__name__)
|
|
22
28
|
|
|
@@ -275,6 +281,46 @@ def send_email_message(channel: Channel, params: dict, user: User=None):
|
|
|
275
281
|
html_content = convert_text_to_html(text_content)
|
|
276
282
|
logger.debug("Converted plain text to HTML")
|
|
277
283
|
|
|
284
|
+
# Optionally render templates for non-CRM use-cases when explicitly requested.
|
|
285
|
+
render_context = params.get('render_context')
|
|
286
|
+
render_variables = params.get('render_variables') or {}
|
|
287
|
+
render_requested = params.get('render_template') or render_context or render_variables
|
|
288
|
+
if render_requested and html_content:
|
|
289
|
+
crm_variables: dict[str, object] = {}
|
|
290
|
+
requested_keys = extract_variable_keys(html_content)
|
|
291
|
+
if requested_keys:
|
|
292
|
+
# Use first To address (if any) to resolve contact for CRM variables.
|
|
293
|
+
primary_email = None
|
|
294
|
+
to_addrs_for_crm = params.get('to') or to_addrs
|
|
295
|
+
if to_addrs_for_crm:
|
|
296
|
+
primary_email = (to_addrs_for_crm[0] or '').strip()
|
|
297
|
+
crm_variables = compute_crm_variables(requested_keys, primary_email)
|
|
298
|
+
|
|
299
|
+
base_context = render_context or build_unicom_message_context(
|
|
300
|
+
params=params,
|
|
301
|
+
channel={
|
|
302
|
+
'id': channel.id,
|
|
303
|
+
'name': channel.name,
|
|
304
|
+
'platform': channel.platform,
|
|
305
|
+
},
|
|
306
|
+
user={
|
|
307
|
+
'id': getattr(user, 'id', None),
|
|
308
|
+
'username': getattr(user, 'username', None),
|
|
309
|
+
'email': getattr(user, 'email', None),
|
|
310
|
+
} if user else {},
|
|
311
|
+
)
|
|
312
|
+
merged_variables = {}
|
|
313
|
+
merged_variables.update(crm_variables)
|
|
314
|
+
merged_variables.update(render_variables)
|
|
315
|
+
render_result = render_unicom_template(
|
|
316
|
+
html_content,
|
|
317
|
+
base_context=base_context,
|
|
318
|
+
variables=merged_variables,
|
|
319
|
+
)
|
|
320
|
+
if render_result.errors:
|
|
321
|
+
logger.warning("Template rendering errors: %s", "; ".join(render_result.errors))
|
|
322
|
+
html_content = render_result.html
|
|
323
|
+
|
|
278
324
|
# Add tracking
|
|
279
325
|
if html_content:
|
|
280
326
|
html_content = to_inline_png_img(html_content) # Convert FontAwesome to inline images
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import Any, Dict, Mapping
|
|
6
|
+
import re
|
|
7
|
+
from urllib.parse import unquote
|
|
8
|
+
|
|
9
|
+
from django.utils import timezone
|
|
10
|
+
from jinja2 import StrictUndefined, TemplateError
|
|
11
|
+
from jinja2.sandbox import SandboxedEnvironment
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Matches TinyMCE-protected placeholders such as "{{ variables.x }}" that TinyMCE wraps.
|
|
15
|
+
_PROTECTED_PLACEHOLDER_RE = re.compile(r'<!--\s*mce:protected\s+([^>]+?)-->')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _datetime_format(value, fmt: str = "%Y-%m-%d %H:%M %Z") -> str:
|
|
19
|
+
if not value:
|
|
20
|
+
return ""
|
|
21
|
+
if timezone.is_naive(value): # pragma: no cover - defensive branch
|
|
22
|
+
value = timezone.make_aware(value)
|
|
23
|
+
return timezone.localtime(value).strftime(fmt)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@lru_cache(maxsize=1)
|
|
27
|
+
def get_jinja_environment() -> SandboxedEnvironment:
|
|
28
|
+
"""
|
|
29
|
+
Returns a singleton sandboxed environment for rendering email templates.
|
|
30
|
+
Shared between Unicom and Unicrm to keep behavior consistent.
|
|
31
|
+
"""
|
|
32
|
+
env = SandboxedEnvironment(
|
|
33
|
+
autoescape=True,
|
|
34
|
+
trim_blocks=True,
|
|
35
|
+
lstrip_blocks=True,
|
|
36
|
+
undefined=StrictUndefined,
|
|
37
|
+
)
|
|
38
|
+
env.filters['datetime'] = _datetime_format
|
|
39
|
+
env.globals.update({
|
|
40
|
+
'now': timezone.now,
|
|
41
|
+
})
|
|
42
|
+
return env
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def unprotect_tinymce_markup(content: str | None) -> str:
|
|
46
|
+
"""
|
|
47
|
+
Restore TinyMCE protected placeholders ({{ ... }}) back to plain Jinja markup.
|
|
48
|
+
"""
|
|
49
|
+
if not content:
|
|
50
|
+
return content or ''
|
|
51
|
+
|
|
52
|
+
def _restore(match: re.Match[str]) -> str:
|
|
53
|
+
encoded = match.group(1).strip()
|
|
54
|
+
try:
|
|
55
|
+
return unquote(encoded)
|
|
56
|
+
except Exception: # pragma: no cover - defensive
|
|
57
|
+
return encoded
|
|
58
|
+
|
|
59
|
+
return _PROTECTED_PLACEHOLDER_RE.sub(_restore, content)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class RenderResult:
|
|
64
|
+
html: str
|
|
65
|
+
context: Dict[str, Any]
|
|
66
|
+
variables: Dict[str, Any]
|
|
67
|
+
errors: list[str]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def render_template(
|
|
71
|
+
template_html: str,
|
|
72
|
+
*,
|
|
73
|
+
base_context: Mapping[str, Any] | None = None,
|
|
74
|
+
variables: Mapping[str, Any] | None = None,
|
|
75
|
+
extra_context: Mapping[str, Any] | None = None,
|
|
76
|
+
) -> RenderResult:
|
|
77
|
+
"""
|
|
78
|
+
Render arbitrary HTML with a sandboxed Jinja2 environment.
|
|
79
|
+
|
|
80
|
+
- Does nothing destructive to callers: on template error, returns original HTML and records the error.
|
|
81
|
+
- Callers can pass `variables` to expose as `variables.*` in templates; optional extra context merges in.
|
|
82
|
+
"""
|
|
83
|
+
env = get_jinja_environment()
|
|
84
|
+
rendered_context: Dict[str, Any] = {}
|
|
85
|
+
if base_context:
|
|
86
|
+
rendered_context.update(base_context)
|
|
87
|
+
existing_vars = dict(rendered_context.get('variables') or {})
|
|
88
|
+
existing_vars.update(variables or {})
|
|
89
|
+
rendered_context['variables'] = existing_vars
|
|
90
|
+
|
|
91
|
+
if extra_context:
|
|
92
|
+
rendered_context.update(extra_context)
|
|
93
|
+
|
|
94
|
+
template_html = unprotect_tinymce_markup(template_html)
|
|
95
|
+
template = env.from_string(template_html)
|
|
96
|
+
errors: list[str] = []
|
|
97
|
+
try:
|
|
98
|
+
html = template.render(rendered_context)
|
|
99
|
+
except TemplateError as exc:
|
|
100
|
+
errors.append(str(exc))
|
|
101
|
+
html = template_html
|
|
102
|
+
return RenderResult(
|
|
103
|
+
html=html,
|
|
104
|
+
context=rendered_context,
|
|
105
|
+
variables=rendered_context['variables'],
|
|
106
|
+
errors=errors,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def build_unicom_message_context(
|
|
111
|
+
*,
|
|
112
|
+
params: Mapping[str, Any],
|
|
113
|
+
channel: Mapping[str, Any] | None = None,
|
|
114
|
+
user: Mapping[str, Any] | None = None,
|
|
115
|
+
extra: Mapping[str, Any] | None = None,
|
|
116
|
+
) -> Dict[str, Any]:
|
|
117
|
+
"""
|
|
118
|
+
Construct a safe, serialisable context for Unicom standalone messages.
|
|
119
|
+
Intentionally excludes CRM-only data (contacts, unsubscribe links).
|
|
120
|
+
"""
|
|
121
|
+
message_ctx = {
|
|
122
|
+
'subject': params.get('subject'),
|
|
123
|
+
'text': params.get('text'),
|
|
124
|
+
'html': params.get('html'),
|
|
125
|
+
'to': params.get('to'),
|
|
126
|
+
'cc': params.get('cc'),
|
|
127
|
+
'bcc': params.get('bcc'),
|
|
128
|
+
'attachments': params.get('attachments'),
|
|
129
|
+
'chat_id': params.get('chat_id'),
|
|
130
|
+
'reply_to_message_id': params.get('reply_to_message_id'),
|
|
131
|
+
'timestamp': timezone.now(),
|
|
132
|
+
}
|
|
133
|
+
ctx: Dict[str, Any] = {
|
|
134
|
+
'message': message_ctx,
|
|
135
|
+
'channel': channel or {},
|
|
136
|
+
'sender': user or {},
|
|
137
|
+
}
|
|
138
|
+
if extra:
|
|
139
|
+
ctx.update(extra)
|
|
140
|
+
return ctx
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
_VARIABLE_PLACEHOLDER_RE = re.compile(r"\{\{\s*variables\.([^\s\}]+)\s*\}\}")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def extract_variable_keys(template_html: str | None) -> set[str]:
|
|
147
|
+
"""
|
|
148
|
+
Return the set of variable keys referenced as {{ variables.* }} in the template.
|
|
149
|
+
"""
|
|
150
|
+
if not template_html:
|
|
151
|
+
return set()
|
|
152
|
+
unprotected = unprotect_tinymce_markup(template_html)
|
|
153
|
+
return {m.group(1) for m in _VARIABLE_PLACEHOLDER_RE.finditer(unprotected)}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _load_crm_models():
|
|
157
|
+
"""
|
|
158
|
+
Dynamically load CRM models if unicrm is installed; otherwise return (None, None).
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
from django.apps import apps
|
|
162
|
+
Contact = apps.get_model('unicrm', 'Contact')
|
|
163
|
+
TemplateVariable = apps.get_model('unicrm', 'TemplateVariable')
|
|
164
|
+
if Contact is None or TemplateVariable is None:
|
|
165
|
+
return None, None
|
|
166
|
+
return Contact, TemplateVariable
|
|
167
|
+
except Exception:
|
|
168
|
+
return None, None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def compute_crm_variables(keys: set[str], contact_email: str | None) -> Dict[str, Any]:
|
|
172
|
+
"""
|
|
173
|
+
Best-effort evaluation of CRM TemplateVariables for a contact resolved by email.
|
|
174
|
+
Safe to call when unicrm is not installed (returns {}).
|
|
175
|
+
"""
|
|
176
|
+
if not keys or not contact_email:
|
|
177
|
+
return {}
|
|
178
|
+
Contact, TemplateVariable = _load_crm_models()
|
|
179
|
+
if Contact is None or TemplateVariable is None:
|
|
180
|
+
return {}
|
|
181
|
+
contact = Contact.objects.filter(email__iexact=contact_email).first()
|
|
182
|
+
if not contact:
|
|
183
|
+
return {}
|
|
184
|
+
results: Dict[str, Any] = {}
|
|
185
|
+
for variable in TemplateVariable.objects.filter(is_active=True, key__in=keys):
|
|
186
|
+
try:
|
|
187
|
+
results[variable.key] = variable.get_callable()(contact)
|
|
188
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
189
|
+
results[variable.key] = f"<error: {exc}>"
|
|
190
|
+
return results
|