django-unicom 25.3.45.dev0__tar.gz → 25.3.45.dev1__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.dev1}/PKG-INFO +44 -2
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/README.md +43 -1
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/django_unicom.egg-info/PKG-INFO +44 -2
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/django_unicom.egg-info/SOURCES.txt +13 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/models.py +11 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/services/llm_handler.py +41 -16
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/_version.py +2 -2
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/models/draft_message.py +2 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/models/message.py +34 -6
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/email/send_email_message.py +31 -0
- django_unicom-25.3.45.dev1/unicom/services/template_renderer.py +140 -0
- django_unicom-25.3.45.dev1/unicom/tests/test_email_tracking_unsubscribe.py +10 -0
- django_unicom-25.3.45.dev1/unicom/tests/test_template_renderer_context.py +35 -0
- django_unicom-25.3.45.dev1/unicom/tests/test_template_renderer_unicom.py +31 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/views/compose_view.py +2 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/views/email_tracking.py +28 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/admin.py +6 -13
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/apps.py +19 -31
- django_unicom-25.3.45.dev1/unicrm/forms.py +267 -0
- django_unicom-25.3.45.dev1/unicrm/migrations/0021_fix_unsubscribe_counts.py +22 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/models.py +9 -11
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/seed_data.py +81 -8
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/services/__init__.py +7 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/services/audience.py +7 -3
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/services/audience_preview.py +7 -2
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/services/communication_scheduler.py +49 -0
- django_unicom-25.3.45.dev1/unicrm/services/disk_usage_monitor.py +84 -0
- django_unicom-25.3.45.dev1/unicrm/services/disk_usage_monitor_runner.py +99 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/services/getprospect_email_poller.py +22 -2
- django_unicom-25.3.45.dev1/unicrm/services/lead_search.py +279 -0
- django_unicom-25.3.45.dev1/unicrm/services/red_alerts.py +243 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/services/template_renderer.py +8 -56
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/tests/test_communication_scheduler.py +53 -9
- django_unicom-25.3.45.dev1/unicrm/tests/test_disk_usage_monitor.py +43 -0
- django_unicom-25.3.45.dev1/unicrm/tests/test_disk_usage_monitor_runner.py +40 -0
- django_unicom-25.3.45.dev1/unicrm/tests/test_red_alerts.py +122 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/tests/test_retry_delivery_api.py +26 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/urls.py +3 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/views_api.py +7 -1
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/views_communication.py +42 -3
- django_unicom-25.3.45.dev1/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.dev1}/.cursor/rules/match_codebase_style.mdc +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/.cursor/rules/preserve_comments_and_irrelevant_code.mdc +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/.cursor/rules/require_context_before_changes.mdc +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/Dockerfile +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/MANIFEST.in +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/Makefile +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/conftest.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/data/definitions/bots/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/data/definitions/tools/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/django_unicom.egg-info/dependency_links.txt +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/django_unicom.egg-info/requires.txt +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/django_unicom.egg-info/top_level.txt +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/docker-compose.yaml +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/entrypoint.sh +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/internal-channel-modernization.md +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/manage.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/pyproject.toml +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/pytest.ini +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/requirements.txt +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/scripts/reacher_validate.sh +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/setup.cfg +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/setup.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/tests/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/tests/test_cross_platform_buttons.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/tests/test_email_authentication.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/tests/test_email_live.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/tests/test_email_request_processing.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/tests/test_telegram_live.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/tests/test_webchat.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/tests/test_webchat_branching.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/tests/utils.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/todo.md +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/apps.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/migrations/0001_initial.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/migrations/0002_tool_bot_tools.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/migrations/0003_encryptedcredential.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/migrations/0004_credentialfielddefinition_and_more.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/migrations/0005_alter_credentialfielddefinition_key.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/migrations/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/services/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/services/credentials.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/services/tool_exceptions.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/services/tool_result_handler.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/templatetags/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/templatetags/unibot_extras.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/tests.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/urls.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unibot/views.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/admin/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/admin/account_admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/admin/channel_admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/admin/chat_admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/admin/draft_message_admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/admin/email_inline_image_admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/admin/filters.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/admin/member_admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/admin/message_admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/admin/message_template_admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/admin/request_admin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/apps.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/consumers/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/consumers/webchat_consumer.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/management/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/management/commands/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/management/commands/mail_inbox_listen.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/management/commands/run_as_llm_chat.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/management/commands/send_scheduled_messages.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/management/commands/start_imap_listeners.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/migrations/0001_initial.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/migrations/0002_emailinlineimage.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/migrations/0003_alter_emailinlineimage_email_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/migrations/0004_messagetemplateinlineimage.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/migrations/0005_emailinlineimage_hash_and_more.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/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.dev1}/unicom/migrations/0007_message_imap_uid.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/migrations/0008_add_all_members_group.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/migrations/0009_add_tool_call_types.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/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.dev1}/unicom/migrations/0011_toolcall_add_message_reference.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/migrations/0012_message_response_to_tool_call_and_more.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/migrations/0013_callbackexecution.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/migrations/0014_rename_authorized_user_to_intended_account.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/migrations/0015_simplify_callback_execution.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/migrations/0016_callbackexecution_tool_call.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/migrations/0017_add_chat_metadata.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/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.dev1}/unicom/migrations/0019_draftmessage_skip_reacher_validation.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/migrations/0020_message_provider_message_id.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/migrations/0021_message_open_count.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/migrations/0022_toolcall_result_status.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/migrations/0023_toolcall_progress_updates_for_user.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/migrations/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/models/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/models/account.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/models/account_chat.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/models/callback_execution.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/models/channel.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/models/chat.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/models/constants.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/models/fields.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/models/member.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/models/member_group.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/models/message_template.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/models/request.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/models/request_category.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/models/tool_call.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/models/update.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/chat_summary.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/crossplatform/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/crossplatform/reply_to_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/crossplatform/scheduler.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/crossplatform/send_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/decode_base64_image.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/email/IMAP_thread_manager.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/email/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/email/email_tracking.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/email/listen_to_IMAP.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/email/quote_filter.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/email/replace_cid_images_with_base64.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/email/save_email_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/email/validate_email_config.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/get_public_origin.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/html_inline_images.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/internal/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/internal/generate_text_message_data.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/internal/save_internal_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/internal/send_internal_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/llm/README.md +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/llm/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/llm/tool_calls.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/telegram/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/telegram/answer_callback_query.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/telegram/create_inline_keyboard.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/telegram/download_file.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/telegram/edit_telegram_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/telegram/escape_markdown.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/telegram/get_file_path.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/telegram/handle_telegram_callback.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/telegram/save_telegram_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/telegram/send_telegram_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/telegram/set_telegram_webhook.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/telegram/start_typing_in_telegram.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/telegram/stop_typing_in_telegram.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/webchat/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/webchat/generate_chat_title.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/webchat/get_or_create_account.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/webchat/migrate_guest_to_user.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/webchat/save_webchat_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/webchat/send_webchat_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/whatsapp/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/whatsapp/get_template.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/whatsapp/save_whatsapp_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/whatsapp/save_whatsapp_message_status.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/services/whatsapp/send_whatsapp_message.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/signals.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/static/unicom/css/bootstrap_scoped.css +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/static/unicom/css/draft_message_mobile.css +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/static/unicom/js/channel_config.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/static/unicom/js/tinymce_ai_template.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/static/unicom/js/tinymce_init.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/static/unicom/webchat/components/chat-list.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/static/unicom/webchat/components/media-preview.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/static/unicom/webchat/components/message-input.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/static/unicom/webchat/components/message-item.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/static/unicom/webchat/components/message-list.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/static/unicom/webchat/components/voice-recorder.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/static/unicom/webchat/utils/api-client.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/static/unicom/webchat/utils/datetime-formatter.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/static/unicom/webchat/utils/font-awesome-loader.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/static/unicom/webchat/utils/realtime-client.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/static/unicom/webchat/webchat-component.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/static/unicom/webchat/webchat-styles.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/static/unicom/webchat/webchat-with-sidebar.js +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/templates/admin/unicom/chat/change_list.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/templates/admin/unicom/chat/compose.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/templates/admin/unicom/chat_history.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/templates/admin/unicom/forms/email_message_form.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/templates/admin/unicom/forms/text_message_form.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/templates/admin/unicom/includes/ai_template_modal.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/templates/admin/unicom/includes/loading_indicators.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/templates/admin/unicom/includes/message_actions_menu.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/templates/admin/unicom/messagetemplate/change_form.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/templates/code_templates/category_processor.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/templates/unicom/webchat_demo.html +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/templatetags/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/templatetags/unicom_tags.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/urls.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/views/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/views/chat_history_view.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/views/inline_image.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/views/message_template.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/views/telegram_webhook.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/views/webchat_demo_view.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/views/webchat_views.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom/views/whatsapp_webhook.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom_project/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom_project/apps.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom_project/asgi.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom_project/callback_handlers.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom_project/definitions/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom_project/definitions/bots/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom_project/definitions/bots/assistant_bot.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom_project/definitions/tools/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom_project/definitions/tools/cross_platform_buttons.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom_project/definitions/tools/get_system_info.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom_project/definitions/tools/interactive_menu.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom_project/definitions/tools/interval_alarm.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom_project/definitions/tools/ip_lookup.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom_project/definitions/tools/simple_timer.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom_project/settings.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom_project/test_button_handlers.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom_project/urls.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicom_project/wsgi.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/management/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/management/commands/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/management/commands/refresh_contact_cache.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/management/commands/send_scheduled_communications.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/management/commands/sync_getprospect_saved_contacts.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/migrations/0001_initial.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/migrations/0002_alter_communicationmessage_unique_together_and_more.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/migrations/0003_remove_template_add_content.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/migrations/0004_alter_contact_email.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/migrations/0005_contact_user_alter_communication_status_summary.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/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.dev1}/unicrm/migrations/0007_auto_enroll_and_status.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/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.dev1}/unicrm/migrations/0009_remove_unused_draft_field.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/migrations/0010_communication_evergreen_refreshed_at.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/migrations/0011_add_unique_constraints_to_company.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/migrations/0012_company_attributes_company_industry.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/migrations/0013_contact_insight_id.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/migrations/0014_contact_gp_fields.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/migrations/0015_contact_history_unsubscribe_all.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/migrations/0016_add_unsubscribe_template_variable.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/migrations/0017_unsubscribe_link_html_variable.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/migrations/0018_communication_mailing_list.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/migrations/0019_communication_follow_up_for_segment_for_followup.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/migrations/0020_communication_skip_antispam_guards.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/migrations/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/services/communication_dispatcher.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/services/communication_enrollment.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/services/communication_runner.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/services/contact_interaction_cache.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/services/save_new_leads_callbacks.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/services/unsubscribe_links.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/services/user_contact_sync.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/signals.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/templatetags/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/templatetags/unicrm_extras.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/tests/__init__.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/tests/test_communication_compose_view.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/tests/test_communication_dispatcher.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/tests/test_contact_interactions.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/tests/test_template_renderer.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/tests/test_unsubscribe_view.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/tests/test_user_contact_sync.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/unicrm/views.py +0 -0
- {django_unicom-25.3.45.dev0 → django_unicom-25.3.45.dev1}/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.dev1
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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.dev1
|
|
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
|
|
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.
|
|
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.dev1}/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.dev1'
|
|
32
|
+
__version_tuple__ = version_tuple = (25, 3, 45, 'dev1')
|
|
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,10 @@ 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
|
+
)
|
|
20
24
|
|
|
21
25
|
logger = logging.getLogger(__name__)
|
|
22
26
|
|
|
@@ -275,6 +279,33 @@ def send_email_message(channel: Channel, params: dict, user: User=None):
|
|
|
275
279
|
html_content = convert_text_to_html(text_content)
|
|
276
280
|
logger.debug("Converted plain text to HTML")
|
|
277
281
|
|
|
282
|
+
# Optionally render templates for non-CRM use-cases when explicitly requested.
|
|
283
|
+
render_context = params.get('render_context')
|
|
284
|
+
render_variables = params.get('render_variables')
|
|
285
|
+
render_requested = params.get('render_template') or render_context or render_variables
|
|
286
|
+
if render_requested and html_content:
|
|
287
|
+
base_context = render_context or build_unicom_message_context(
|
|
288
|
+
params=params,
|
|
289
|
+
channel={
|
|
290
|
+
'id': channel.id,
|
|
291
|
+
'name': channel.name,
|
|
292
|
+
'platform': channel.platform,
|
|
293
|
+
},
|
|
294
|
+
user={
|
|
295
|
+
'id': getattr(user, 'id', None),
|
|
296
|
+
'username': getattr(user, 'username', None),
|
|
297
|
+
'email': getattr(user, 'email', None),
|
|
298
|
+
} if user else {},
|
|
299
|
+
)
|
|
300
|
+
render_result = render_unicom_template(
|
|
301
|
+
html_content,
|
|
302
|
+
base_context=base_context,
|
|
303
|
+
variables=render_variables or {},
|
|
304
|
+
)
|
|
305
|
+
if render_result.errors:
|
|
306
|
+
logger.warning("Template rendering errors: %s", "; ".join(render_result.errors))
|
|
307
|
+
html_content = render_result.html
|
|
308
|
+
|
|
278
309
|
# Add tracking
|
|
279
310
|
if html_content:
|
|
280
311
|
html_content = to_inline_png_img(html_content) # Convert FontAwesome to inline images
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from unicom.views.email_tracking import is_unsubscribe_url
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_is_unsubscribe_url_detects_relative_and_absolute(settings):
|
|
5
|
+
settings.UNICRM_UNSUBSCRIBE_PATH = '/unicrm/unsubscribe/'
|
|
6
|
+
assert is_unsubscribe_url('/unicrm/unsubscribe/?token=abc')
|
|
7
|
+
assert is_unsubscribe_url('https://example.com/unicrm/unsubscribe/?token=abc')
|
|
8
|
+
assert is_unsubscribe_url('https://example.com/unicrm/unsubscribe/extra/path')
|
|
9
|
+
assert not is_unsubscribe_url('https://example.com/other/path')
|
|
10
|
+
assert not is_unsubscribe_url('/different/path')
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from unicom.services.template_renderer import (
|
|
4
|
+
build_unicom_message_context,
|
|
5
|
+
render_template,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_build_unicom_message_context_includes_message_channel_sender():
|
|
10
|
+
params = {
|
|
11
|
+
"subject": "Hi",
|
|
12
|
+
"html": "<p>Hello {{ variables.name }}</p>",
|
|
13
|
+
"to": ["a@example.com"],
|
|
14
|
+
"reply_to_message_id": "msg-123",
|
|
15
|
+
}
|
|
16
|
+
channel = {"id": 1, "name": "Main", "platform": "Email"}
|
|
17
|
+
user = {"id": 9, "username": "alice", "email": "alice@example.com"}
|
|
18
|
+
ctx = build_unicom_message_context(params=params, channel=channel, user=user)
|
|
19
|
+
assert ctx["message"]["subject"] == "Hi"
|
|
20
|
+
assert ctx["message"]["to"] == ["a@example.com"]
|
|
21
|
+
assert ctx["channel"]["platform"] == "Email"
|
|
22
|
+
assert ctx["sender"]["username"] == "alice"
|
|
23
|
+
assert ctx["message"]["reply_to_message_id"] == "msg-123"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_render_template_with_built_context_and_variables():
|
|
27
|
+
params = {"html": "<p>Hello {{ variables.name }}</p>"}
|
|
28
|
+
ctx = build_unicom_message_context(params=params, channel={}, user={})
|
|
29
|
+
result = render_template(
|
|
30
|
+
params["html"],
|
|
31
|
+
base_context=ctx,
|
|
32
|
+
variables={"name": "Bob"},
|
|
33
|
+
)
|
|
34
|
+
assert result.html == "<p>Hello Bob</p>"
|
|
35
|
+
assert result.variables["name"] == "Bob"
|